Erstellen Sie einen Neon Vector Shooter für iOS: Erste Schritte
() translation by (you can also view the original English article)
In dieser Reihe von Tutorials zeige ich Ihnen, wie Sie einen von Geometry Wars inspirierten Twin-Stick-Shooter mit Neongrafiken, verrückten Partikeleffekten und großartiger Musik für iOS mit C++ und OpenGL ES 2.0 erstellen.
Anstatt sich auf ein vorhandenes Spielframework oder eine Sprite-Bibliothek zu verlassen, werden wir versuchen, so nah wie möglich an der Hardware (oder "Bare Metal") zu programmieren. Da Geräte mit iOS im Vergleich zu einem Desktop-PC oder einer Spielekonsole auf kleinerer Hardware ausgeführt werden, können wir so viel Geld wie möglich für unser Geld verdienen.
Ziel dieser Tutorials ist es, die erforderlichen Elemente zu erläutern, mit denen Sie Ihr eigenes hochwertiges Handyspiel für iOS erstellen können, entweder von Grund auf neu oder basierend auf einem vorhandenen Desktop-Spiel. Ich ermutige Sie, den Code herunterzuladen und damit zu spielen oder ihn sogar als Grundlage für Ihre eigenen Projekte zu verwenden.
Wir werden die folgenden Themen in dieser Reihe behandeln:
- Erste Schritte: Einführung in die Utility-Bibliothek, Einrichten des grundlegenden Gameplays, Erstellen des Schiffs, des Sounds und der Musik des Spielers.
- Beenden Sie die Implementierung der Spielmechanik, indem Sie Feinde hinzufügen, die Kollisionserkennung übernehmen und die Punktzahl und das Leben des Spielers verfolgen.
- Fügen Sie ein virtuelles Gamepad auf dem Bildschirm hinzu, damit wir das Spiel mithilfe der Multitouch-Eingabe steuern können.
- Fügen Sie verrückte, übertriebene Partikeleffekte hinzu.
- Fügen Sie das Warping-Hintergrundraster hinzu.
Folgendes haben wir am Ende der Serie:
Und hier ist, was wir am Ende dieses ersten Teils haben werden:
Die Musik und Soundeffekte, die Sie in diesen Videos hören, wurden von RetroModular erstellt, und Sie können in unserer Audio-Sektion darüber lesen.
Die Sprites stammen von Jacob Zinman-Jeanes, unserem ansässigen Tuts+ Designer.



Die Schriftart, die wir verwenden, ist eine Bitmap-Schriftart (mit anderen Worten, keine tatsächliche "Schriftart", sondern eine Bilddatei), die ich für dieses Tutorial erstellt habe.



