Crea uno sparatutto al neon per iOS gamepad virtuali e buchi neri

In questa serie di tutorial, ti mostrerò come creare uno sparatutto twin-stick ispirato a Geometry Wars, con grafica al neon, effetti pazzeschi e musica fantastica, per iOS che utilizza C ++ e OpenGL ES 2.0. In questa parte, aggiungeremo i controlli del gamepad virtuale e i nemici del "buco nero".

Panoramica

Nella serie finora abbiamo impostato il gameplay di base per il nostro sparatutto al neon gemello, Shape Blaster. Successivamente, aggiungeremo due "gamepad virtuali" su schermo per controllare la nave.


L'input è un must per qualsiasi videogioco e iOS ci offre una sfida interessante e ambigua con l'input multi-touch. Ti mostrerò un approccio, basato sul concetto di gamepad virtuali, dove simuleremo i gamepad hardware usando solo il tocco e un po 'di logica complessa per capire le cose. Dopo aver aggiunto i gamepad virtuali per l'input multi-touch, aggiungeremo anche buchi neri al gioco.

Gamepad virtuali

I comandi a schermo, basati sul tocco sono il mezzo principale di input per la maggior parte delle app e dei giochi basati su iPhone e iPad. Infatti, iOS consente l'uso di un'interfaccia multi-touch, che significa leggere diversi punti di tocco contemporaneamente. La bellezza delle interfacce basate sul tocco è che puoi definire l'interfaccia come qualsiasi cosa tu voglia, sia che si tratti di un pulsante, di un joystick virtuale o di un controllo scorrevole. Quello che implementeremo è un'interfaccia touch che chiamerò "gamepad virtuali".

UN gamepad in genere descrive un controllo fisico standard più di forma simile all'interfaccia plus su un sistema Game Boy o controller PlayStation (noto anche come pad direzionale o D-pad). Un gamepad consente il movimento sia sull'asse ascendente che su quello inferiore e sull'asse sinistro e destro. Il risultato è che sei in grado di segnalare otto direzioni distinte, con l'aggiunta di "nessuna direzione". In Shape Blaster, la nostra interfaccia gamepad non sarà fisica, ma sullo schermo, quindi a virtuale gamepad.


Un tipico gamepad fisico; il pad direzionale in questo caso è a forma più.

Sebbene siano disponibili solo quattro ingressi, sono disponibili otto direzioni (più neutro).

Per avere un gamepad virtuale nel nostro gioco, dobbiamo riconoscere il touch input quando succede e convertirlo in una forma che il gioco comprende già.

Il gamepad virtuale qui implementato funziona in tre passaggi:

  1. Determina il tipo di tocco.
  2. Determina se si trova nell'area di un gamepad su schermo.
  3. Emula il tocco come un tasto premuto o il movimento del mouse.

In ogni passaggio ci concentreremo esclusivamente sul tocco che abbiamo e teniamo traccia dell'ultimo evento touch che abbiamo dovuto confrontare. Terremo anche traccia del tocco ID, che determina quale dito sta toccando quale gamepad.

Lo screenshot qui sotto mostra come appariranno i gamepad sullo schermo:

Screenshot dei gamepad finali in posizione.

Aggiunta di Multi-Touch per Shape Blaster

Nel Utilità libreria, diamo un'occhiata alla classe di eventi che utilizzeremo principalmente. tTouchEvent incapsula tutto ciò di cui abbiamo bisogno per gestire gli eventi tattili a livello base.

 class tTouchEvent public: enum EventType kTouchBegin, kTouchEnd, kTouchMove,; public: EventType mEvent; tPoint2f mLocation; uint8_t mID; public: tTouchEvent (const EventType e newEvent, const tPoint2f & newLocation, const uint8_t & newID): mEvent (newEvent), mLocation (newLocation), mID (newID) ;

