Für
Janne, Julia, Daniel und Katrin
Hans-Georg Schumann
Bibliografische Information der Deutschen Nationalbibliothek
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über <http://dnb.d-nb.de> abrufbar.
ISBN 978-3-95845-714-0
1. Auflage 2018
www.mitp.de
E-Mail: mitp-verlag@sigloch.de
Telefon: +49 7953 / 7189 - 079
Telefax: +49 7953 / 7189 - 082
© 2018 mitp Verlags GmbH & Co. KG
Dieses Werk, einschließlich aller seiner Teile, ist urheberrechtlich geschützt. Jede Verwertung außerhalb der engen Grenzen des Urheberrechtsgesetzes ist ohne Zustimmung des Verlages unzulässig und strafbar. Dies gilt insbesondere für Vervielfältigungen, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk berechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften.
Lektorat: Katja Völpel
Sprachkorrektorat: Petra Heubach-Erdmann
Covergestaltung: Sandrina Dralle, Christian Kalkert
Coverbild: © yayasya @ fotolia.com
electronic publication: III-satz, Husby, www.drei-satz.de
Dieses Ebook verwendet das ePub-Format und ist optimiert für die Nutzung mit dem iBooks-reader auf dem iPad von Apple. Bei der Verwendung anderer Reader kann es zu Darstellungsproblemen kommen.
Der Verlag räumt Ihnen mit dem Kauf des ebooks das Recht ein, die Inhalte im Rahmen des geltenden Urheberrechts zu nutzen. Dieses Werk, einschließlich aller seiner Teile, ist urheberrechtlich geschützt. Jede Verwertung außerhalb der engen Grenzen des Urheherrechtsgesetzes ist ohne Zustimmung des Verlages unzulässig und strafbar. Dies gilt insbesondere für Vervielfältigungen, Übersetzungen, Mikroverfilmungen und Einspeicherung und Verarbeitung in elektronischen Systemen.
Der Verlag schützt seine ebooks vor Missbrauch des Urheberrechts durch ein digitales Rechtemanagement. Bei Kauf im Webshop des Verlages werden die ebooks mit einem nicht sichtbaren digitalen Wasserzeichen individuell pro Nutzer signiert.
Bei Kauf in anderen ebook-Webshops erfolgt die Signatur durch die Shopbetreiber. Angaben zu diesem DRM finden Sie auf den Seiten der jeweiligen Anbieter.
Die Suche nach Fehlern hat so manchen Programmentwickler schon an den Rand des Wahnsinns getrieben. Zumal die schlimmsten meist so gut verborgen sind, dass man zuerst daran zweifelt, sie jemals zu finden. Gerade deshalb ist es gut und wichtig zu wissen, dass Visual Studio bemüht ist, dir dein Programmierleben mit C++ so bequem wie möglich zu machen, und darüber hinaus auch eine Reihe von Werkzeugen zur Fehlersuche bereitstellt. Aber du selbst kannst auch schon eine Menge tun, wenn du beim Erstellen deiner Programme ein paar Regeln beachtest.
Sind vielleicht scheinbare »Kleinigkeiten« wie z.B. Komma, Punkt, Semikolon vergessen worden?
Ist jede Anweisung mit einem Semikolon abgeschlossen?
Sind alle Blöcke einer Programm-Einheit (z.B. hinter if
, while
, do
, for
in Funktionen) mit geschweiften Klammern versehen?
Sind Bedingungen geklammert, haben Funktionen ihre nachfolgenden runden Klammern – auch wenn die Parameterlisten leer sind?
Wurden überall die richtigen Klammern verwendet – ()
, []
, {}
, <>
?
Können Bedingungen z.B. hinter if
oder while
überhaupt erfüllt werden?
Sind alle Variablen, Elemente, Funktionen (richtig) vereinbart?
Haben Variablen und Parameter, die weiterverarbeitet werden sollen, schon einen (sinnvollen) Wert?
Passt bei Zuweisungen der Typ links und rechts vom Zuweisungsoperator (=
)?
Wurde vielleicht in einer Bedingung der Zuweisungsoperator (=
) mit dem Gleichheitsoperator (==
) verwechselt?
Stimmen bei der Übergabe von Parametern an eine Funktion Typ und Anzahl überein?
Weil allzu viele Fehler so hinterlistig sind, sich nicht gleich zu zeigen, müssen sie erst einmal aufgespürt werden. In Visual Studio gibt es deshalb einige Hilfsmittel, die dir bei der Suche nach Fehlern helfen können.
Um genau zu beobachten, was ein Programm macht, kann es nützlich sein, dass jede Anweisung einzeln ausgeführt wird. Dann lässt sich der Programmlauf Schritt für Schritt mit einem Tastendruck steuern:
Starte das Programm nicht wie üblich, sondern drücke die Tastenkombination F10. Dann wird jede Anweisung einzeln ausgeführt.
So kannst du beobachten, was das Programm an welcher Stelle macht. (F10 entspricht dem Menüpunkt Debug/Prozedurschritt.)
Weil F10 Funktionen wie eine einzelne Anweisung betrachtet, wird eine Funktion (auch Prozedur genannt) in einem Schritt ausgeführt.
Deshalb gibt es noch die Möglichkeit, die Taste F11 zu nutzen. Damit werden auch die Anweisungen in einer Methode einzeln ausgeführt. (F11 entspricht dem Menüpunkt Debug/Einzelschritt.)
Falls du die schrittweise Programmausführung wieder abbrechen möchtest, bringst du sie im (geänderten) Debug-Menü über Debuggen beenden wieder zum Stillstand. (Das gilt auch, wenn das Programm in einem Laufzeitfehler steckt.)
Es kann auch nötig sein, ein Programm an genau einer bestimmten Stelle anzuhalten, um zu sehen, was bis dahin passiert ist. Dazu gibt es sogenannte Haltepunkte:
Setze den Textcursor in die Zeile, in der das Programm anhalten soll.
Drücke die Taste F9. (Oder klicke mit der Maus auf den linken Editorrand.)
Die betreffende Zeile ist nun markiert. Nach einem Start des Programms läuft es bis zu diesem Punkt und bleibt dann stehen.
Mit erneutem Klick auf Debug/Debuggen starten bzw. mit F5 läuft das Programm weiter – bis zum nächsten Haltepunkt (falls es einen gibt) oder bis zum Ende.
Ausschalten kannst du Haltepunkte, indem du den Textcursor darauf setzt und wieder die Taste F9 drückst (wie zum Einschalten).
Die Installation von Visual Studio ist recht einfach. Du installierst ein komplettes umfangreiches Paket, in dem die Programmiersprache C++ bereits enthalten ist. Dazu sind nur ein paar Schaltflächen anzuklicken, um die Installation zu steuern. Im Zweifelsfall kannst du dir aber auch von jemandem helfen lassen.
Zuerst aber musst du dir das benötigte Paket aus dem Internet holen. Es heißt Visual Studio Community und ist kostenlos (im Gegensatz zu den anderen Versionen). Was nicht heißt, dass es nicht auch für Profis geeignet ist.
Öffne nun deinen Browser und gib dort diese Adresse ein:
https://www.visualstudio.com/de/downloads/
Klicke dort unter Visual Studio Community auf Kostenloser Download.
Und kurze Zeit später landet das (noch ziemlich kleine) Startprogramm auf deiner Festplatte. Wahrscheinlich im Download-Ordner (je nachdem, was du für deinen Browser als Download-Ziel eingestellt hast).
Du kannst vom Browser aus über Ausführen das Setup starten. Oder du schließt den Browser wieder und öffnest das Fenster zu dem Ordner, in dem das Setup-Programm liegt.
Doppelklicke dort auf das Symbol für vs_community.exe.
Und das Setup-Programm legt gleich mit einer kurzen Begrüßung los:
Klicke einfach auf Weiter.
Nun heißt es einen Moment warten. Im nächsten Fenster kannst du dir aussuchen, welche Komponenten installiert werden sollen.
Für unsere Zwecke reicht das Paket Desktopentwicklung mit C++, du kannst aber gern mehr Komponenten (oder alles) hinzufügen.
Du hast hier auch die Möglichkeit, das Verzeichnis zu ändern, in das Visual Studio installiert wird. Ich schlage vor, den Speicherort so zu kürzen:
C:\Program Files (x86)\Microsoft Visual Studio\Community
bzw.
C:\Programme\Microsoft Visual Studio\Community
Abschließend klicke auf Installieren.
Und nun wird installiert, dabei kannst du an den Balken den Fortschritt der Installation ablesen.
Stelle dich auf eine (sehr) lange Wartezeit ein, bis alles installiert ist.
Zum Schluss siehst du im Fenster, dass die Installation geklappt hat:
Klicke auf Starten.
Es erscheint ein Dialogfeld, in dem dir angeboten wird, dich bei einem Microsoft-Konto anzumelden oder ein neues einzurichten.
Du kannst aber auch einfach auf Jetzt nicht, vielleicht später klicken.
Als Nächstes kannst du dir noch aussuchen, wie deine Arbeitsumgebung aussehen soll:
Lass den Vorschlag stehen oder wähle etwas anderes aus. Dann klicke auf Visual Studio starten.
Und einige Zeit später landest du in der Entwicklungsumgebung, in der du dann deine Projekte in Visual C++ erstellen kannst. Wie das geht, erfährst du ab Kapitel 1.
Für das SFML-Paket ist keine Extra-Installation nötig, aber herunterladen und entpacken musst du es trotzdem.
Dazu gehst du auf diese Download-Seite:
https://www.sfml-dev.org/download.php
Klicke auf SFML (Latest stable version).
Nun bekommst du eine Menge Versionen angeboten. Visual Studio 2017 war zum Zeitpunkt, als dieses Buch fertiggestellt wurde, noch nicht im Angebot, aber ich hatte keine Probleme mit der Version für 2015.
Je nachdem, welches Windows-System du hast, wähle also Visual C++14 (32-bit oder 64-bit) und klicke darauf.
Heruntergeladen wird eine zip-Datei, die anschließend entpackt werden muss.
Extrahiere also das Paket mit dem Windows-Explorer oder einer App wie z.B. dem kostenlosen 7-zip. Anschließend benenne den Ordner in SFML um. Verschiebe das Paket dann in den Ordner D:\Projekte.
Wenn du den SFML-Ordner öffnest, müssten darin einige Unterordner mit Namen wie bin und include zu finden sein.
Wenn da nur ein einzelner Ordner ist, der auch wieder etwas mit SFML im Namen trägt, dann musst du aus dem alles in den aktuellen Ordner verschieben.
Alle Unterordner müssen über Projekte\SFML erreichbar sein, sonst findet Visual Studio nachher die SFML-Dateien für die Buch-Projekte nicht.
Wenn du die Beispiel-Projekte für C++ wie auch die Lösungen nutzen willst, lassen sie sich von dieser Seite herunterladen:
http://www.mitp.de/712
Du kannst sie dann alle in einem Ordner auf deiner Festplatte unterbringen. Ich benutze im Buch einen Ordner namens D:\Projekte.
Nun ist es an der Zeit, unser kleines Jump-&-Run-Spiel um Gegenstände zu erweitern, die der Player einsammeln und einsetzen muss. Dabei könnten ihm einige Hindernisse die Arbeit etwas schwerer machen. Konkret soll es darum gehen, dass der Player farbige Scheiben in einer bestimmten Reihenfolge einsammelt, damit am Ende so etwas wie ein Regenbogen herauskommt.
Außerdem beschäftigen wir uns noch mit dem Springen nicht nur über Hindernisse – und mit dem möglichen Tod des Players.
In diesem Kapitel lernst du
eine Klasse für (viele) Dinge kennen
wie der Player Gegenstände aufnehmen kann
mehr über Tastenereignisse
wie der Player sich durch Hindernisse aufhalten lässt
etwas über Gravitation
wie man die Anzeigeebene von Objekten ändert
Beginnen wir mit der Klasse, die für Gegenstände, Hindernisse und andere Dinge zuständig sein soll. Ich dachte zuerst an Thing
, habe mich aber dann für Gizmo
entschieden (was das englische Wort für »Ding«, »Dingsbums«, »Zeugs« oder Ähnliches ist, also all dem Kram, den wir hier zusammenfassen wollen).
Erzeuge zuerst im Projekt-Ordner zwei neue Dateien namens Gizmo.h und Gizmo.cpp und verknüpfe sie mit dem Projekt.
Beginnen wir wieder mit der Deklaration all der Elemente, die ich gern in eine Klasse Gizmo
hineinpacken würde ( Rainbow1, Gizmo.h):
class Gizmo { private: // Elementvariablen Texture texThing; float x, y, z; int error = 0; public: static int Collected; Sprite sprite; bool isObstacle = false; bool isMoveable = false; bool isPickable = false; bool isUsable = false; bool isVisible = false; // Elementfunktionen Gizmo(); Gizmo(const string texName); void loadTexture(const string texName); void initSprite(float xPos, float yPos, float scale); void initSprite (float xPos, float yPos, float zLev, float scale); void setPosition(float xx, float yy, float zz = 0); void move(float xx, float yy, int num); Vector2f getPosition(); float getX(); float getY(); float getZ(); };
Wenn du genau hinschaust, wirst du hier eine ganze Menge Elementvariablen finden. Ein Pärchen wie texThing
und sprite
hast du sicher ohnehin erwartet, denn wenn der Gegenstand bzw. das Ding universell sein soll, dann brauchen wir eine Textur. Die kann das Bild eines Steins, einer Kiste, einer Blume, eines Schlüssels, also alles Mögliche sein. Weil diese Dinge verschiedene Eigenschaften haben, brauchen wir dazu auch einige öffentliche Elemente, auf die ich gleich komme.
Mit den beiden Variablen x
und y
wird die Position des Objekts erfasst, in z
steht die Nummer der Szene, in der der Gegenstand liegt. Statt Szene kann man auch Level sagen, gemeint ist damit: Der Player wechselt am Rand des Spielfeldes in eine neue Szene mit eventuell neuem Hintergrund, auf jeden Fall aber mit neuen (anderen) Objekten.
Kommen wir nun zu den public
-Elementen: Es gibt eine statische Variable, die mitzählt, wie viele Instanzen der Klasse (also wie viele Gegenstände) eingesammelt oder verbraucht wurden:
static int Collected;
Dann folgen mehrere Schalt-Variablen, die ich kurz in einer Tabelle erläutern möchte:
Bedeutung bei |
|
|
Ding ist ein Hindernis, muss also umgangen oder übersprungen werden |
|
Ding lässt sich verschieben (rollen, werfen) |
|
Ding lässt sich aufheben |
|
Ding kann verwendet werden (z.B. Schlüssel) |
|
Ding ist sichtbar |
Am Anfang ist so ein Objekt also quasi ein Nichts. Es muss erst »eingeschaltet« werden, ob es z.B. ein Hindernis ist oder ein Gegenstand, den man aufsammeln kann. Ist ein Ding kein Hindernis, kann der Player einfach quasi hindurchgehen; ist das Ding nutzbar, kann er es mit einem bestimmten Tastendruck aufheben (was erst einmal nichts weiter heißt, als dass es von dort verschwindet).
Schauen wir uns die Definition der Elementfunktionen an ( Rainbow1, Gizmo.cpp). Das heißt: Zuerst kommt die statische Elementvariable, die wir auf 0 setzen:
int Gizmo::Collected = 0;
Der Konstruktor übernimmt den Namen einer Bild-Datei:
Gizmo::Gizmo(string texName) { if (!texThing.loadFromFile(texName)) error = -1; if (error != -1) sprite.setTexture(texThing); }
Das Gleiche gibt es noch mal als normale Elementfunktion:
void Gizmo::loadTexture(string texName) { if (!texThing.loadFromFile(texName)) error = -1; if (error != -1) sprite.setTexture(texThing); }
Damit wird die Vereinbarung eines Arrays von Gegenständen einfacher, wenn man keine Zeiger benutzen will (wie du später sehen wirst).
Die Methode zum Initialisieren des Sprites kennst du ja aus anderen Klassen, die du bis jetzt erstellt hast:
void Gizmo::initSprite (float xPos, float yPos, float scale) { // Größe und Position festlegen sprite.setScale(scale, scale); sprite.setPosition(xPos, yPos); // Mittelpunkt finden IntRect picture = sprite.getTextureRect(); sprite.setOrigin(picture.width/2, picture.height/2); // Position und Szene merken x = xPos; y = yPos; z = 0; } void Gizmo::initSprite (float xPos, float yPos, float zLev, float scale) { initSprite(xPos, yPos, scale); z = zLev; }
Hier gibt es zwei Versionen, falls man einen bestimmten Level als Startwert übergeben will. Wie du siehst, lässt sich die eine Methode in der anderen aufrufen, was Tipparbeit spart.
Nun kommen Funktionen für das Setzen der Position (inklusive Szene bzw. Level) und das Bewegen des Gegenstandes – falls er beweglich ist:
void Gizmo::setPosition(float xx, float yy, float zz) { x = xx, y = yy; z = zz; sprite.setPosition(x, y); } void Gizmo::move(float xx, float yy, int num) { if (isMoveable) { sprite.move(xx, yy); sleep(milliseconds(num)); } }
Schließlich folgen noch einige Funktionen, die die Position als Vektor oder die x
- und y
-Koordinaten des Objekts bzw. die Nummer der Szene einzeln zurückgeben:
Vector2f Gizmo::getPosition() { return (sprite.getPosition()); } float Gizmo::getX() { return x; } float Gizmo::getY() { return y; } float Gizmo::getZ() { return z; }
Füge in den beiden Dateien Gizmo.h und Gizmo.cpp den jeweiligen Quelltext ein. Vergiss nicht den Hinweis auf "gizmo.h"
im #include
-Bereich.
Das war schon alles? Und damit wollen wir dann einige farbige Scheiben auf dem Spielfeld verteilen und in der richtigen Reihenfolge einsammeln und anordnen lassen? Du liegst richtig, wenn du denkst, dass die Mammutarbeit im Hauptprogramm stattfindet. Da brauchen wir zuerst einmal je eine Konstante und eine Variable:
const int diskMax = 6; // maximale Scheibenzahl int current = 0; // aktuelle Scheibe
Dazu kommt ein Array mit Schalt-Elementen, in dem steht, welche Scheiben eingesammelt wurden:
bool collect[diskMax];
Für den Hintergrund beschränke ich mich hier auf einen Boden und einen Himmel:
string BackName[1] = { "Ground00.jpg" }; Background background (1, BackName, Color(180, 220, 200)); background.initArea(xMax, yMax/2+80, yMax);
Wobei ich ein Array mit einem Element vereinbare und dort gleich den Dateinamen als Startwert setze.
Als Nächstes kommt das Scheiben-Array. Weil jedes Element nur eine Textur benötigt, genügt eine Variable für den Dateinamen:
string diskName; Gizmo disk[diskMax];
Recht umfangreich wird nun der Teil, in dem die sechs Scheiben ihre Textur bekommen, wobei ich halbe Ringe mit den sechs Grundfarben verwende. (Ich hatte vorher mit ganzen Ringen und mit vollen Kreisen experimentiert.)
Bei mir heißen die Dateien Element00.png bis Element05.png. In einer Schleife laden wir alle Bild-Elemente herunter und ordnen sie den Objekten zu:
for (int Nr = 0; Nr < diskMax; Nr++) { diskName = "Element0" + to_string(Nr) + ".png"; disk[Nr].loadTexture(diskName);
Dafür benutze ich hier nicht den Konstruktor, mit dem klappt es nämlich nicht, weil ich das disk
-Array direkt erzeugt habe. Und da lässt sich nur der Standard-Konstruktor ohne Parameter benutzen.
Auch in die for
-Schleife gehört dann das zufällige Verteilen der Objekte auf dem unteren Spielfeldbereich:
float xx = rand() % (xMax-250) + 150; disk[Nr].initSprite(xx, yMax - 100, 0.6f);
Anschließend wird jede Scheibe als »greifbar« gekennzeichnet:
disk[Nr].isPickable = true;
Außerdem werden alle Elemente des bool
-Feldes auf false
gesetzt, was »noch nicht eingesammelt« bedeutet:
collect[Nr] = false;
Damit endet die for
-Schleife für die Erzeugung und Vorbereitung der Scheiben.
Nun soll bei jedem Szenenwechsel eine zufällige Scheibe auftauchen und die gerade vorhandene wieder verschwinden:
if (player.controlRange(xMax)) { if (!collect[current]) disk[current].isVisible = false; current = rand() % (diskMax); disk[current].isVisible = true; }
Wenn die aktuelle (current
) Scheibe noch nicht eingesammelt wurde, wird sie unsichtbar. Dann wird eine neue Zufallszahl für current
erzeugt und damit eine neue Scheibe sichtbar gemacht. (Natürlich kann das auch zufällig dieselbe wie vorher sein.)
Dieser Teil gehört in die äußere while
-Schleife, am besten direkt unter player.adaptState()
.
Nun müssen wir noch ganz unten im Hauptprogramm dafür sorgen, dass die Scheiben auch dargestellt werden:
window.draw(player.sprite); if (!collect[current] && disk[current].isVisible) window.draw(disk[current].sprite);
Dabei ist eine bestimmte Reihenfolge wichtig: Erst wird der Player angezeigt, dann die jeweilige Scheibe, damit der Player hinter der Scheibe herläuft.
Erweitere den Quelltext der Hauptfunktion (und vergiss #include "gizmo.h"
. nicht). Dann starte das Programm und laufe herum.
Vorläufig gilt »nur schauen, nicht anfassen«, aber das werden wir schon bald ändern.
Zuerst definieren wir eine Taste als »Greifwerkzeug«. Ich habe mich dabei für die Strg-Tasten entschieden. Der folgende Text gehört direkt unter die Abfrage der anderen Tasten ( Rainbow2):
if (Keyboard::isKeyPressed(Keyboard::LControl) || Keyboard::isKeyPressed(Keyboard::RControl)) { Vector2f xy = disk[current].getPosition(); if (player.spotCollision(xy) && current == Gizmo::Collected) collect[current] = true; }
Wenn die linke oder rechte Strg-Taste gedrückt wurde, ermitteln wir zunächst die Position der aktuellen Scheibe. Dann wird überprüft, ob Player und Scheibe nahe genug beisammen sind. Das genügt aber nicht: Der Player darf nur eine bestimmte Scheibe aufsammeln, nämlich die, die gerade dran ist. Welche das ist, ermitteln wir über die statische Variable Gizmo::Collected
.
Um einen Regenbogen zu erzeugen, müssen die Scheiben in dieser Reihenfolge aufgenommen werden:
|
Dateiname |
Farbe |
---|---|---|
0 |
|
Rot |
1 |
|
Gelb |
2 |
|
Grün |
3 |
|
Zyan |
4 |
|
Blau |
5 |
|
Magenta |
Die entsprechende Scheibe darf jetzt nicht mehr in irgendeiner Szene auf dem Boden des Spielfeldes auftauchen. Wir machen es uns hier einfach und setzen die Scheibe auch gleich woanders ein, um daraus einen Regenbogen zu machen.
Gilt eine Scheibe als gesammelt und ist es die richtige, dann »hängen« wir sie doch direkt am Himmel auf:
if (collect[current] && current == Gizmo::Collected) { float scale = 2.8f - (float) 2*current/6; disk[current].sprite.setScale(1.2f*scale, scale); disk[current].sprite.setPosition(xMax/2, yMax/3 + 22*current); Gizmo::Collected++; }
Zuerst muss das Objekt kräftig vergrößert werden. Bei mir passt die obige Skalierung sehr gut, bei dir musst du experimentieren, wenn du andere Bilder benutzt. Das, was vorher eine Scheibe war, wird jetzt zum Bogen und in die Himmelsmitte gesetzt.
Die aktuelle Scheiben-Nummer (current
) hilft dabei, die Bögen nach und nach kleiner zu machen und leicht zu verschieben, damit nachher alles schön ineinanderpasst. (Auch da ist es wahrscheinlich, dass du deine eigenen Werte finden musst.) Zuletzt wird der Zähler für die Scheiben-Sammlung (Collected
) um 1 erhöht.
Sobald alle Elemente für einen Regenbogen am Himmel »hängen«, ist das Spiel zu Ende. Das machen wir mit einer entsprechenden Meldung deutlich:
if (Gizmo::Collected == diskMax) { game.initMessage(20, 10, 36, Color::Blue); game.setMessage("Game over"); }
Zuletzt schauen wir noch mal nach ganz unten ans Ende der main
-Funktion, da, wo die vielen draw
-Anweisungen stehen, die dafür sorgen, dass man überhaupt etwas im Game-Fenster zu sehen bekommt. Weil es mehr als nur ein bisschen blöd aussieht, wenn der Player hinter dem Regenbogen herläuft statt davor, müssen wir die Anzeige der Scheiben bzw. Bögen auf zwei Bereiche verteilen.
Hängen die Bögen am Himmel, sollen sie hinter dem Player liegen, sodass der davor hergeht. Damit gehört die entsprechende Schleife vor die draw
-Anweisung für den Player:
for (int Nr = 0; Nr < diskMax; Nr++) { if (disk[Nr].isVisible) window.draw(disk[Nr].sprite); } window.draw(player.sprite);
Das jeweilige Element, dem der Player beim Wandern begegnet, soll vor ihm liegen, sodass er dahinter vorbeigehen kann:
if (!collect[current] && disk[current].isVisible) window.draw(disk[current].sprite);
Erweitere das Hauptprogramm um den neuen Quelltext. Oder lade die Projekt-Dateien direkt von www.mitp.de/712 herunter. Dann starte das Programm und baue dir deinen Regenbogen.
Nun bist du mit dem Player viel gewandert und hast einiges aufgesammelt. Aber was ist mit dem Springen? Das war in unserem Spiel nicht nötig. Aber es interessiert dich schon, wie der Player sich beim Hindernislauf macht? Dann kommen wir erst einmal zurück zu unserer letzten Jumper-Version, in der die Gizmo
-Klasse noch nicht vorkam. Die nehme ich mir als Basis und erweitere das Programm um ein paar Gegenstände, die dem Player beim Wandern immer wieder mal begegnen. Einige davon sind Hindernisse, aber man kann auch etwas mitnehmen.
Wir brauchen also in der Jumperklasse die beiden Gizmo-Dateien, die wir vorhin für das Regenbogen-Spiel erstellt haben. Widmen wir uns gleich dem Hauptprogramm (in das unsere neue Klasse natürlich mit #include "gizmo.h"
eingebunden sein muss).
Zuerst erzeugen wir ein »Ding« und bereiten es darauf vor, dass es im Spiel als Hindernis fungieren kann ( Jumper4).
Gizmo thing("Thing00.png"); thing.initSprite(xMax/2, yMax/2+100, 0.7f); thing.isObstacle = true;
Dazu müssen wir natürlich eine Textur haben, die dazu passt. Ich stelle hier kurz die Sammlung vor, die du von der mitp-Seite herunterladen kannst, die Namen durchnummeriert von Thing00.png bis Thing04.png.
Dabei habe ich vor, nur die ersten drei als Hindernisse zu benutzen. Die anderen beiden Elemente sollen sich mitnehmen lassen. Zunächst aber reicht mir eine einzige Textur, z.B. die für eine Mauer (oder ein Mäuerchen).
Auch hier soll die Position des Hindernis-Elements sich von Szene zu Szene ändern:
if (player.controlRange(xMax)) { background.change(); float xPos = rand() % (xMax-500) + 250; thing.setPosition(xPos, yMax/2+100); }
Damit danach der Player auch am Hindernis aufgehalten wird, müssen wir eine Kollisionskontrolle durchführen, dazu vereinbaren wir am Anfang der main
-Funktion eine Schalt-Variable:
bool isBlocked = false;
Ich habe mich hier dafür entschieden, den Test mit der Funktion areaCollision()
durchzuführen, also zu überprüfen, ob sich die Flächen der Sprites von Player und Hindernis überschneiden:
FloatRect xy = thing.sprite.getGlobalBounds(); if (player.areaCollision(xy) && thing.isObstacle) isBlocked = true; else isBlocked = false;
Kommt es zu einer Berührung und ist das »Ding« ein Hindernis, dann wird isBlocked
auf true
gesetzt, sonst auf false
.
Was soll nun passieren, wenn der Player blockiert wird? Er muss aufgehalten werden, darf nicht mehr weiterlaufen. Man könnte das ganz einfach lösen, indem man speed
auf 0 setzt. Der Haken ist, dass der Player im »Kollisionsgeflecht« festhängt, er muss sozusagen etwas zurückprallen, damit er sich außerhalb der Zone befindet, in der sich die Flächen der Sprites überschneiden.
Die Player
-Klasse braucht also eine neue Methode, die ich bounce()
nennen will:
// player.h void bounce(); // player.cpp void Player::bounce() { Vector2f xy = sprite.getPosition(); sprite.setPosition(xy.x-5*speed, xy.y); }
Hier wird der Player ein Stückchen gegen seine Laufrichtung (also zurück) geschubst.
Nun müssen wir die mögliche Blockade für den Lauf-Modus einbauen:
if (player.isWalking) { if (!isBlocked) player.move(2); else player.bounce(); }
Gibt es kein Hindernis, dann kann der Player einfach drauflos gehen, ansonsten wird er blockiert. Er muss dann über das Hindernis springen oder in die andere Richtung weiterlaufen.
Ergänze das Hauptprogramm und die Player
-Klasse um die neuen »Errungenschaften«. Vergiss nicht, ganz unten die Anweisung window.draw(thing.sprite);
zu ergänzen.
Du kannst dich auch bei den Projekt-Dateien von www.mitp.de/712 bedienen. Dann starte das Programm und probiere aus, wie du mit dem Hindernis zurechtkommst.
Nun wollen wir ein paar Gegenstände mehr in Spiel bringen. Dabei ist hier keine Vereinbarung eines Gizmo
-Arrays nötig, nein, wir bleiben bei dem einen Objekt und verpassen ihm nur verschiedene Texturen, wie wir es ja beim Player längst tun. Somit taucht der eigentlich gleiche Gegenstand in verschiedenem Outfit auf, außerdem hat er nicht immer die gleichen Eigenschaften.
Beginnen wir mit den Textur-Dateien. Dazu vereinbare ich eine weitere Konstante:
const int thingMax = 5;
Um das eine Objekt zu erzeugen, das dann in jeder Szene anders »auftreten« soll, sind einige Vorbereitungen nötig, dazu gehört das Laden einer Textur.
Weil bei jedem Szenenwechsel eine neue Textur geladen wird, wiederholen sich eine ganze Reihe von Anweisungen. Deshalb habe ich hier entschieden, eine externe Funktion zu vereinbaren, die ich oberhalb der main
-Funktion als Prototyp aufführe, um sie später unterhalb zu definieren:
void setThing(int xx, int yy, int num, Gizmo &object);
Der letzte Parameter ist eine Referenz auf den jeweiligen Gegenstand, weil der ja verändert werden soll.
Nun sieht die Vorbereitung des »Dings« nur noch so aus:
Gizmo thing; setThing (xMax, yMax/2, thingMax, thing);
Dafür ist die Funktion setThing()
ziemlich umfangreich ( Jumper5):
void setThing(int xx, int yy, int num, Gizmo &object) { string thingName; // Zufällige Textur int Nr = rand() % num; thingName = "Thing0" + to_string(Nr) + ".png";; object.loadTexture(thingName); // Typ-Eigenschaften object.isVisible = true; if (Nr < num - 2) { object.isObstacle = true; object.isPickable = false; } else { object.isObstacle = false; object.isPickable = true; } // Zufällige Position float xPos = rand() % (xx - 500) + 250; object.initSprite(xPos, yy + 110, 0.7f); }
Wie du siehst, wird zuerst eine zufällige Datei ausgesucht und geladen, damit wird das Sprite dann texturiert. Nun wird der Typ festgelegt: Bei mir sind die ersten drei Texturen für Hindernisse, die letzten beiden für »Mitnehmsel«. Schließlich wird das Objekt dann noch irgendwo auf dem Spielfeld positioniert.
Wenn du das alles eintippst, achte darauf, dass die Deklaration (= Prototyp) direkt über die main
-Zeile gehört und die Definition direkt unter die letzte geschweifte Klammer. (Du kannst aber auch die komplette Definition gleich ganz oben hinsetzen und dir damit den Prototyp sparen.)
So lässt sich das Programm bereits ausprobieren, in jeder neuen Szene wird dir wahrscheinlich ein neues »Ding« begegnen (nicht immer), zum Teil musst du drüberspringen, zum Teil kannst du dahinter vorbeilaufen.
Um ein solches Teil aufzunehmen, brauchen wir eine weitere Taste, die wir schon im Regenbogen-Projekt benutzt hatten (genau genommen zwei, die linke und die rechte Strg-Taste). Die Abfrage übernehmen wir hier in unser Jumper-Projekt und passen sie an ( Jumper5):
if (Keyboard::isKeyPressed(Keyboard::LControl) || Keyboard::isKeyPressed(Keyboard::RControl)) { Vector2f xy = thing.getPosition(); if (player.spotCollision(xy) && thing.isPickable) thing.isVisible = false; }
Natürlich wird der betreffende Gegenstand dadurch noch nicht unsichtbar, deshalb müssen wir noch dafür sorgen, dass er anschließend nicht mehr angezeigt wird:
if (thing.isVisible) window.draw(thing.sprite);
Das steht ganz unten in der main
-Funktion, womit das aufgenommene Ding dann in dieser Szene wirklich verschwunden ist. Weil nach jedem Szenenwechsel das Objekt mit neuer Textur wieder sichtbar gemacht wird, erscheint es auch später wieder.
Was macht man dann mit den gesammelten Sachen? Das wird hier nicht geklärt, aber mich interessiert im Moment auch mehr die Sprungtechnik des Players. Bis jetzt ist es nämlich so, dass man einfach nur die Leertaste festhalten muss und die Figur fliegt geradezu über alle Hindernisse hinweg. Und sie landet erst wieder auf dem Boden, wenn man die Sprung-Taste loslässt. Schauen wir mal, ob und wie sich das ändern lässt.
Was ist mit der Gravitation, auch Anziehungskraft genannt, also die, die der Boden eigentlich auf den Player ausüben sollte? Denn während er zum Sprung ansetzt, hält ihn die Gravitation doch eigentlich zunächst noch unten, dann ist die Gegenkraft so stark, dass er sich vom Boden löst und nach oben steigt. Aber nur für kurze Zeit, denn der Schwung des Absprungs und die stärkere Gravitation sind nun Gegenspieler, sie wirken in entgegengesetzte Richtungen.
Mit jumpSpeed
meine ich die Strecke, um die der Player nach oben geschoben werden soll. Dafür genügt mir der Wert -1, wie ich ihn auch in der jump
-Methode verwende, die in der Player
-Klasse bereits definiert ist.
Stärker als die Sprungkraft ist auf Dauer die Gravitation. Dafür muss ich später einen Wert suchen. Die echte Gravitationskonstante kann ich hier nicht gebrauchen. Ich kann nicht einfach einen festen Wert nehmen, weil sich der Player sonst gleichförmig nach unten bewegen würde.
Auf gravity
kommen wir später zurück, erst schauen wir uns die neue (zusätzliche) jump
-Funktion für die Player
-Klasse an ( Jumper6):
//player.h void jump(float gravity); //player.cpp void Player::jump(float gravity) { float y = gravity - 1; height += y; if (height < 0) sprite.move(0, y); else { height = 0; isJumping = false; } }
Zuerst wird der y
-Wert aus gravity
und Sprungkraft (-1
) gebildet. Dann wird die Höhe des Players über dem Boden ermittelt:
float y = gravity - 1; height += y;
Ist sie kleiner als 0 (muss sie sein, weil »oben« hier negativ bedeutet), dann wird der Player bewegt:
if (height < 0) sprite.move(0, y);
Ist height
größer als 0, wird die Variable auf 0 und isJumping
auf false
gesetzt:
else { height = 0; isJumping = false; }
Das Springen funktioniert aus dem Stand gut, damit das auch beim Laufen klappt, müssen wir hier die sleep
-Anweisung in der move
-Methode deaktivieren, was zu dieser kleinen Änderung führt:
void Player::move(int ms) { sprite.move(speed, 0); if (!isJumping) sleep(milliseconds(ms)); isWalking = false; speed = 0; }
Das bringt auch in der normalen jump
-Methode keine Probleme, aber andersherum würde die sleep
-Methode die arme Gravitation »ins Schwitzen« bringen (probiere es aus).
Erweitere die Player
-Klasse um die neue Elementfunktion, die die alte ergänzen (nicht ersetzen) soll. Und passe die move
-Funktion an.
Nun wollen wir die neue Sprung-Technik gleich im Hauptprogramm ausprobieren. Der Wert, den ich als gravity
-Parameter übergeben möchte, muss sich ständig vergrößern, nur dann wird der Player in seiner Bewegung nach oben abgebremst und kommt auch wieder herunter.
Doch woher bekommen wir einen Wert, der mit der Zeit größer wird? Es ist die Zeitspanne, die wir mit game.getTime()
auffangen. Beim Drücken der Leertaste wird der Timer neu gestartet. Solange der Sprung anhält, wird die abgelaufene Zeit immer länger, und damit hätten wir etwas für unseren graviity
-Parameter.
Das funktioniert aber nur, wenn die Zeit mit jedem Tastendruck nur einmal zurückgesetzt wird. Wir brauchen eine neue Schalt-Variable, die ich inTime
nennen möchte. Die vereinbaren wir direkt unter der isBlocked
-Variablen:
bool inTime = false;
Nun passen wir die Abfrage der Leertaste neu an:
if (Keyboard::isKeyPressed(Keyboard::Space)) { player.isJumping = true; player.adaptState(); if (!inTime) { game.getTime(false); inTime = true; } }
Wenn inTime
»ausgeschaltet« ist (also der Player noch nicht nach oben geschickt wurde), wird der Timer auf 0 gesetzt:
if (!inTime) { game.getTime(false); inTime = true; }
Außerdem wird inTime
»angeschaltet«. Wird die Leertaste festgehalten, kann der Timer nicht erneut auf 0 zurückgesetzt werden, er läuft also weiter.
Was konkret bedeutet: Drückt man die Leertaste und hält sie fest, springt der Player hoch und landet nach einiger Zeit wieder auf dem Boden. Doch das klappt nur einmal, irgendwie müssen wir inTime
auch wieder »ausschalten«, der Player soll wieder springen können, wenn ich die Leertaste loslasse und dann erneut drücke.
Tatsächlich bietet das SFML-Paket ein Event an, das ermittelt, ob eine Taste losgelassen wurde:
if (event.type == Event::KeyReleased) { if (event.key.code == Keyboard::Space) inTime = false; }
Dabei können wir nicht auf die Keyboard
-Klasse zurückgreifen, sondern KeyReleased
findet sich in der Event
-Gruppe. Beim Loslassen einer Taste wird ermittelt, welche es ist. Ist es die Leertaste (Space
), dann wird inTime
deaktiviert. Anschließend kann der Timer wieder zurückgesetzt werden (wenn man die Leertaste erneut drückt).
Eine weitere Verwendung von inTime
ist nicht nötig, aber die Anweisungen für den Sprung-Modus müssen angepasst werden:
float stay = (float) game.getTime(true)/500; if (player.isJumping) player.jump(stay);
Wir übernehmen die aktuell verstrichene Zeit (in Millisekunden) und teilen dies durch eine ziemlich große Zahl, damit ein nicht zu großer Wert herauskommt.
Ob auch bei dir die 500 passt, musst du ausprobieren (mit größeren Zahlen springt der Player höher).
Damit stay
keine ganze Zahl zugewiesen bekommt, müssen wir auch hier mit (float)
eine Dezimalzahl daraus machen. Danach bekommt die jump
-Methode ihre »Gravitationskraft«, der Player springt und wird wieder auf die Erde zurückgeholt.
Erweitere das Hauptprogramm um die neuen Anweisungen. Dann lass den Player laufen und springen. Perfekt wird es nicht sein, aber es gibt ja viele »Schrauben«, an denen man durch Änderung der Werte drehen kann.
Auch ein Player ist sterblich – normalerweise. Irgendwann geht jedes Spiel zu Ende, aber nicht unbedingt mit dem Tod des Players. Doch damit haben wir uns noch gar nicht auseinandergesetzt: Was ist, wenn man in ein Spiel so etwas wie eine tödliche Falle einbauen will? Oder einen Gegenstand, der den Player bei Kontakt aus dem Leben befördert?
Auch dazu könnte man die Gizmo-Klasse benutzen, die ich dann um eine Eigenschaft erweitern würde (in gizmo.h):
bool isLethal = false;
Was zunächst heißt: Das »Ding« ist (noch) ungefährlich. Auch im Hauptprogramm sollte es eine Schalt-Variable geben, die natürlich auf »lebendig« eingestellt ist:
bool inLife = true;
Zuerst müssen wir nun dafür sorgen, dass das Laufen und Springen nur dann stattfindet, wenn der Player am Leben ist. Dazu bekommen die entsprechenden if
-Zweige jeweils eine Zusatzbedingung ( Jumper7):
if (player.isJumping && inLife) player.jump(stay); if (player.isWalking && inLife) { if (!isBlocked) player.move(2); else player.bounce(); }
Ausprobieren könntest du das, indem du inLife
am Anfang der main
-Funktion auf false
setzt, dann dürfte sich der Player nicht von der Stelle rühren. Dass er dennoch herumhampelt, macht eigentlich nichts, das könnte man aber auch dadurch eindämmen, dass man den Aufruf von adaptState()
an eine passende Bedingung knüpft:
if (inLife) player.adaptState();
Was soll nun passieren, wenn es den Player tödlich erwischt? Hier eine einfache Lösung:
if (!inLife) { player.sprite.setRotation(90); player.sprite.move(0, -0.1f); game.initMessage(xMax/2-120, 10, 48, Color::Black); game.setMessage("Game over"); }
Der Player wird »umgelegt« und schwebt in den Himmel, während die Meldung »Game over« erscheint. Du findest sicher einen originelleren Tod für den Player, ich überlasse das ganz deiner Fantasie.
Bevor der Player sterben kann, muss es etwas geben, das für ihn tödlich ist. Ich habe mir den mittleren Gegenstand meiner Gizmo-Sammlung herausgesucht, den man als seltsam aussehenden Stein mit magisch-tödlichen Kräften ansehen könnte. Zuerst muss die setThing
-Funktion erweitert werden. Dabei geht es nur um den Teil, in dem bestimmte Eigenschaften der Objekte »geschaltet« werden ( Jumper7):
switch (Nr) { case 0: case 1: object.isObstacle = true; object.isPickable = false; object.isLethal = false; break; case 2: object.isObstacle = false; object.isPickable = false; object.isLethal = true; break; case 3: case 4: object.isObstacle = false; object.isPickable = true; object.isLethal = false; }
Ich habe hier mal wieder eine switch
-Struktur eingesetzt. Und wie du siehst, kann man auch mehrere case
-Elemente hintereinander einsetzen, wenn dieselben Anweisungsblöcke damit verbunden werden sollen.
Nun sind zwei Gegenstände Hindernisse, zwei kann man einsammeln – und eines ist von nun an tödlich. Damit das auch seine Wirkung zeigt, muss das bei der Kollisionskontrolle berücksichtigt werden:
if (player.areaCollision(xy)) { if (thing.isObstacle) isBlocked = true; if (thing.isLethal) inLife = false; } else isBlocked = false;
Wenn du willst, trage das alles in den entsprechenden Quelltexten nach – oder bediene dich beim mitp-Download. Laufe herum und schau, was passiert, wenn du in Kontakt mit dem besagten Objekt kommst, das ja von außen gar nicht so tödlich aussieht.
Mit dem möglichen Tod des Players sind auch wir leider am Ende. Und zwar nicht nur dieses Kapitels, sondern aller Kapitel. Es gibt weiterhin vieles, was du in Sachen C++ und Spieleprogrammierung noch nicht weißt (und damit bist du nicht allein). Aber du hast mindestens drei Spiele selbst erstellt und bist wahrscheinlich inzwischen dabei, diese zu verbessern oder neue zu programmieren. Weiter so!
Es gab auch hier einige Neuigkeiten, aber nur ein neues Element aus dem SFML-Wortschatz, das Ereignis KeyReleased
. Es lohnt sich auf jeden Fall, sich das, was SFML noch zu bieten hat, anzuschauen, z.B. unter dieser Internet-Adresse:
https://www.sfml-dev.org/learn.php
Hier findest du auch kleine Tutorials sowie den kompletten Wortschatz (API Documentation). Und wenn du (auf Englisch) im Forum mitreden willst, dann schau dich mal hier um:
https://en.sfml-dev.org/forums/index.php
Es gibt auch ein paar deutsche Seiten, die sich mit dem SFML befassen, da findet man aber deutlich weniger Informationen.
Es muss ja auch nicht beim SFML-Paket bleiben. Es gibt einige Game Engines, die sich mit C++ steuern lassen. Dazu gehört u.a. auch Panda3D, zu finden auf dieser Internet-Seite:
http://www.panda3d.org/
Speziell mit der Programmierung in C++ befasst sich ein Manual, das du hier findest:
http://www.panda3d.org/manual/index.php?title=Main_Page&language=cxx
Damit machst du den Schritt von 2D zu 3D, den ich hier nicht mehr mitgehen kann.
Baue in das Rainbow-Projekt ein paar Hindernisse ein.