Alle Grafiken befinden sich in den Quelldateien.
Lassen Sie uns anfangen.
Überblick
Bevor wir uns mit den Besonderheiten des Spiels befassen, wollen wir uns mit dem Utility Library- und Application Bootstrap-Code befassen, den ich zur Unterstützung der Entwicklung unseres Spiels bereitgestellt habe.
Die Utility-Bibliothek
Obwohl wir hauptsächlich C++ und OpenGL verwenden, um unser Spiel zu codieren, benötigen wir einige zusätzliche Dienstprogrammklassen. Dies sind alles Klassen, die ich geschrieben habe, um die Entwicklung in anderen Projekten zu unterstützen. Sie sind daher zeitgetestet und für neue Projekte wie dieses verwendbar.
-
package.h
: Ein Convenience-Header, der alle relevanten Header aus der Utility-Bibliothek enthält. Wir werden es einschließen, indem wir#include "Utility/package.h"
angeben, ohne etwas anderes einfügen zu müssen.
Muster
Wir werden einige vorhandene bewährte Programmiermuster nutzen, die in C++ und anderen Sprachen verwendet werden.
-
tSingleton
: Implementiert eine Singleton-Klasse mit einem "Meyers Singleton" -Muster. Es ist vorlagenbasiert und erweiterbar, sodass wir den gesamten Singleton-Code in eine einzige Klasse abstrahieren können. -
tOptional
: Das ist eine Funktion aus C++ 14 (std::optional
genannt), die in aktuellen Versionen von C++ noch nicht ganz verfügbar ist (wir befinden uns noch in C++ 11). Es ist auch eine Funktion, die in XNA und C# verfügbar ist (wo esNullable
heißt). Es ermöglicht uns, "optionale" Parameter für Methoden zu haben. Es wird in dertSpriteBatch
-Klasse verwendet.
Vector Math
Da wir kein vorhandenes Spiel-Framework verwenden, benötigen wir einige Klassen, um die Mathematik hinter den Kulissen zu behandeln.
-
tMath
: Eine statische Klasse bietet einige Methoden, die über das in C++ verfügbare Maß hinausgehen, z. B. das Konvertieren von Grad in Bogenmaß oder das Runden von Zahlen in Zweierpotenzen. -
tVector
: Ein grundlegender Satz von Vektorklassen, der Varianten mit 2 Elementen, 3 Elementen und 4 Elementen bereitstellt. Wir schreiben diese Struktur auch für Punkte und Farben. -
tMatrix
: Zwei Matrixdefinitionen, eine 2x2-Variante (für Rotationsoperationen) und eine 4x4-Option (für die Projektionsmatrix, die erforderlich ist, um die Dinge auf den Bildschirm zu bringen), -
tRect
: Eine Rechteckklasse, die Position, Größe und eine Methode angibt, um zu bestimmen, ob Punkte innerhalb von Rechtecken liegen oder nicht.
OpenGL Wrapper Klassen
Obwohl OpenGL eine leistungsstarke API ist, ist sie C-basiert, und die Verwaltung von Objekten kann in der Praxis etwas schwierig sein. Wir haben also eine kleine Handvoll Klassen, um die OpenGL-Objekte für uns zu verwalten.
-
tSurface
: Bietet eine Möglichkeit, eine Bitmap basierend auf einem Bild zu erstellen, das aus dem Bundle der Anwendung geladen wurde. -
tTexture
: Umschließt die Schnittstelle mit den Texturbefehlen von OpenGL und lädttSurfaces
in Texturen. -
tShader
: Umschließt die Schnittstelle zum OpenGL-Shader-Compiler und erleichtert so das Kompilieren von Shadern. -
tProgram
: Umschließt die Schnittstelle mit der Shader-Programmschnittstelle von OpenGL, bei der es sich im Wesentlichen um die Kombination zweiertShader
-Klassen handelt.
Spielunterstützungsklassen
Diese Klassen stellen diejenige dar, die einem "Spiel-Framework" am nächsten kommt. Sie bieten einige Konzepte auf hoher Ebene, die für OpenGL nicht typisch sind, aber für Spieleentwicklungszwecke nützlich sind.
-
tViewport
: Enthält den Status des Ansichtsfensters. Wir verwenden dies hauptsächlich, um Änderungen an der Geräteorientierung zu behandeln. -
tAutosizeViewport
: Eine Klasse, die Änderungen am Ansichtsfenster verwaltet. Es verarbeitet Änderungen der Geräteausrichtung direkt und skaliert das Ansichtsfenster so, dass es auf den Bildschirm des Geräts passt, sodass das Seitenverhältnis gleich bleibt - was bedeutet, dass Dinge nicht gedehnt oder gequetscht werden. -
tSpriteFont
: Ermöglicht das Laden einer "Bitmap-Schriftart" aus dem Anwendungspaket und das Schreiben von Text auf dem Bildschirm. -
tSpriteBatch
: Inspiriert von derSpriteBatch
-Klasse von XNA habe ich diese Klasse geschrieben, um das Beste aus dem herauszuholen, was unser Spiel benötigt. Es ermöglicht uns, Sprites beim Zeichnen so zu sortieren, dass wir die bestmögliche Geschwindigkeitssteigerung für die vorhandene Hardware erzielen. Wir werden es auch direkt verwenden, um Text auf dem Bildschirm zu schreiben.
Verschiedene Klassen
Eine minimale Anzahl von Klassen, um die Dinge abzurunden.
-
tTimer
: Ein System-Timer für Animationen. -
tInputEvent
: Grundlegende Klassendefinitionen, um Orientierungsänderungen (Neigen des Geräts), Berührungsereignisse und ein "virtuelles Tastatur" -Ereignis bereitzustellen, um ein Gamepad diskreter zu emulieren. -
tSound
: Eine Klasse zum Laden und Abspielen von Soundeffekten und Musik.
Anwendungs-Bootstrap
Wir benötigen auch den sogenannten "Boostrap" -Code, d.h. Code, der den Start einer Anwendung abstrahiert oder "hochfährt".
Folgendes ist in Bootstrap
enthalten:
-
AppDelegate
: Diese Klasse verwaltet den Anwendungsstart sowie das Anhalten und Fortsetzen von Ereignissen, wenn der Benutzer die Home-Taste drückt. -
ViewController
: Diese Klasse verarbeitet Geräteorientierungsereignisse und erstellt unsere OpenGL-Ansicht -
OpenGLView
: Diese Klasse initialisiert OpenGL, weist das Gerät an, mit 60 Bildern pro Sekunde zu aktualisieren, und verarbeitet Berührungsereignisse.
Überblick über das Spiel
In diesem Tutorial erstellen wir einen Twin-Stick-Shooter. Der Spieler steuert das Schiff mithilfe von Multitouch-Steuerelementen auf dem Bildschirm.
Wir werden eine Reihe von Klassen verwenden, um dies zu erreichen:
-
Entity
: Die Basisklasse für Feinde, Kugeln und das Schiff des Spielers. Entitäten können sich bewegen und gezeichnet werden. -
Bullet
undPlayerShip
. -
EntityManager
: Verfolgt alle Entitäten im Spiel und führt eine Kollisionserkennung durch. -
Input
: Hilft bei der Verwaltung von Eingaben über den Touchscreen. -
Art
: Lädt und enthält Verweise auf die für das Spiel benötigten Texturen. -
Sound
: Lädt und enthält Verweise auf Sounds und Musik. -
MathUtil
undExtensions
: Enthält einige hilfreiche statische Methoden und Erweiterungsmethoden. -
GameRoot
: Steuert die Hauptschleife des Spiels. Dies ist unsere Hauptklasse.
Der Code in diesem Tutorial soll einfach und leicht verständlich sein. Es werden nicht alle Funktionen entwickelt, um alle möglichen Anforderungen zu erfüllen. Vielmehr wird es nur das tun, was es tun muss. Wenn Sie es einfach halten, können Sie die Konzepte leichter verstehen und sie dann ändern und zu Ihrem eigenen einzigartigen Spiel erweitern.
Entitäten und das Schiff des Spielers
Öffnen Sie das vorhandene Xcode-Projekt. GameRoot ist die Hauptklasse unserer Anwendung.
Wir beginnen mit der Erstellung einer Basisklasse für unsere Spieleinheiten. Schauen Sie sich das an Entitätsklasse:
1 |
class Entity |
2 |
{
|
3 |
public:
|
4 |
enum Kind |
5 |
{
|
6 |
kDontCare = 0, |
7 |
kBullet, |
8 |
kEnemy, |
9 |
kBlackHole, |
10 |
};
|
11 |
protected:
|
12 |
tTexture* mImage; |
13 |
tColor4f mColor; |
14 |
|
15 |
tPoint2f mPosition; |
16 |
tVector2f mVelocity; |
17 |
float mOrientation; |
18 |
float mRadius; |
19 |
bool mIsExpired; |
20 |
Kind mKind; |
21 |
|
22 |
public:
|
23 |
Entity(); |
24 |
virtual ~Entity(); |
25 |
|
26 |
tDimension2f getSize() const; |
27 |
virtual void update() = 0; |
28 |
|
29 |
virtual void draw(tSpriteBatch* spriteBatch); |
30 |
|
31 |
tPoint2f getPosition() const; |
32 |
tVector2f getVelocity() const; |
33 |
void setVelocity(const tVector2f& nv); |
34 |
float getRadius() const; |
35 |
bool isExpired() const; |
36 |
Kind getKind() const; |
37 |
|
38 |
void setExpired(); |
39 |
};
|
Alle unsere Entitäten (Feinde, Kugeln und das Schiff des Spielers) haben einige grundlegende Eigenschaften, wie z. B. ein Bild und eine Position. mIsExpired
wird verwendet, um anzuzeigen, dass die Entität zerstört wurde und aus allen Listen entfernt werden sollte, die einen Verweis darauf enthalten.
Danach erstellen wir einen EntityManager
, um unsere Entitäten zu verfolgen und zu aktualisieren und zu zeichnen:
1 |
class EntityManager |
2 |
: public tSingleton<EntityManager> |
3 |
{
|
4 |
protected:
|
5 |
std::list<Entity*> mEntities; |
6 |
std::list<Entity*> mAddedEntities; |
7 |
std::list<Bullet*> mBullets; |
8 |
bool mIsUpdating; |
9 |
|
10 |
protected:
|
11 |
EntityManager(); |
12 |
|
13 |
public:
|
14 |
int getCount() const; |
15 |
|
16 |
void add(Entity* entity); |
17 |
void addEntity(Entity* entity); |
18 |
|
19 |
void update(); |
20 |
void draw(tSpriteBatch* spriteBatch); |
21 |
|
22 |
bool isColliding(Entity* a, Entity* b); |
23 |
|
24 |
friend class tSingleton<EntityManager>; |
25 |
};
|
26 |
|
27 |
void EntityManager::add(Entity* entity) |
28 |
{
|
29 |
if (!mIsUpdating) |
30 |
{
|
31 |
addEntity(entity); |
32 |
}
|
33 |
else
|
34 |
{
|
35 |
mAddedEntities.push_back(entity); |
36 |
}
|
37 |
}
|
38 |
|
39 |
void EntityManager::update() |
40 |
{
|
41 |
mIsUpdating = true; |
42 |
|
43 |
for(std::list<Entity*>::iterator iter = mEntities.begin(); iter != mEntities.end(); iter++) |
44 |
{
|
45 |
(*iter)->update(); |
46 |
if ((*iter)->isExpired()) |
47 |
{
|
48 |
*iter = NULL; |
49 |
}
|
50 |
}
|
51 |
|
52 |
mIsUpdating = false; |
53 |
|
54 |
for(std::list<Entity*>::iterator iter = mAddedEntities.begin(); iter != mAddedEntities.end(); iter++) |
55 |
{
|
56 |
addEntity(*iter); |
57 |
}
|
58 |
|
59 |
mAddedEntities.clear(); |
60 |
|
61 |
mEntities.remove(NULL); |
62 |
|
63 |
for(std::list<Bullet*>::iterator iter = mBullets.begin(); iter != mBullets.end(); iter++) |
64 |
{
|
65 |
if ((*iter)->isExpired()) |
66 |
{
|
67 |
delete *iter; |
68 |
*iter = NULL; |
69 |
}
|
70 |
}
|
71 |
mBullets.remove(NULL); |
72 |
}
|
73 |
|
74 |
void EntityManager::draw(tSpriteBatch* spriteBatch) |
75 |
{
|
76 |
for(std::list<Entity*>::iterator iter = mEntities.begin(); iter != mEntities.end(); iter++) |
77 |
{
|
78 |
(*iter)->draw(spriteBatch); |
79 |
}
|
80 |
}
|
Denken Sie daran, dass Sie eine Laufzeitausnahme erhalten, wenn Sie eine Liste ändern, während Sie sie durchlaufen. Der obige Code sorgt dafür, dass alle während der Aktualisierung hinzugefügten Entitäten in einer separaten Liste in die Warteschlange gestellt und nach Abschluss der Aktualisierung der vorhandenen Entitäten hinzugefügt werden.
Sichtbar machen
Wir müssen einige Texturen laden, wenn wir etwas zeichnen möchten, also erstellen wir eine statische Klasse, die Verweise auf alle unsere Texturen enthält:
1 |
class Art |
2 |
: public tSingleton<Art> |
3 |
{
|
4 |
protected:
|
5 |
tTexture* mPlayer; |
6 |
tTexture* mSeeker; |
7 |
tTexture* mWanderer; |
8 |
tTexture* mBullet; |
9 |
tTexture* mPointer; |
10 |
|
11 |
protected:
|
12 |
Art(); |
13 |
|
14 |
public:
|
15 |
tTexture* getPlayer() const; |
16 |
tTexture* getSeeker() const; |
17 |
tTexture* getWanderer() const; |
18 |
tTexture* getBullet() const; |
19 |
tTexture* getPointer() const; |
20 |
|
21 |
friend class tSingleton<Art>; |
22 |
};
|
23 |
|
24 |
Art::Art() |
25 |
{
|
26 |
mPlayer = new tTexture(tSurface("player.png")); |
27 |
mSeeker = new tTexture(tSurface("seeker.png")); |
28 |
mWanderer = new tTexture(tSurface("wanderer.png")); |
29 |
mBullet = new tTexture(tSurface("bullet.png")); |
30 |
mPointer = new tTexture(tSurface("pointer.png")); |
31 |
}
|
Wir laden die Kunst, indem wir Art::getInstance()
in GameRoot::onInitView()
aufrufen. Dies führt dazu, dass der Art
-Singleton erstellt wird und der Konstruktor Art::Art()
aufgerufen wird.
Außerdem müssen einige Klassen die Bildschirmabmessungen kennen, sodass wir die folgenden Mitglieder in GameRoot
haben:
1 |
tDimension2f mViewportSize; |
2 |
tSpriteBatch* mSpriteBatch; |
3 |
|
4 |
tAutosizeViewport* mViewport; |
Und im GameRoot
-Konstruktor legen wir die Größe fest:
1 |
GameRoot::GameRoot() |
2 |
: mViewportSize(800, 600), |
3 |
mSpriteBatch(NULL) |
4 |
{
|
5 |
}
|
Die Auflösung 800x600px entspricht der des ursprünglichen XNA-basierten Shape Blaster. Wir können jede gewünschte Auflösung verwenden (z. B. eine, die näher an der spezifischen Auflösung eines iPhones oder iPads liegt), aber wir bleiben bei der ursprünglichen Auflösung, um sicherzustellen, dass unser Spiel dem Erscheinungsbild des Originals entspricht.
Jetzt gehen wir die PlayerShip
-Klasse durch:
1 |
class PlayerShip |
2 |
: public Entity, |
3 |
public tSingleton<PlayerShip> |
4 |
{
|
5 |
protected:
|
6 |
static const int kCooldownFrames; |
7 |
|
8 |
int mCooldowmRemaining; |
9 |
int mFramesUntilRespawn; |
10 |
|
11 |
protected:
|
12 |
PlayerShip(); |
13 |
|
14 |
public:
|
15 |
void update(); |
16 |
void draw(tSpriteBatch* spriteBatch); |
17 |
|
18 |
bool getIsDead(); |
19 |
void kill(); |
20 |
|
21 |
friend class tSingleton<PlayerShip>; |
22 |
};
|
23 |
|
24 |
PlayerShip::PlayerShip() |
25 |
: mCooldowmRemaining(0), |
26 |
mFramesUntilRespawn(0) |
27 |
{
|
28 |
mImage = Art::getInstance()->getPlayer(); |
29 |
mPosition = tPoint2f(GameRoot::getInstance()->getViewportSize().x / 2, GameRoot::getInstance()->getViewportSize().y / 2); |
30 |
mRadius = 10; |
31 |
}
|
Wir haben PlayerShip
zu einem Singleton gemacht, sein Bild festgelegt und es in der Mitte des Bildschirms platziert.
Zuletzt fügen wir das Spielerschiff dem EntityManager
hinzu. Der Code in GameRoot::onInitView
sieht folgendermaßen aus:
1 |
//In GameRoot::onInitView
|
2 |
EntityManager::getInstance()->add(PlayerShip::getInstance()); |
3 |
.
|
4 |
.
|
5 |
.
|
6 |
glClearColor(0,0,0,1); |
7 |
glEnable(GL_BLEND); |
8 |
glBlendFunc(GL_SRC_ALPHA, GL_ONE); |
9 |
|
10 |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); |
11 |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); |
12 |
|
13 |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
14 |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
15 |
glHint(GL_GENERATE_MIPMAP_HINT, GL_DONT_CARE); |
16 |
|
17 |
glDisable(GL_DEPTH_TEST); |
18 |
glDisable(GL_CULL_FACE); |
Wir zeichnen die Sprites mit additiver Mischung, was Teil ihres "Neon" -Looks ist. Wir wollen auch kein Verwischen oder Mischen, deshalb verwenden wir GL_NEAREST
für unsere Filter. Wir brauchen oder kümmern uns nicht um Tiefenprüfungen oder das Keulen von Rückseiten (es verursacht ohnehin nur unnötigen Overhead), also schalten wir es aus.
Der Code in GameRoot::onRedrawView
sieht folgendermaßen aus:
1 |
//In GameRoot::onRedrawView
|
2 |
EntityManager::getInstance()->update(); |
3 |
|
4 |
EntityManager::getInstance()->draw(mSpriteBatch); |
5 |
|
6 |
mSpriteBatch->draw(0, Art::getInstance()->getPointer(), Input::getInstance()->getMousePosition(), tOptional<tRectf>()); |
7 |
|
8 |
mViewport->run(); |
9 |
|
10 |
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); |
11 |
|
12 |
mSpriteBatch->end(); |
13 |
|
14 |
glFlush(); |
Wenn Sie das Spiel zu diesem Zeitpunkt ausführen, sollte Ihr Schiff in der Mitte des Bildschirms angezeigt werden. Es reagiert jedoch nicht auf Eingaben. Lassen Sie uns als nächstes dem Spiel etwas Input hinzufügen.
Eingang
Für die Bewegung verwenden wir eine Multitouch-Oberfläche. Bevor wir mit den Gamepads auf dem Bildschirm voll auf Touren kommen, müssen wir nur eine grundlegende Touch-Oberfläche zum Laufen bringen.
Im ursprünglichen Shape Blaster für Windows kann die Bewegung des Players mit den WASD-Tasten auf der Tastatur erfolgen. Zum Zielen könnten sie die Pfeiltasten oder die Maus verwenden. Dies soll die Twin-Stick-Steuerelemente von Geometry Wars emulieren: einen analogen Stick für die Bewegung, einen zum Zielen.
Da Shape Blaster bereits das Konzept der Tastatur- und Mausbewegung verwendet, können Sie Eingaben am einfachsten hinzufügen, indem Sie Tastatur- und Mausbefehle durch Berühren emulieren. Wir beginnen mit der Mausbewegung, da sowohl Berührung als auch Maus eine ähnliche Komponente haben: einen Punkt, der X- und Y-Koordinaten enthält.
Wir werden eine statische Klasse erstellen, um die verschiedenen Eingabegeräte zu verfolgen und um zwischen den verschiedenen Zielarten zu wechseln:
1 |
class Input |
2 |
: public tSingleton<Input> |
3 |
{
|
4 |
protected:
|
5 |
tPoint2f mMouseState; |
6 |
tPoint2f mLastMouseState; |
7 |
tPoint2f mFreshMouseState; |
8 |
std::vector<bool> mKeyboardState; |
9 |
std::vector<bool> mLastKeyboardState; |
10 |
std::vector<bool> mFreshKeyboardState; |
11 |
bool mIsAimingWithMouse; |
12 |
uint8_t mLeftEngaged; |
13 |
uint8_t mRightEngaged; |
14 |
|
15 |
public:
|
16 |
enum KeyType |
17 |
{
|
18 |
kUp = 0, |
19 |
kLeft, |
20 |
kDown, |
21 |
kRight, |
22 |
kW, |
23 |
kA, |
24 |
kS, |
25 |
kD, |
26 |
};
|
27 |
|
28 |
protected:
|
29 |
tVector2f GetMouseAimDirection() const; |
30 |
|
31 |
protected:
|
32 |
Input(); |
33 |
|
34 |
public:
|
35 |
tPoint2f getMousePosition() const; |
36 |
|
37 |
void update(); |
38 |
|
39 |
// Checks if a key was just pressed down
|
40 |
bool wasKeyPressed(KeyType) const; |
41 |
tVector2f getMovementDirection() const; |
42 |
tVector2f getAimDirection() const; |
43 |
|
44 |
void onKeyboard(const tKeyboardEvent& msg); |
45 |
void onTouch(const tTouchEvent& msg); |
46 |
|
47 |
friend class tSingleton<Input>; |
48 |
};
|
49 |
|
50 |
void Input::update() |
51 |
{
|
52 |
mLastKeyboardState = mKeyboardState; |
53 |
mLastMouseState = mMouseState; |
54 |
|
55 |
mKeyboardState = mFreshKeyboardState; |
56 |
mMouseState = mFreshMouseState; |
57 |
|
58 |
if (mKeyboardState[kLeft] || mKeyboardState[kRight] || mKeyboardState[kUp] || mKeyboardState[kDown]) |
59 |
{
|
60 |
mIsAimingWithMouse = false; |
61 |
}
|
62 |
else if (mMouseState != mLastMouseState) |
63 |
{
|
64 |
mIsAimingWithMouse = true; |
65 |
}
|
66 |
}
|
Wir rufen Input::update()
zu Beginn von GameRoot::onRedrawView()
auf, damit die Eingabeklasse funktioniert.
Wie bereits erwähnt, werden wir den keyboard
status später in der Serie verwenden, um Bewegungen zu berücksichtigen.
Schießen
Lassen wir jetzt das Schiff schießen.
Erstens brauchen wir eine Klasse für Kugeln.
1 |
class Bullet |
2 |
: public Entity |
3 |
{
|
4 |
public:
|
5 |
Bullet(const tPoint2f& position, const tVector2f& velocity); |
6 |
|
7 |
void update(); |
8 |
};
|
9 |
|
10 |
Bullet::Bullet(const tPoint2f& position, const tVector2f& velocity) |
11 |
{
|
12 |
mImage = Art::getInstance()->getBullet(); |
13 |
mPosition = position; |
14 |
mVelocity = velocity; |
15 |
mOrientation = atan2f(mVelocity.y, mVelocity.x); |
16 |
mRadius = 8; |
17 |
mKind = kBullet; |
18 |
}
|
19 |
|
20 |
void Bullet::update() |
21 |
{
|
22 |
if (mVelocity.lengthSquared() > 0) |
23 |
{
|
24 |
mOrientation = atan2f(mVelocity.y, mVelocity.x); |
25 |
}
|
26 |
|
27 |
mPosition += mVelocity; |
28 |
|
29 |
if (!tRectf(0, 0, GameRoot::getInstance()->getViewportSize()).contains(tPoint2f((int32_t)mPosition.x, (int32_t)mPosition.y))) |
30 |
{
|
31 |
mIsExpired = true; |
32 |
}
|
33 |
}
|
Wir wollen eine kurze Abklingzeit zwischen den Kugeln, also haben wir eine Konstante dafür:
1 |
const int PlayerShip::kCooldownFrames = 6; |
Außerdem fügen wir PlayerShip::Update()
den folgenden Code hinzu:
1 |
tVector2f aim = Input::getInstance()->getAimDirection(); |
2 |
if (aim.lengthSquared() > 0 && mCooldowmRemaining <= 0) |
3 |
{
|
4 |
mCooldowmRemaining = kCooldownFrames; |
5 |
float aimAngle = atan2f(aim.y, aim.x); |
6 |
|
7 |
float cosA = cosf(aimAngle); |
8 |
float sinA = sinf(aimAngle); |
9 |
tMatrix2x2f aimMat(tVector2f(cosA, sinA), |
10 |
tVector2f(-sinA, cosA)); |
11 |
|
12 |
float randomSpread = tMath::random() * 0.08f + tMath::random() * 0.08f - 0.08f; |
13 |
tVector2f vel = 11.0f * (tVector2f(cosA, sinA) + tVector2f(randomSpread, randomSpread)); |
14 |
|
15 |
tVector2f offset = aimMat * tVector2f(35, -8); |
16 |
EntityManager::getInstance()->add(new Bullet(mPosition + offset, vel)); |
17 |
|
18 |
offset = aimMat * tVector2f(35, 8); |
19 |
EntityManager::getInstance()->add(new Bullet(mPosition + offset, vel)); |
20 |
|
21 |
tSound* curShot = Sound::getInstance()->getShot(); |
22 |
if (!curShot->isPlaying()) |
23 |
{
|
24 |
curShot->play(0, 1); |
25 |
}
|
26 |
}
|
27 |
|
28 |
if (mCooldowmRemaining > 0) |
29 |
{
|
30 |
mCooldowmRemaining--; |
31 |
}
|
Dieser Code erstellt zwei Aufzählungszeichen, die parallel zueinander verlaufen. Es fügt der Richtung ein wenig Zufälligkeit hinzu, wodurch sich die Schüsse ein wenig wie bei einem Maschinengewehr ausbreiten. Wir addieren zwei Zufallszahlen, da dies die Wahrscheinlichkeit erhöht, dass ihre Summe zentriert ist (um Null) und die Wahrscheinlichkeit geringer ist, dass Kugeln weit entfernt gesendet werden. Wir verwenden eine zweidimensionale Matrix, um die Anfangsposition der Kugeln in die Richtung zu drehen, in die sie sich bewegen.
Wir haben auch zwei neue Hilfsmethoden verwendet:
-
Extensions::NextFloat()
: Gibt einen zufälligen Float zwischen einem minimalen und einem maximalen Wert zurück. -
MathUtil::FromPolar()
: Erstellt einentVector2f
aus einem Winkel und einer Größe.
Mal sehen, wie sie aussehen:
1 |
//In Extensions
|
2 |
float Extensions::nextFloat(float minValue, float maxValue) |
3 |
{
|
4 |
return (float)tMath::random() * (maxValue - minValue) + minValue; |
5 |
}
|
6 |
|
7 |
//In MathUtil
|
8 |
tVector2f MathUtil::fromPolar(float angle, float magnitude) |
9 |
{
|
10 |
return magnitude * tVector2f((float)cosf(angle), (float)sinf(angle)); |
11 |
}
|
Benutzerdefinierter Cursor
Jetzt, wo wir die erste Input
klasse haben, sollten wir noch etwas tun: Zeichnen wir einen benutzerdefinierten Mauszeiger, damit Sie leichter erkennen können, wohin das Schiff zielt. Zeichnen Sie in GameRoot.Draw
einfach den mPointer
von Art an der Position der "Maus".
1 |
mSpriteBatch->draw(0, Art::getInstance()->getPointer(), Input::getInstance()->getMousePosition(), tOptional<tRectf>()); |
Abschluss
Wenn Sie das Spiel jetzt testen, können Sie eine beliebige Stelle auf dem Bildschirm berühren, um auf den kontinuierlichen Strom von Kugeln zu zielen. Dies ist ein guter Anfang.
Im nächsten Teil werden wir das anfängliche Gameplay abschließen, indem wir die Feinde und eine Punktzahl hinzufügen.