Il Tipo di evento ci consente di definire il tipo di eventi che consentiremo senza complicarci troppo. mlocation sarà il vero punto di contatto, e mID sarà l'ID del dito, che inizia da zero e ne aggiunge uno per ogni dito sfiorato sullo schermo. Se definiamo il costruttore da prendere solo const riferimenti, saremo in grado di creare istanze di classi di eventi senza dover creare in modo esplicito variabili con nome.

Useremo tTouchEvent esclusivamente per inviare eventi touch dal sistema operativo al nostro Ingresso classe. Lo utilizzeremo anche in seguito per aggiornare la rappresentazione grafica dei gamepad in VirtualGamepad classe.

La classe di input

La versione originale di XNA e C # del Ingresso class può gestire il mouse, la tastiera e gli ingressi fisici reali del gamepad. Il mouse viene utilizzato per sparare a un punto arbitrario sullo schermo da qualsiasi posizione; la tastiera può essere utilizzata per spostare e scattare in determinate direzioni. Dato che abbiamo scelto di emulare l'input originale (per rimanere fedele a una "porta diretta"), manterremo la maggior parte del codice originale lo stesso, usando i nomi tastiera e topo, anche se non abbiamo né su dispositivi iOS.

Ecco come il nostro Ingresso la classe sembrerà. Per ogni dispositivo, sarà necessario mantenere una "istantanea corrente" e "un'istantanea precedente" in modo da poter dire cosa è cambiato tra l'ultimo evento di input e l'evento di input corrente. Nel nostro caso, mMouseState e mKeyboardState sono "l'istantanea corrente" e mLastMouseState e mLastKeyboardState rappresenta la "precedente istantanea".

 classe Input: pubblico tSingleton protected: tPoint2f mMouseState; tPoint2f mLastMouseState; tPoint2f mFreshMouseState; std :: vector mKeyboardState; 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; protected: Input (); public: tPoint2f getMousePosition () const; void update (); // Controlla se una chiave è stata appena premuta bool wasKeyPressed (KeyType) const; tVector2f getMovementDirection () const; tVector2f getAimDirection () const; void onTouch (const tTouchEvent & msg); classe di amici tSingleton; classe di amici VirtualGamepad; ; Input :: Input (): mMouseState (-1, -1), mLastMouseState (-1, -1), mIsAimingWithMouse (false), mLeftEngaged (255), mRightEngaged (255) mKeyboardState.resize (8); mLastKeyboardState.resize (8); mFreshKeyboardState.resize (8); per (size_t i = 0; i < 8; i++)  mKeyboardState[i] = false; mLastKeyboardState[i] = false; mFreshKeyboardState[i] = false;   tPoint2f Input::getMousePosition() const  return mMouseState; 

Aggiornamento dell'input

Su un PC, qualsiasi evento che otteniamo è "distinto", il che significa che un movimento del mouse è diverso da quello della lettera UN, e anche la lettera UN è abbastanza diverso dalla lettera S che possiamo dire che non lo è Esattamente lo stesso evento.

Con iOS, noi solo sempre ricevi gli input input touch, e un tocco non è abbastanza distinto da un altro per noi per dire se è pensato per essere un movimento del mouse o una pressione di un tasto, o anche quale chiave è. Tutti gli eventi sembrano esattamente uguali dal nostro punto di vista.

Per aiutare a capire questa ambiguità, introdurremo due nuovi membri, mFreshMouseState e mFreshKeyboardState. Il loro scopo è quello di aggregare, o "catturare tutti", gli eventi in un particolare fotogramma, senza modificare altrimenti le altre variabili di stato. Una volta che siamo soddisfatti, un frame è passato, possiamo aggiornare lo stato corrente con i membri "freschi" chiamando Ingresso :: aggiornamento. Ingresso :: aggiornamento dice anche al nostro stato di input di avanzare.

 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; 

Dato che lo faremo una volta per frame, chiameremo Ingresso :: update () dall'interno GameRoot :: onRedrawView ():

 // In GameRoot :: onRedrawView () Input :: getInstance () -> update ();

