In dieser Serie von Tutorials zeige ich Ihnen, wie Sie einen von Geometry Wars inspirierten Doppelstock-Shooter mit Neon-Grafik, verrückten Partikeleffekten und beeindruckender Musik für iOS mit C ++ und OpenGL ES 2.0 erstellen. In diesem Teil fügen wir die virtuellen Gamepad-Steuerelemente und die "Schwarzen Löcher" -Einsteiger hinzu.
In der bisherigen Serie haben wir das grundlegende Gameplay für unseren Neon-Twin-Stick-Shooter Shape Blaster eingerichtet. Als Nächstes fügen wir zwei "virtuelle Gamepads" auf dem Bildschirm hinzu, mit denen Sie das Schiff steuern können.
Die Eingabe ist ein Muss für jedes Videospiel, und iOS bietet uns mit Multi-Touch-Eingabe eine interessante und mehrdeutige Herausforderung. Ich zeige Ihnen einen Ansatz, der auf dem Konzept von basiert virtuelle Gamepads, Hier simulieren wir Hardware-Gamepads, indem wir nur die Berührung und ein wenig komplexe Logik verwenden, um die Dinge herauszufinden. Nachdem Sie die virtuellen Gamepads für die Multi-Touch-Eingabe hinzugefügt haben, fügen wir dem Spiel auch schwarze Löcher hinzu.
Berührungsbasierte Steuerelemente auf dem Bildschirm sind die wichtigsten Eingabemöglichkeiten für die meisten iPhone- und iPad-basierten Apps und Spiele. Tatsächlich erlaubt iOS die Verwendung einer Multi-Touch-Oberfläche, dh das gleichzeitige Lesen mehrerer Berührungspunkte. Das Schöne an berührungsbasierten Benutzeroberflächen ist, dass Sie die Benutzeroberfläche so definieren können, wie Sie möchten, ob nun eine Taste, ein virtueller Steuerhebel oder ein Schieberegler. Was wir implementieren, ist ein Touch-Interface, das ich "virtuelle Gamepads" nenne.
EIN Gamepad Beschreibt normalerweise eine standardmäßige, Plus-förmige physische Steuerung, ähnlich der Plus-Schnittstelle eines Game Boy-Systems oder eines PlayStation-Controllers (auch als Richtungspad oder D-Pad bezeichnet). Ein Gamepad ermöglicht Bewegungen sowohl nach oben und nach unten als auch nach links und rechts. Das Ergebnis ist, dass Sie acht verschiedene Richtungen signalisieren können, wobei "keine Richtung" hinzugefügt wird. In Shape Blaster wird unsere Gamepad-Oberfläche nicht physisch sein, sondern auf dem Bildschirm virtuell Gamepad.
Ein typisches physisches Gamepad; Das Richtungspad ist in diesem Fall plusförmig.
Obwohl es nur vier Eingänge gibt, stehen acht Richtungen (plus Neutral) zur Verfügung.
Um ein virtuelles Gamepad in unserem Spiel zu haben, müssen wir die Berührungseingaben sofort erkennen und in eine Form konvertieren, die das Spiel bereits versteht.
Das hier implementierte virtuelle Gamepad funktioniert in drei Schritten:
In jedem Schritt konzentrieren wir uns ausschließlich auf die Berührung, die wir haben, und verfolgen das letzte Touch-Event, das wir vergleichen mussten. Wir werden auch den Überblick behalten Berührungsidentifikation, was bestimmt, welcher Finger welches Gamepad berührt.
Der Screenshot unten zeigt, wie die Gamepads auf dem Bildschirm erscheinen:
Screenshot der letzten in Position befindlichen Gamepads.
In dem Nützlichkeit
Bibliothek, schauen wir uns die Ereignisklasse an, die wir hauptsächlich verwenden werden. tTouchEvent
kapselt alles ein, was wir benötigen, um Berührungsereignisse auf einer grundlegenden Ebene zu behandeln.
Klasse tTouchEvent public: enum EventType kTouchBegin, kTouchEnd, kTouchMove,; public: EventType mEvent; tPoint2f mLocation; uint8_t mID; public: tTouchEvent (const EventType & newEvent, const tPoint2f & newLocation, const uint8_t & newID): mEvent (newEvent), mLocation (newLocation), mID (newID) ;
Das EventType
erlaubt uns, die Art der Ereignisse zu definieren, die wir zulassen, ohne zu kompliziert zu werden. mLocation
wird der eigentliche Berührungspunkt sein und mID
wird die Finger-ID sein, die bei Null beginnt und für jeden auf dem Bildschirm berührten Finger eine Eins hinzufügt. Wenn wir den Konstruktor definieren, um nur zu übernehmen const
Über Referenzen können wir Ereignisklassen instanziieren, ohne explizit benannte Variablen für sie erstellen zu müssen.
Wir werden verwenden tTouchEvent
um ausschließlich Touch-Events vom OS an unser zu senden Eingang
Klasse. Wir werden es später auch verwenden, um die grafische Darstellung der Gamepads im zu aktualisieren VirtualGamepad
Klasse.
Die ursprüngliche XNA- und C # -Version der Eingang
Klasse kann Maus-, Tastatur- und tatsächliche physische Gamepad-Eingaben verarbeiten. Mit der Maus wird an einer beliebigen Stelle auf dem Bildschirm von einer beliebigen Position aus geschossen. Die Tastatur kann verwendet werden, um sich in bestimmte Richtungen zu bewegen und zu schießen. Da wir uns entschieden haben, die ursprüngliche Eingabe zu emulieren (um einen "direkten Port" zu erhalten), bleibt der ursprüngliche Code größtenteils unverändert, wobei die Namen verwendet werden Tastatur
und Maus
, obwohl wir weder auf iOS-Geräten haben.
Hier ist wie unser Eingang
Klasse wird aussehen. Für jedes Gerät müssen wir einen "aktuellen Snapshot" und einen "vorherigen Snapshot" aufbewahren, damit wir feststellen können, was sich zwischen dem letzten Eingabeereignis und dem aktuellen Eingabeereignis geändert hat. In unserem Fall, mMouseState
und mKeyboardState
sind der "aktuelle Schnappschuss" und mLastMouseState
und mLastKeyboardState
repräsentieren den "vorherigen Schnappschuss".
Klasse Eingabe: public tSingleton protected: tPoint2fMMouseState; tPoint2f mLastMouseState; tPoint2f mFreshMouseState; std :: vectormKeyboardState; std :: vector mLastKeyboardState; std :: vector mFreshKeyboardState; bool mIsAimingWithMouse; uint8_t mLeftEngaged; uint8_t mRightEngaged; public: enum KeyType kUp = 0, kLeft, kDown, kRight, kW, kA, kS, kD; protected: tVector2f GetMouseAimDirection () const; geschützt: Eingabe (); public: tPoint2f getMousePosition () const; void update (); // prüft, ob gerade eine Taste gedrückt wurde bool wasKeyPressed (KeyType) const; tVector2f getMovementDirection () const; tVector2f getAimDirection () const; void onTouch (const tTouchEvent & msg); Freundenklasse tSingleton; Freundenklasse VirtualGamepad; ; Input :: Input (): mMouseState (-1, -1), mLastMouseState (-1, -1), mIsAimingWithMouse (false), mLeftEngaged (255), mRightEngaged (255) mKeyboardState.resize (8); mLastKeyboardState.resize (8); mFreshKeyboardState.resize (8); für (size_ti = 0; i < 8; i++) mKeyboardState[i] = false; mLastKeyboardState[i] = false; mFreshKeyboardState[i] = false; tPoint2f Input::getMousePosition() const return mMouseState;
Auf einem PC ist jedes Ereignis "eindeutig", was bedeutet, dass eine Mausbewegung anders ist als das Drücken des Buchstabens EIN, und sogar der Brief EIN unterscheidet sich genug vom Brief S dass wir sagen können, dass es nicht ist genau das gleiche Ereignis.
Mit iOS haben wir nur immer Holen Sie sich Tastereingaben, und eine Berührung unterscheidet sich nicht von einer anderen, um festzustellen, ob es sich um eine Mausbewegung oder einen Tastendruck handelt oder um welche Taste es sich handelt. Alle Ereignisse sehen aus unserer Sicht genau gleich aus.
Um diese Mehrdeutigkeit herauszufinden, stellen wir zwei neue Mitglieder vor, mFreshMouseState
und mFreshKeyboardState
. Ihr Zweck ist es, die Ereignisse in einem bestimmten Frame zusammenzufassen oder "alle abzufangen", ohne die anderen Zustandsvariablen anderweitig zu ändern. Wenn wir zufrieden sind, dass ein Frame bestanden hat, können wir den aktuellen Status mit den "neuen" Mitgliedern durch Aufruf aktualisieren Eingabe :: Update
. Eingabe :: Update
teilt auch unserem Eingabezustand mit, um voranzukommen.
void Input :: update () mLastKeyboardState = mKeyboardState; mLastMouseState = mMouseState; mKeyboardState = mFreshKeyboardState; mMouseState = mFreshMouseState; if (mKeyboardState [kLeft] || mKeyboardState [kRight] || mKeyboardState [kUp] || mKeyboardState [kDown]) mIsAimingWithMouse = false; else if (mMouseState! = mLastMouseState) mIsAimingWithMouse = true;
Da wir es einmal pro Frame machen, rufen wir an Eingabe :: Update ()
von innen GameRoot :: onRedrawView ()
:
// In GameRoot :: onRedrawView () Eingabe :: getInstance () -> update ();
Schauen wir uns nun an, wie wir die Berührungseingabe in eine simulierte Maus oder eine Tastatur umwandeln. Zunächst planen wir zwei verschiedene rechteckige Bereiche, die die virtuellen Gamepads darstellen. Alles außerhalb dieser Bereiche betrachten wir als "definitiv ein Mausereignis". In jedem Fall betrachten wir "definitiv ein Tastaturereignis".
Alles in den roten Kästchen werden wir unserer simulierten Tastatureingabe zuordnen. Alles andere behandeln wir wie eine Mauseingabe.
Lass uns sehen Eingabe :: onTouch ()
, das bekommt alle Berührungsereignisse. Wir machen einen großen Überblick über die Methode und notieren nur Bereiche MACHEN
Wo spezifischer Code sein sollte:
void Input :: onTouch (const tTouchEvent & msg) tPoint2f leftPoint = VirtualGamepad :: getInstance () -> mLeftPoint - tPoint2f (18, 18); tPoint2f rightPoint = VirtualGamepad :: getInstance () -> mRightPoint - tPoint2f (18, 18); tPoint2f intPoint ((int) msg.mLocation.x, (int) msg.mLocation.y); bool mouseDown = (msg.mEvent == tTouchEvent :: kTouchBegin) || (msg.mEvent == tTouchEvent :: kTouchMove); if (! mouseDown) if (msg.mID == mLeftEngaged) // TODO: Alle Bewegungstasten auf "key up" setzen else if (msg.mID == mRightEngaged) // TODO: Alle Abfeuertasten auf setzen "key up" if (mouseDown && tRectf (leftPoint, 164, 164) .contains (intPoint)) mLeftEngaged = msg.mID; // TODO: Alle Bewegungstasten auf "Schlüssel nach oben" setzen // TODO: Bestimmen, welche Bewegungstasten gesetzt werden sollen if (mouseDown && tRectf (rightPoint, 164, 164) .contains (intPoint)) mRightEngaged = msg.mID; // TODO: Alle Zündschlüssel als "Schlüssel nach oben" setzen // TODO: Bestimmen, welche Zündschlüssel gesetzt werden sollen if (! TRectf (leftPoint, 164, 164) .contains (intPoint) &&! TRectf (rightPoint, 164, 164) .contains (intPoint)) // Wenn wir es hier schaffen, war Touch definitiv ein "Mausereignis" mFreshMouseState = tPoint2f ((int32_t) msg.mLocation.x, (int32_t) msg.mLocation.y);
Der Code ist einfach genug, aber es gibt eine mächtige Logik, auf die ich hinweisen möchte:
leftPoint
und rightPoint
lokale Variablen.Maus nach unten
Zustand: Wenn wir mit einem Finger "drücken", müssen wir wissen, ob er drin ist leftPoint
ist richtig oder rightPoint
ist richtig, und wenn ja, ergreife das Update frisch Zustand für die Tastatur. Wenn dies nicht der Fall ist, nehmen wir an, dass es sich um ein Mausereignis handelt, und aktualisieren das frisch Zustand für die Maus.Nun, da wir das große Bild sehen, gehen wir noch ein bisschen weiter.
Wenn ein Finger von der Oberfläche des iPhones oder iPads abgehoben wird, prüfen wir, ob es sich um einen Finger handelt, von dem wir wissen, dass er sich auf einem Gamepad befindet. Wenn dies der Fall ist, setzen wir alle "simulierten Tasten" für dieses Gamepad zurück:
if (! mouseDown) if (msg.mID == mLeftEngaged) mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = false; mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = false; else if (msg.mID == mRightEngaged) mFreshKeyboardState [kUp] = false; mFreshKeyboardState [kDown] = false; mFreshKeyboardState [kLeft] = false; mFreshKeyboardState [kRight] = false;
Die Situation ist etwas anders, wenn eine Berührung von der Oberfläche aus beginnt oder sich bewegt; Wir prüfen, ob sich die Berührung in einem der Gamepads befindet. Da der Code für beide Gamepads ähnlich ist, werfen wir nur einen Blick auf das linke Gamepad (das sich mit Bewegung befasst)..
Sobald wir ein Touch-Event erhalten, löschen wir den Tastaturstatus für dieses bestimmte Gamepad vollständig und prüfen im rechten Bereich, welche Tasten oder Tasten gedrückt werden müssen. Obwohl wir insgesamt acht Richtungen (plus Neutral) haben, werden immer nur vier Rechtecke geprüft: eines für Aufwärts, eines für Abwärts, eines für Links und eines für Rechts.
Die neun interessanten Bereiche in unserem Gamepad.
if (mouseDown && tRectf (leftPoint, 164, 164) .contains (intPoint)) mLeftEngaged = msg.mID; mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = false; mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = false; if (tRectf (leftPoint, 72, 164) .contains (intPoint)) mFreshKeyboardState [kA] = true; mFreshKeyboardState [kD] = false; else if (tRectf (leftPoint + tPoint2f (128, 0), 72, 164) .contains (intPoint)) mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = true; if (tRectf (leftPoint, 164, 72) .contains (intPoint)) mFreshKeyboardState [kW] = true; mFreshKeyboardState [kS] = false; else if (tRectf (leftPoint + tPoint2f (0, 128), 164, 72) .contains (intPoint)) mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = true;
Wenn Sie das Spiel jetzt ausführen, haben Sie Unterstützung für virtuelles Gamepad, aber Sie können nicht wirklich sehen, wo die virtuellen Gamepads beginnen oder enden.
Dies ist, wo die VirtualGamepad
Klasse kommt ins Spiel. Das VirtualGamepad
Der Hauptzweck ist das Zeichnen der Gamepads auf dem Bildschirm. Die Art und Weise, wie wir das Gamepad anzeigen, ist die Art und Weise, wie andere Spiele es tun, wenn sie Gamepads haben: Als größeren "Basis" -Kreis und einen kleineren "Kontrollstab" -Kreis können wir uns bewegen. Dies ähnelt einem Arcade-Joystick von oben nach unten und ist einfacher zu zeichnen als andere Alternativen.
Beachten Sie zunächst, dass die Bilddateien vpad_top.png
und vpad_bot.png
wurden dem Projekt hinzugefügt. Ändern wir das Kunst
Klasse, um sie zu laden:
Klasse Kunst: public tSingletonprotected:… tTexture * mVPadBottom; tTexture * mVPadTop;… public:… tTexture * getVPadBottom () const; tTexture * getVPadTop () const;… Friend-Klasse tSingleton ; ; Art :: Art () … mVPadTop = new tTexture (tSurface ("vpad_top.png")); mVPadBottom = new tTexture (tSurface ("vpad_bot.png")); tTexture * Art :: getVPadBottom () const return mVPadBottom; tTexture * Art :: getVPadTop () const return mVPadTop;
Das VirtualGamepad
Die Klasse zeichnet beide Gamepads auf dem Bildschirm und behält sie bei Zustand
Informationen in den Mitgliedern mleftStick
und mRightStick
wo die "Steuerknüppel" der Gamepads gezeichnet werden.
Ich habe ein paar willkürliche Positionen für die Gamepads gewählt, die in der initialisiert werden mleftPoint
und mRightPoint
Mitglieder - die Berechnungen legen sie auf etwa 3,75% vom linken oder rechten Rand des Bildschirms und etwa 13% vom unteren Rand des Bildschirms ab. Ich habe diese Messungen an einem kommerziellen Spiel mit ähnlichen virtuellen Gamepads, aber unterschiedlichem Gameplay vorgenommen.
Klasse VirtualGamepad: public tSingletonpublic: enum State kCenter = 0x00, kTop = 0x01, kBottom = 0x02, kLeft = 0x04, kRight = 0x08, kTopLeft = 0x05, kTopRight = 0x09, kBottomLeft = 0x06, kBottomRight = 0x0a,; protected: tPoint2f mLeftPoint; tPoint2f mRightPoint; int mLeftStick; int mRightStick; protected: VirtualGamepad (); void DrawStickAtPoint (tSpriteBatch * spriteBatch, const tPoint2f & point, state state); void UpdateBasedOnKeys (); public: void draw (tSpriteBatch * spriteBatch); void update (const tTouchEvent & msg); Freundenklasse tSingleton ; Freundenklasse Eingabe; ; VirtualGamepad :: VirtualGamepad (): mLeftStick (kCenter), mRightStick (kCenter) mLeftPoint = tPoint2f (int (3.0f / 80.0f * 800.0f), 600 - int (21.0f / 160.0f * 600.0f) - 128); mRightPoint = tPoint2f (800 - int (3.0f / 80.0f * 800.0f) - 128, 600 - int (21.0f / 160.0f * 600.0f) - 128);
Wie vorab erwähnt, mleftStick
und mRightStick
sind Bitmasken, und sie bestimmen, wo der innere Kreis des Gamepads gezeichnet wird. Wir berechnen die Bitmaske in der Methode VirtualGamepad :: UpdateBasedOnKeys ()
.
Diese Methode wird unmittelbar danach aufgerufen Eingabe :: onTouch
, damit wir die "frischen" Staatsmitglieder lesen können und wissen, dass sie auf dem neuesten Stand sind:
void VirtualGamepad :: UpdateBasedOnKeys () Eingabe * inp = Eingabe :: getInstance (); mLeftStick = kCenter; if (inp-> mFreshKeyboardState [Eingabe :: kA]) mLeftStick | = kLeft; else if (inp-> mFreshKeyboardState [Eingabe :: kD]) mLeftStick | = kRight; if (inp-> mFreshKeyboardState [Eingabe :: kW]) mLeftStick | = kTop; else if (inp-> mFreshKeyboardState [Eingabe :: kS]) mLeftStick | = kBottom; mRightStick = kCenter; if (inp-> mFreshKeyboardState [Eingabe :: kLeft]) mRightStick | = kLeft; else if (inp-> mFreshKeyboardState [Eingabe :: kRight]) mRightStick | = kRight; if (inp-> mFreshKeyboardState [Eingabe :: kUp]) mRightStick | = kTop; else if (inp-> mFreshKeyboardState [Eingabe :: kDown]) mRightStick | = kBottom;
Um ein individuelles Gamepad zu zeichnen, rufen wir an VirtualGamepad :: DrawStickAtPoint ()
; Diese Methode weiß nicht, welches Gamepad Sie zeichnen, es weiß nur, wo es gezeichnet werden soll und in welchem Zustand es gezeichnet werden soll. Da wir Bitmasken verwendet und im Voraus berechnet haben, wird unsere Methode kleiner und einfacher lesen:
void VirtualGamepad :: DrawStickAtPoint (tSpriteBatch * spriteBatch, const tPoint2f & point, state state) tPoint2f offset = tPoint2f (18, 18); spriteBatch-> draw (10, Art :: getInstance () -> getVPadBottom (), point, tOptional()); switch (state) case kCenter: offset + = tPoint2f (0, 0); brechen; Fall kTopLeft: Offset + = tPoint2f (-13, -13); brechen; Fall kTop: Offset + = tPoint2f (0, -18); brechen; Fall kTopRight: Offset + = tPoint2f (13, -13); brechen; Fall kRight: Offset + = tPoint2f (18, 0); brechen; case kBottomRight: offset + = tPoint2f (13, 13); brechen; case kBottom: offset + = tPoint2f (0, 18); brechen; case kBottomLeft: offset + = tPoint2f (-13, 13); brechen; case kLeft: offset + = tPoint2f (-18, 0); brechen; spriteBatch-> draw (11, Art :: getInstance () -> getVPadTop (), Punkt + Versatz, tOptional ());
Das Zeichnen von zwei Gamepads wird viel einfacher, da die obige Methode nur zweimal aufgerufen wird. Lass uns sehen VirtualGamepad :: draw ()
:
void VirtualGamepad :: draw (tSpriteBatch * spriteBatch) DrawStickAtPoint (spriteBatch, mLeftPoint, (State) mLeftStick); DrawStickAtPoint (spriteBatch, mRightPoint, (State) mRightStick);
Schließlich müssen wir tatsächlich das virtuelle Gamepad zeichnen, also in GameRoot :: onRedrawView ()
, füge folgende Zeile hinzu:
VirtualGamepad :: getInstance () -> draw (mSpriteBatch);
Das ist es. Wenn Sie das Spiel jetzt ausführen, sollten Sie die virtuellen Gamepads in voller Wirkung sehen. Wenn Sie das linke Gamepad berühren, sollten Sie sich bewegen. Wenn Sie das rechte Gamepad berühren, sollte sich Ihre Schussrichtung ändern. Tatsächlich können Sie beide Gamepads gleichzeitig verwenden. Sie können sich sogar mit dem linken Gamepad bewegen und außerhalb des rechten Gamepads berühren, um die Mausbewegung zu erhalten. Und wenn Sie loslassen, hören Sie auf sich zu bewegen und stoppen (möglicherweise) das Schießen.
Wir haben die Unterstützung für virtuelle Gamepads vollständig implementiert, und es funktioniert, aber Sie finden es vielleicht etwas unbeholfen oder schwer zu benutzen. Warum ist das so? Hier besteht die eigentliche Herausforderung von touchbasierten Steuerelementen unter iOS bei traditionellen Spielen, die ursprünglich nicht für sie entwickelt wurden.
Du bist aber nicht alleine. Viele Spiele leiden entweder unter diesen Problemen und haben sie überwunden.
Hier sind einige Dinge, die ich bei der Eingabe über den Touchscreen beobachtet habe. Vielleicht haben Sie selbst ähnliche Beobachtungen:
Erstens haben Game-Controller ein anderes Gefühl als ein flacher Touchscreen. Sie wissen, wo sich Ihr Finger auf einem echten Gamepad befindet und wie Sie verhindern, dass Ihre Finger abrutschen. Auf einem Touchscreen können Ihre Finger jedoch etwas zu weit aus dem Touchbereich herausdriften, sodass Ihre Eingabe möglicherweise nicht richtig erkannt wird und Sie möglicherweise nicht erkennen, dass dies der Fall ist, bis es zu spät ist.
Zweitens haben Sie möglicherweise auch beim Spielen mit Touch-Bedienelementen bemerkt, dass Ihre Hand Ihre Sicht verdeckt, so dass Ihr Schiff möglicherweise von einem Feind unter Ihrer Hand getroffen wird, den Sie anfangs nicht gesehen haben!
Schließlich stellen Sie möglicherweise fest, dass die Berührungsbereiche auf einem iPad einfacher zu verwenden sind als auf einem iPhone oder umgekehrt. Wir haben also Probleme mit einer anderen Bildschirmgröße, die sich auf unsere "Eingabebereichsgröße" auswirkt. Dies ist definitiv etwas, das wir auf einem Desktop-Computer nicht so häufig erleben. (Die meisten Tastaturen und Mäuse haben dieselbe Größe und wirken auf dieselbe Weise oder können angepasst werden.)
Hier sind einige Änderungen, die Sie an dem in diesem Artikel beschriebenen Eingabesystem vornehmen könnten:
Wieder liegt es an Ihnen, was Sie tun möchten und wie Sie es tun wollen. Auf der positiven Seite gibt es viele Möglichkeiten, die Berührungseingabe durchzuführen. Der schwierige Teil ist, es richtig zu machen und die Spieler glücklich zu machen.
Einer der interessantesten Feinde in Geometry Wars ist der schwarzes Loch. Sehen wir uns an, wie wir in Shape Blaster etwas ähnliches machen können. Wir werden jetzt die grundlegenden Funktionen erstellen und den Feind im nächsten Tutorial erneut besuchen, um Partikeleffekte und Partikelinteraktionen hinzuzufügen.
Ein schwarzes Loch mit umkreisenden Partikeln
Die schwarzen Löcher ziehen das Schiff des Spielers, Feinde in der Nähe und (nach dem nächsten Lernprogramm) Partikel an, stoßen aber Kugeln ab.
Es gibt viele mögliche Funktionen, die wir zum Anziehen oder Abstoßen verwenden können. Am einfachsten ist es, eine konstante Kraft anzuwenden, damit das Schwarze Loch unabhängig von der Entfernung des Objekts mit der gleichen Stärke zieht. Eine andere Möglichkeit besteht darin, die Kraft linear von null auf eine maximale Entfernung bis zur vollen Stärke für Objekte direkt über dem schwarzen Loch erhöhen zu lassen. Wenn wir die Schwerkraft realistischer modellieren möchten, können wir das umgekehrte Quadrat der Entfernung verwenden, dh die Schwerkraft ist proportional zu 1 / (Abstand ^ 2)
.
Wir werden tatsächlich jede dieser drei Funktionen verwenden, um verschiedene Objekte zu behandeln. Die Kugeln werden mit konstanter Kraft abgestoßen; Die Feinde und das Schiff des Spielers werden mit einer linearen Kraft angezogen. und die Teilchen werden eine inverse Quadratfunktion verwenden.
Wir werden eine neue Klasse für Schwarze Löcher machen. Beginnen wir mit der grundlegenden Funktionalität:
Klasse BlackHole: public Entity protected: int mHitPoints; public: BlackHole (const tVector2f & position); void update (); void draw (tSpriteBatch * spriteBatch); void wasShot (); void kill (); ; BlackHole :: BlackHole (const tVector2f & position): mHitPoints (10) mImage = Art :: getInstance () -> getBlackHole (); mPosition = Position; mRadius = mImage-> getSurfaceSize (). width / 2.0f; mKind = kBlackHole; void BlackHole :: wasShot () mHitPoints--; if (mHitPoints <= 0) mIsExpired = true; void BlackHole::kill() mHitPoints = 0; wasShot(); void BlackHole::draw(tSpriteBatch* spriteBatch) // make the size of the black hole pulsate float scale = 1.0f + 0.1f * sinf(tTimer::getTimeMS() * 10.0f / 1000.0f); spriteBatch->draw (1, mImage, tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y), tOptional(), mColor, mOrientation, getSize () / 2.0f, tVector2f (Skala));
Die schwarzen Löcher brauchen zehn Schüsse, um zu töten. Wir passen die Skalierung des Sprites leicht an, damit es pulsiert. Wenn Sie beschließen, dass das Zerstören von Schwarzen Löchern auch Punkte gewähren sollte, müssen Sie ähnliche Anpassungen an dem vornehmen Schwarzes Loch
Klasse wie wir es mit dem gemacht haben Feind
Klasse.
Als Nächstes werden wir die schwarzen Löcher dazu bringen, eine Kraft auf andere Entitäten anzuwenden. Wir brauchen eine kleine Hilfsmethode von unserer EntityManager
:
std :: listEntityManager :: getNearbyEntities (const tPoint2f & pos, Floatradius) std :: list Ergebnis; für (std :: list :: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) if (* iter) if (pos.distanceSquared ((* iter) -> getPosition ()) < radius * radius) result.push_back(*iter); return result;
Diese Methode könnte durch Verwendung eines komplizierteren räumlichen Partitionierungsschemas effizienter gestaltet werden, aber für die Anzahl der Entitäten, die wir haben werden, ist dies in Ordnung.
Jetzt können wir die schwarzen Löcher dazu bringen, Kraft in ihre zu bringen BlackHole :: Update ()
Methode:
void BlackHole :: update () std :: listEntities = EntityManager :: getInstance () -> getNearbyEntities (mPosition, 250); für (std :: list :: iterator iter = entity.begin (); iter! = entity.end (); iter ++) if ((* iter) -> getKind () == kEnemy &&! ((Feind *) (* iter)) -> getIsActive ()) // Nichts tun sonst, wenn ((* iter) -> getKind () == kBullet) tVector2f temp = ((* iter) -> getPosition () - mPosition); (* iter) -> setVelocity ((* iter) -> getVelocity () + temp.normalize () * 0.3f); else tVector2f dPos = mPosition - (* iter) -> getPosition (); Floatlänge = dPos.length (); (* iter) -> setVelocity ((* iter) -> getVelocity () + dPos.normalize () * tMath :: mix (2.0f, 0.0f, Länge / 250.0f));
Schwarze Löcher betreffen nur Objekte innerhalb eines ausgewählten Radius (250 Pixel). Auf Kugeln innerhalb dieses Radius wird eine konstante abstoßende Kraft ausgeübt, während auf alle anderen eine lineare Anziehungskraft ausgeübt wird.
Wir müssen das Kollisionshandling für Schwarze Löcher ergänzen EntityManager
. Fügen Sie eine hinzu std :: list
für Schwarze Löcher wie bei den anderen Entitäten, und fügen Sie den folgenden Code hinzu EntityManager :: handleCollisions ()
:
// behandelt Kollisionen mit schwarzen Löchern für (std :: list:: Iterator i = mBlackHoles.begin (); i! = mBlackHoles.end (); i ++) für (std :: list :: Iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) if ((* j) -> getIsActive () && isColliding (* i, * j)) (* j) -> wasShot (); für (std :: list :: Iterator j = mBullets.begin (); j! = mBullets.end (); j ++) if (isColliding (* i, * j)) (* j) -> setExpired (); (* i) -> wasShot (); if (isColliding (PlayerShip :: getInstance (), * i)) KillPlayer (); brechen;
Schließlich öffnen Sie die EnemySpawner
Klasse und lassen Sie einige schwarze Löcher schaffen. Ich beschränkte die maximale Anzahl von schwarzen Löchern auf zwei und gab die Chance, dass in jedem Bild ein schwarzes Loch erscheint.
if (EntityManager :: getInstance () -> getBlackHoleCount () < 2 && int32_t(tMath::random() * mInverseBlackHoleChance) == 0) EntityManager::getInstance()->add (new BlackHole (GetSpawnPosition ()));
Wir haben virtuelle Gamepads besprochen und hinzugefügt und Schwarze Löcher mit verschiedenen Kraftformeln hinzugefügt. Shape Blaster fängt an, ziemlich gut auszusehen. Im nächsten Teil werden wir einige verrückte Partikeleffekte hinzufügen.