Advertisement
  1. Game Development

Erstellen Sie einen Neon Vector Shooter für iOS: Erste Schritte

Scroll to top
Read Time: 16 min
This post is part of a series called Cross-Platform Vector Shooter: iOS.
Make a Neon Vector Shooter for iOS: The Warping Grid
Make a Neon Vector Shooter for iOS: More Gameplay

() 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:

  1. Erste Schritte: Einführung in die Utility-Bibliothek, Einrichten des grundlegenden Gameplays, Erstellen des Schiffs, des Sounds und der Musik des Spielers.
  2. Beenden Sie die Implementierung der Spielmechanik, indem Sie Feinde hinzufügen, die Kollisionserkennung übernehmen und die Punktzahl und das Leben des Spielers verfolgen.
  3. Fügen Sie ein virtuelles Gamepad auf dem Bildschirm hinzu, damit wir das Spiel mithilfe der Multitouch-Eingabe steuern können.
  4. Fügen Sie verrückte, übertriebene Partikeleffekte hinzu.
  5. 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.

spritesspritessprites

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.

font_invertfont_invertfont_invert

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 es Nullable heißt). Es ermöglicht uns, "optionale" Parameter für Methoden zu haben. Es wird in der tSpriteBatch-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ädt tSurfaces 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 zweier tShader-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 der SpriteBatch-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 und PlayerShip.
  • 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 und Extensions: 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 einen tVector2f 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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.