Ora vediamo come trasformiamo l'input touch in mouse o tastiera simulati. Innanzitutto, pianificheremo di avere due diverse aree rettangolari che rappresentano i gamepad virtuali. Qualunque cosa al di fuori di queste aree considereremo "sicuramente un evento del mouse"; qualsiasi cosa all'interno, considereremo "sicuramente un evento di tastiera".

Qualsiasi cosa all'interno delle caselle rosse mapperemo al nostro input di tastiera simulato; qualsiasi altra cosa tratteremo come input del mouse.

Guardiamo Ingresso :: onTouch (), che ottiene tutti gli eventi touch. Daremo un'occhiata al metodo e osserveremo le aree FARE dove dovrebbe essere più codice specifico:

 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: imposta tutti i tasti di spostamento come "key up" else if (msg.mID == mRightEngaged) // TODO: imposta tutti i tasti di attivazione come "key up" if (mouseDown && tRectf (leftPoint, 164, 164). contiene (intPoint)) mLeftEngaged = msg.mID; // TODO: Imposta tutti i tasti di movimento come "key up" // TODO: Determina quali tasti di spostamento impostare if (mouseDown && tRectf (rightPoint, 164, 164). Contiene (intPoint)) mRightEngaged = msg.mID; // TODO: Imposta tutte le chiavi di attivazione come "key up" // TODO: Determina quali chiavi di attivazione impostare if (! TRectf (leftPoint, 164, 164). Contiene (intPoint) &&! TRectf (rightPoint, 164, 164) .contains (intPoint)) // Se lo facciamo qui, touch è stato sicuramente un "evento del mouse" mFreshMouseState = tPoint2f ((int32_t) msg.mLocation.x, (int32_t) msg.mLocation.y); 

Il codice è abbastanza semplice, ma c'è un po 'di logica potente che farò notare:

  1. Determiniamo dove saranno posizionati i gamepad sinistro e destro sullo schermo, in modo che possiamo vedere se ci troviamo dentro quando tocchiamo o lasciamo andare. Questi sono memorizzati nel leftPoint e rightPoint variabili locali.
  2. Determiniamo il mouseDown stato: se stiamo "premendo" con un dito, abbiamo bisogno di sapere se è dentro leftPointè rect o rightPointE 'giusto, e se è così agisci per aggiornare il fresco stato per la tastiera. Se non si trova in nessun caso, supponiamo che si tratti di un evento del mouse e che aggiorni il file fresco stato per il topo.
  3. Infine, teniamo traccia degli ID touch (o ID dito) mentre vengono premuti; se rileviamo un dito che si solleva dalla superficie ed è associato a un gamepad attivo, ripristineremo di conseguenza la tastiera simulata per detto gamepad.

Ora che vediamo il quadro generale, approfondiamo un po 'di più.

Riempiendo le lacune

Quando un dito viene sollevato dalla superficie dell'iPhone o dell'iPad, controlliamo se è un dito che conosciamo su un gamepad e, in tal caso, ripristiniamo tutti i "tasti simulati" per quel gamepad:

 if (! mouseDown) if (msg.mID == mLeftEngaged) mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = falso; mFreshKeyboardState [kW] = falso; mFreshKeyboardState [kS] = falso;  else if (msg.mID == mRightEngaged) mFreshKeyboardState [kUp] = false; mFreshKeyboardState [kDown] = falso; mFreshKeyboardState [kLeft] = false; mFreshKeyboardState [kRight] = falso; 

La situazione è un po 'diversa quando c'è un tocco che inizia in superficie o si muove; controlliamo se il tocco è all'interno di entrambi i gamepad. Poiché il codice per entrambi i gamepad è simile, daremo solo un'occhiata al gamepad sinistro (che si occupa del movimento).

Ogni volta che riceviamo un evento touch, azzeriamo completamente lo stato della tastiera per quel particolare gamepad e controlliamo all'interno della nostra area rect per determinare quale tasto o tasto premere. Quindi, anche se abbiamo un totale di otto direzioni (più neutro), controlleremo solo quattro rettangoli: uno per l'alto, uno per il basso, uno per la sinistra e l'altro per la destra.

Le nove aree di interesse nel nostro gamepad.
 if (mouseDown && tRectf (leftPoint, 164, 164). contiene (intPoint)) mLeftEngaged = msg.mID; mFreshKeyboardState [kA] = falso; mFreshKeyboardState [kD] = falso; mFreshKeyboardState [kW] = falso; mFreshKeyboardState [kS] = falso; if (tRectf (leftPoint, 72, 164). containtain (intPoint)) mFreshKeyboardState [kA] = true; mFreshKeyboardState [kD] = falso;  else if (tRectf (leftPoint + tPoint2f (128, 0), 72, 164). contiene (intPoint)) mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = true;  if (tRectf (leftPoint, 164, 72). containtain (intPoint)) mFreshKeyboardState [kW] = true; mFreshKeyboardState [kS] = falso;  else if (tRectf (leftPoint + tPoint2f (0, 128), 164, 72). containtain (intPoint)) mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = true; 

Visualizzazione grafica per il gamepad virtuale

Se esegui il gioco ora, avrai il supporto per il gamepad virtuale, ma non sarai in grado di vedere dove iniziano o terminano i gamepad virtuali.

Questo è dove il VirtualGamepad la classe entra in gioco. Il VirtualGamepadLo scopo principale è quello di disegnare i gamepad sullo schermo. Il modo in cui mostreremo il gamepad sarà il modo in cui gli altri giochi tendono a farlo se dispongono di gamepad: come un cerchio "base" più grande, e un cerchio "control stick" più piccolo che possiamo spostare. Sembra simile a un joystick arcade dall'alto verso il basso e più facile da disegnare rispetto ad altre alternative.

In primo luogo, si noti che i file di immagine vpad_top.png e vpad_bot.png sono stati aggiunti al progetto. Modifichiamo il Arte classe per caricarli:

 classe Arte: pubblico tSingleton protected: ... tTexture * mVPadBottom; tTexture * mVPadTop; ... public: ... tTexture * getVPadBottom () const; tTexture * getVPadTop () const; ... classe di amici 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; 

Il VirtualGamepad la classe disegnerà entrambi i gamepad sullo schermo e manterrà Stato informazioni nei membri mLeftStick e mRightStick su dove disegnare i "bastoncini di controllo" dei gamepad.

Ho scelto alcune posizioni leggermente arbitrarie per i gamepad, che sono inizializzati nel mLeftPoint e mRightPoint membri: i calcoli li collocano a circa il 3,75% dal bordo sinistro o destro dello schermo e circa il 13% dalla parte inferiore dello schermo. Ho basato queste misurazioni su un gioco commerciale con gamepad virtuali simili ma con gameplay diversi.

 classe VirtualGamepad: tSingleton pubblico public: enum State kCenter = 0x00, kTop = 0x01, kBottom = 0x02, kLeft = 0x04, kRight = 0x08, kTopLeft = 0x05, kTopRight = 0x09, kBottomLeft = 0x06, kBottomRight = 0x0a,; protetto: 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); aggiornamento void (const tTouchEvent & msg); classe di amici tSingleton; classe di amici Input; ; 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); 

Come precedentemente menzionato, mLeftStick e mRightStick sono maschere di bit e il loro uso è determinare dove disegnare il cerchio interno del gamepad. Calcoleremo la maschera di bit nel metodo VirtualGamepad :: UpdateBasedOnKeys ().

Questo metodo è chiamato immediatamente dopo Ingresso :: onTouch, in modo che possiamo leggere i membri dello stato "freschi" e sapere che sono aggiornati:

 void VirtualGamepad :: UpdateBasedOnKeys () Input * inp = Input :: getInstance (); mLeftStick = kCenter; if (inp-> mFreshKeyboardState [Input :: kA]) mLeftStick | = kLeft;  else if (inp-> mFreshKeyboardState [Input :: kD]) mLeftStick | = kRight;  if (inp-> mFreshKeyboardState [Input :: kW]) mLeftStick | = kTop;  else if (inp-> mFreshKeyboardState [Input :: kS]) mLeftStick | = kBottom;  mRightStick = kCenter; if (inp-> mFreshKeyboardState [Input :: kLeft]) mRightStick | = kLeft;  else if (inp-> mFreshKeyboardState [Input :: kRight]) mRightStick | = kRight;  if (inp-> mFreshKeyboardState [Input :: kUp]) mRightStick | = kTop;  else if (inp-> mFreshKeyboardState [Input :: kDown]) mRightStick | = kBottom; 

Per disegnare un gamepad individuale, chiamiamo VirtualGamepad :: DrawStickAtPoint (); questo metodo non sa né interessa quale gamepad stai disegnando, sa solo dove vuoi che sia disegnato e lo stato in cui disegnarlo. Perché abbiamo usato le maschere di bit e calcolato in anticipo, il nostro metodo diventa più piccolo e più facile da leggere:

 void VirtualGamepad :: DrawStickAtPoint (tSpriteBatch * spriteBatch, const tPoint2f & point, stato stato) tPoint2f offset = tPoint2f (18, 18); spriteBatch-> draw (10, Art :: getInstance () -> getVPadBottom (), point, tOptional()); switch (stato) case kCenter: offset + = tPoint2f (0, 0); rompere; case kTopLeft: offset + = tPoint2f (-13, -13); rompere; case kTop: offset + = tPoint2f (0, -18); rompere; case kTopRight: offset + = tPoint2f (13, -13); rompere; case kRight: offset + = tPoint2f (18, 0); rompere; case kBottomRight: offset + = tPoint2f (13, 13); rompere; caso kBottom: offset + = tPoint2f (0, 18); rompere; case kBottomLeft: offset + = tPoint2f (-13, 13); rompere; caso kLeft: offset + = tPoint2f (-18, 0); rompere;  spriteBatch-> draw (11, Art :: getInstance () -> getVPadTop (), punto + offset, tOptional()); 

Disegnare due gamepad diventa molto più semplice in quanto è solo una chiamata al metodo sopra due volte. Guardiamo VirtualGamepad :: draw ():

 void VirtualGamepad :: draw (tSpriteBatch * spriteBatch) DrawStickAtPoint (spriteBatch, mLeftPoint, (State) mLeftStick); DrawStickAtPoint (spriteBatch, mRightPoint, (State) mRightStick); 

Infine, dobbiamo effettivamente disegnare il gamepad virtuale, quindi GameRoot :: onRedrawView (), aggiungi la seguente riga:

 VirtualGamepad :: getInstance () -> draw (mSpriteBatch);

Questo è tutto. Se esegui il gioco ora, dovresti vedere i gamepad virtuali in pieno effetto. Quando tocchi il gamepad sinistro, dovresti muoverti. Quando tocchi il gamepad destro, la tua direzione di fuoco dovrebbe cambiare. In effetti, puoi usare entrambi i gamepad contemporaneamente, e persino muoverti usando il gamepad sinistro e toccare il gamepad di destra al di fuori del movimento del mouse. E quando lasci andare, ti fermi e (potenzialmente) smetti di sparare.

Riepilogo della tecnica del gamepad virtuale

Abbiamo completamente implementato il supporto del gamepad virtuale e funziona, ma potresti trovarlo un po 'goffo o difficile da usare. Perché è così? È qui che la vera sfida dei controlli basati su touch su iOS arriva con giochi tradizionali che inizialmente non erano stati progettati per loro.

Non sei solo, però. Molti giochi soffrono di questi problemi e li hanno superati.

Ecco alcune cose che ho osservato con l'input sullo schermo tattile; potresti avere alcune osservazioni simili tu stesso:

Innanzitutto, i controller di gioco hanno un aspetto diverso rispetto a un touchscreen piatto; sai dove si trova il tuo dito su un vero gamepad e come evitare che le dita scivolino via. Tuttavia, su un touchscreen, le dita potrebbero spostarsi leggermente troppo lontano dalla zona di tocco, quindi l'input potrebbe non essere riconosciuto correttamente e potresti non rendertene conto fino a che non sarà troppo tardi.

Secondo, potresti anche aver notato, giocando con i comandi touch, che la tua mano oscura la tua vista, quindi spedisci potresti essere colpito da un nemico sotto la tua mano che non vedevi per cominciare!

Infine, potresti scoprire che le aree tattili sono più facili da usare su un iPad piuttosto che su un iPhone o viceversa. Quindi abbiamo problemi con una dimensione dello schermo diversa che influisce sulla nostra "dimensione dell'area di input", che è sicuramente qualcosa che non sperimentiamo così tanto su un computer desktop. (La maggior parte delle tastiere e dei mouse hanno le stesse dimensioni e agiscono allo stesso modo o possono essere regolate).

Ecco alcune modifiche che potresti apportare al sistema di input descritto in questo articolo:

  • Disegna la posizione centrale del tuo gamepad dove inizia il tuo tocco; ciò consente alla mano del giocatore di spostarsi sempre leggermente senza impatto e significa che possono toccare qualsiasi punto dello schermo.
  • Rendi più piccola la tua "area giocabile" e sposta completamente il gamepad dall'area riproducibile. Ora le tue dita non ostacoleranno la tua vista.
  • Crea interfacce utente distinte e separate per iPhone e iPad. Ciò ti consentirà di modificare il progetto in base al tipo di dispositivo, ma richiede anche di testare diversi dispositivi.
  • Fai diventare i nemici o il giocatore leggermente più lento. Questo potenzialmente consente all'utente di sperimentare il gioco più facilmente, ma anche potenzialmente rende il gioco più facile da vincere.
  • Elimina completamente i gamepad virtuali e utilizza un altro schema. Sei al comando, dopo tutto!

Di nuovo, sta a te decidere cosa vuoi fare e come vuoi farlo. Tra gli aspetti positivi, ci sono molti modi per fare input tattili. La parte difficile è farlo bene e rendere felici i tuoi giocatori.

Buchi neri

Uno dei nemici più interessanti in Geometry Wars è il buco nero. Esaminiamo come possiamo creare qualcosa di simile in Shape Blaster. Creeremo ora le funzionalità di base e rivisiteremo il nemico nel prossimo tutorial per aggiungere effetti particellari e interazioni tra particelle.

Un buco nero con particelle orbitanti

Funzionalità di base

I buchi neri attireranno le particelle del giocatore, i nemici vicini e (dopo il prossimo tutorial), ma respingeranno i proiettili.

Ci sono molte funzioni possibili che possiamo usare per attrazione o repulsione. Il più semplice è usare la forza costante in modo che il buco nero tiri con la stessa forza indipendentemente dalla distanza dell'oggetto. Un'altra possibilità è di aumentare linearmente la forza, da zero a una certa distanza massima, a piena forza per gli oggetti direttamente sopra il buco nero. Se vogliamo modellare la gravità in modo più realistico, possiamo usare il quadrato inverso della distanza, il che significa che la forza di gravità è proporzionale a 1 / (distanza ^ 2).

In realtà utilizzeremo ognuna di queste tre funzioni per gestire oggetti diversi. I proiettili saranno respinti con una forza costante; i nemici e la nave del giocatore saranno attratti da una forza lineare; e le particelle useranno una funzione inversa quadrata.

Faremo una nuova classe per i buchi neri. Iniziamo con la funzionalità di base:

 classe BlackHole: public Entity protected: int mHitPoints; pubblico: BlackHole (const tVector2f e position); void update (); void draw (tSpriteBatch * spriteBatch); void wasShot (); void kill (); ; BlackHole :: BlackHole (const tVector2f e position): mHitPoints (10) mImage = Art :: getInstance () -> getBlackHole (); mPosition = posizione; mRadius = mImage-> getSurfaceSize (). width / 2.0f; mKind = kBlackHole;  void BlackHole :: wasShot () mHitPoints--; se (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 (scala)); 

I buchi neri prendono dieci colpi per uccidere. Regoliamo leggermente la scala dello sprite per farlo pulsare. Se decidi che distruggere buchi neri dovrebbe anche concedere punti, devi fare aggiustamenti simili al Buco nero classe come abbiamo fatto con il Nemico classe.

Successivamente, faremo in modo che i buchi neri applichino effettivamente una forza su altre entità. Avremo bisogno di un piccolo metodo di supporto dal nostro EntityManager:

 std :: list EntityManager :: getNearbyEntities (const tPoint2f e pos, float radius) std :: list risultato; per (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; 

Questo metodo potrebbe essere reso più efficiente utilizzando uno schema di partizionamento spaziale più complicato, ma per il numero di entità che avremo, va bene così com'è.

Ora possiamo fare in modo che i buchi neri applichino forza nelle loro BlackHole :: update () metodo:

 void BlackHole :: update () std :: list entities = EntityManager :: getInstance () -> getNearbyEntities (mPosition, 250); per (std :: list:: iterator iter = entities.begin (); iter! = entities.end (); iter ++) if ((* iter) -> getKind () == kEnemy &&! ((Enemy *) (* iter)) -> getIsActive ()) // Non fare nulla else if ((* iter) -> getKind () == kBullet) tVector2f temp = ((* iter) -> getPosition () - mPosition); (* iter) -> setVelocity ((* iter) -> getVelocity () + temp.normalize () * 0.3f);  else tVector2f dPos = mPosition - (* iter) -> getPosition (); float length = dPos.length (); (* iter) -> setVelocity ((* iter) -> getVelocity () + dPos.normalize () * tMath :: mix (2.0f, 0.0f, length / 250.0f)); 

I buchi neri influenzano solo le entità all'interno di un raggio scelto (250 pixel). I proiettili all'interno di questo raggio hanno una forza repulsiva costante applicata, mentre tutto il resto ha una forza di attrazione lineare applicata.

Avremo bisogno di aggiungere la gestione delle collisioni per i buchi neri al EntityManager. Aggiungi un std :: list per buchi neri come abbiamo fatto per gli altri tipi di entità e aggiungere il seguente codice in EntityManager :: handleCollisions ():

 // gestire le collisioni con buchi neri per (std :: list:: iterator i = mBlackHoles.begin (); i! = mBlackHoles.end (); i ++) for (std :: list:: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) if ((* j) -> getIsActive () && isColliding (* i, * j)) (* j) -> wasShot ();  per (std :: elenco:: iterator j = mBullets.begin (); j! = mBullets.end (); j ++) if (isColliding (* i, * j)) (* j) -> setExpired (); (* I) -> wasShot ();  if (isColliding (PlayerShip :: getInstance (), * i)) KillPlayer (); rompere; 

Infine, apri il EnemySpawner classe e fargli creare dei buchi neri. Ho limitato il numero massimo di buchi neri a due, e ho dato una possibilità su 600 di un buco nero che generava ogni fotogramma.

 if (EntityManager :: getInstance () -> getBlackHoleCount () < 2 && int32_t(tMath::random() * mInverseBlackHoleChance) == 0)  EntityManager::getInstance()->aggiungi (nuovo BlackHole (GetSpawnPosition ())); 

Conclusione

Abbiamo discusso e aggiunto gamepad virtuali e aggiunto buchi neri utilizzando varie formule di forza. Shape Blaster sta iniziando a sembrare abbastanza buono. Nella parte successiva, aggiungeremo alcuni effetti particellari pazzi e esagerati.

Riferimenti

  • Credito fotografico: controller Wii di kazuma jp.