Creați un joc simplu de asteroizi utilizând entități bazate pe componente

În tutorialul anterior, am creat un sistem Entity bazat pe componente de oase goale. Acum vom folosi acest sistem pentru a crea un simplu joc Asteroids.


Rezultatul final al rezultatelor

Iată jocul simplu de asteroizi pe care îl vom crea în acest tutorial. Este scrisă utilizând Flash și AS3, însă conceptele generale se aplică în majoritatea limbilor.

Codul sursă complet este disponibil pe GitHub.


Prezentarea clasei

Există șase clase:

  • AsteroidsGame, care extinde clasa jocului de bază și adaugă logica specifică spațiului nostru de filmare.
  • Navă, care este lucrul pe care îl controlezi.
  • Asteroid, care este lucrul pe care îl trageți.
  • Glonţ, care este chestia pe care o trageți.
  • armă, care creează acele gloanțe.
  • EnemyShip, care este un străin rătăcitor, care este doar acolo pentru a adăuga un pic de varietate la joc.
  • Să trecem prin aceste tipuri de entități unul câte unul.


    Navă Clasă

    Vom începe cu nava jucătorului:

 pachet asteroizi import com.iainlobb.gamepad.Gamepad; import com.iainlobb.gamepad.KeyCode; import engine.Body; import motor.Entity; import engine.Game; import engine.Health; import engine.Physics; import engine.View; import flash.display.GraphicsPathWinding; import flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / navă de clasă publică extinde Entitatea protected var gamepad: Gamepad; funcție publică () body = body nou (aceasta); body.x = 400; body.y = 300; fizica = noua Fizica (aceasta); fizics.drag = 0,9; view = new Vezi (aceasta); view.sprite = nou Sprite (); view.sprite.graphics.lineStyle (1,5, 0xFFFFFF); view.sprite.graphics.drawPath (Vector.([1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]), Vector.([-7,3, 10,3, -5,5, 10,3, -7, 0,6, -0,5, -2,8, 6,2, 0,3, 4,5, 10,3, 6,3, 10,3, 11,1, -1,4, -0,2, -9,6, -11,9, - 1.3, -7.3, 10.3]), GraphicsPathWinding.NON_ZERO); sănătate = sănătate nouă (acest lucru); health.hits = 5; health.died.add (onDied); arma = pistol nou (acest lucru); gamepad = Gamepad nou (Game.stage, false); gamepad.fire1.mapKey (KeyCode.SPACEBAR);  suprascrie actualizarea funcției publice (): void super.update (); body.angle + = gamepad.x * 0.1; physics.thrust (-gamepad.y); dacă (gamepad.fire1.isPressed) weapon.fire ();  funcția protejată onDied (entitate: entitate): void destroy (); 

Există destul de multe detalii de implementare aici, dar cel mai important lucru pe care trebuie să-l observați este că în constructor se instanțiază și se configurează Corp, Fizică, Sănătate, Vedere și Armă componente. (The Armă componenta este, de fapt, un exemplu de armă mai degrabă decât clasa de bază a armelor.)

Folosesc API-urile de grafică Flash pentru a crea nava mea (linii 29-32), dar am putea folosi la fel de ușor o imagine bitmap. De asemenea, creez o instanță a clasei Gamepad - aceasta este o bibliotecă open source pe care am scris-o acum câțiva ani pentru a facilita intrarea tastaturii în Flash.

De asemenea, am răsturnat Actualizați funcția din clasa de bază pentru a adăuga un anumit comportament personalizat: după declanșarea întregului comportament implicit cu super.update () rotim și împingem nava pe baza intrării de la tastatură și tragem arma dacă apasă tasta de incendiu.

Prin ascultarea mesajului decedat Semnalul componentei de sănătate, declanșăm onDied dacă jucătorul nu mai are puncte de lovit. Când se întâmplă acest lucru, îi spunem navei să se distrugă.


armă Clasă

Înainte să lămurem asta armă clasă:

 pachet asteroizi import engine.Entity; import engine.Weapon; / ** * ... * @author Iain Lobb - [email protected] * / clasa publica Gun extinde Weapon functie publica Gun (entitate: Entitate) super (entitate);  suprascrie funcția publică (): void var bullet: Bullet = nou Bullet (); bullet.targets = entity.targets; bullet.body.x = entity.body.x; bullet.body.y = entity.body.y; bullet.body.angle = entity.body.angle; bullet.physics.thrust (10); entity.entityCreated.dispatch (glonț); super.fire (); 

Acesta este unul frumos! Tocmai am suprascrie foc() funcția de a crea un nou Glonţ ori de câte ori playerul se declanșează. După potrivirea poziției și rotirii glonțului cu nava și împingerea acesteia în direcția corectă, vom expedia entityCreated astfel încât acesta să poată fi adăugat la joc.

Un lucru minunat despre asta armă clasa este că este folosit atât de jucător, cât și de navele inamice.


Glonţ Clasă

A armă creează un exemplu de acest lucru Glonţ clasă:

 asteroizii de pachete import engine.Body; import motor.Entity; import engine.Physics; import engine.View; import flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / clasa publica Bullet extinde Entitatea public var age: int; funcția publică Bullet () body = new Body (aceasta); body.radius = 5; fizica = noua Fizica (aceasta); view = new Vezi (aceasta); view.sprite = nou Sprite (); view.sprite.graphics.beginFill (0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius);  suprascrie actualizarea funcției publice (): void super.update (); pentru fiecare (var target: Entitate în ținte) if (body.testCollision (target)) target.health.hit (1); distruge(); întoarcere;  vârsta ++; dacă (vârsta> 20) view.alpha - = 0.2; dacă (vârsta> 25 ani) distrug (); 

Constructorul instantează și configurează corpul, fizica și vederea. În funcția de actualizare, puteți vedea acum lista numită obiective veniți la îndemână, pe măsură ce treceți prin toate lucrurile pe care vrem să le loviți și să vedem dacă unul dintre ele se intersectează cu gloanțele.

Acest sistem de coliziune nu s-ar potrivi la mii de gloanțe, dar este bine pentru majoritatea jocurilor casual.

Dacă glonțul are vechime de peste 20 de cadre, începem să-l distrugem și, dacă e mai vechi de 25 de cadre, îl distrugem. Ca și în cazul armă, Glonţ este folosit atât de către jucător, cât și de inamic - instanțele au doar o listă de ținte diferite.

Vorbind despre care ...


EnemyShip Clasă

Acum să ne uităm la acel vas inamic:

 asteroizii de pachete import engine.Body; import motor.Entity; import engine.Health; import engine.Physics; import engine.View; import flash.display.GraphicsPathWinding; import flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / clasa publica EnemyShip extinde Entitatea protected var turnDirection: Number = 1; funcția publică EnemyShip () body = corp nou (acesta); body.x = 750; body.y = 550; fizica = noua Fizica (aceasta); fizics.drag = 0,9; view = new Vezi (aceasta); view.sprite = nou Sprite (); view.sprite.graphics.lineStyle (1,5, 0xFFFFFF); view.sprite.graphics.drawPath (Vector.([1, 2, 2, 2, 2]), Vector.([0, 10, 10, -10, 0, 0, -10, -10, 0, 10]), GraphicsPathWinding.NON_ZERO); sănătate = sănătate nouă (acest lucru); health.hits = 5; health.died.add (onDied); arma = pistol nou (acest lucru);  suprascrie actualizarea funcției publice (): void super.update (); dacă (Math.random () < 0.1) turnDirection = -turnDirection; body.angle += turnDirection * 0.1; physics.thrust(Math.random()); if (Math.random() < 0.05) weapon.fire();  protected function onDied(entity:Entity):void  destroy();   

După cum puteți vedea, este destul de similar cu clasa navei de jucători. Singura diferență reală este că în Actualizați() funcția, mai degrabă decât să avem controlul jucătorului prin tastatură, avem o "prostie artificială" pentru ca nava să rătăcească și să tragă la întâmplare.


Asteroid Clasă

Celălalt tip de entitate pe care se poate trage jucătorul este asteroidul însuși:

 asteroizii de pachete import engine.Body; import motor.Entity; import engine.Health; import engine.Physics; import engine.View; import flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / clasa publica Asteroid extinde Entitatea functie publica Asteroid () body = corp nou (aceasta); body.radius = 20; body.x = Math.random () * 800; body.y = Math.random () * 600; fizica = noua Fizica (aceasta); fizics.velocityX = (Math.random () * 10) - 5; fizics.velocityY = (Math.random () * 10) - 5; view = new Vezi (aceasta); view.sprite = nou Sprite (); view.sprite.graphics.lineStyle (1,5, 0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius); sănătate = sănătate nouă (acest lucru); health.hits = 3; health.hurt.add (onHurt);  suprascrie actualizarea funcției publice (): void super.update (); pentru fiecare (var target: Entitate în ținte) if (body.testCollision (target)) target.health.hit (1); distruge(); întoarcere;  funcție protejată onHurt (entitate: Entitate): void body.radius * = 0.75; view.scale * = 0.75; dacă (body.radius < 10)  destroy(); return;  var asteroid:Asteroid = new Asteroid(); asteroid.targets = targets; group.push(asteroid); asteroid.group = group; asteroid.body.x = body.x; asteroid.body.y = body.y; asteroid.body.radius = body.radius; asteroid.view.scale = view.scale; entityCreated.dispatch(asteroid);   

Sperăm că vă obișnuiți cu modul în care arată aceste clase de entități până acum.

În constructor inițializăm componentele noastre și ne alegăm poziția și viteza.

În Actualizați() funcția pe care o verificăm pentru coliziuni cu lista țintă - care în acest exemplu va avea doar un singur element - nava jucătorului. Dacă găsim o coliziune, facem daune țintă și apoi distrugem asteroidul. Pe de altă parte, dacă asteroidul este el însuși deteriorat (adică este lovit de un glonț al jucătorului), îl micșorăm și creăm un al doilea asteroid, creând iluzia că a fost distrusă în două bucăți. Știm când să facem acest lucru ascultând semnalul "rănit" al componentei Sănătate.


AsteroidsGame Clasă

În cele din urmă, să examinăm clasa AsteroidsGame care controlează întregul spectacol:

 pachet asteroizi import engine.Entity; import engine.Game; importul flash.events.MouseEvent; import flash.filters.GlowFilter; import flash.text.TextField; / ** * ... * @author Iain Lobb - [email protected] * / clasa publica AsteroidsGame se extinde jocul public var players: Vector. = Vector nou.(); vrăjitori publici var: Vector. = Vector nou.(); public var mesajField: TextField; funcția publică AsteroidsGame ()  suprascrie funcția protejată startGame (): void var asteroid: Asteroid; pentru (var i: int = 0; i < 10; i++)  asteroid = new Asteroid(); asteroid.targets = players; asteroid.group = enemies; enemies.push(asteroid); addEntity(asteroid);  var ship:Ship = new Ship(); ship.targets = enemies; ship.destroyed.add(onPlayerDestroyed); players.push(ship); addEntity(ship); var enemyShip:EnemyShip = new EnemyShip(); enemyShip.targets = players; enemyShip.group = enemies; enemies.push(enemyShip); addEntity(enemyShip); filters = [new GlowFilter(0xFFFFFF, 0.8, 6, 6, 1)]; update(); render(); isPaused = true; if (messageField)  addChild(messageField);  else  createMessage();  stage.addEventListener(MouseEvent.MOUSE_DOWN, start);  protected function createMessage():void  messageField = new TextField(); messageField.selectable = false; messageField.textColor = 0xFFFFFF; messageField.width = 600; messageField.scaleX = 2; messageField.scaleY = 3; messageField.text = "CLICK TO START"; messageField.x = 400 - messageField.textWidth; messageField.y = 240; addChild(messageField);  protected function start(event:MouseEvent):void  stage.removeEventListener(MouseEvent.MOUSE_DOWN, start); isPaused = false; removeChild(messageField); stage.focus = stage;  protected function onPlayerDestroyed(entity:Entity):void  gameOver();  protected function gameOver():void  addChild(messageField); isPaused = true; stage.addEventListener(MouseEvent.MOUSE_DOWN, restart);  protected function restart(event:MouseEvent):void  stopGame(); startGame(); stage.removeEventListener(MouseEvent.MOUSE_DOWN, restart); isPaused = false; removeChild(messageField); stage.focus = stage;  override protected function stopGame():void  super.stopGame(); players.length = 0; enemies.length = 0;  override protected function update():void  super.update(); for each (var entity:Entity in entities)  if (entity.body.x > 850) entity.body.x = 900; dacă (entity.body.x < -50) entity.body.x += 900; if (entity.body.y > 650) entity.body.y = 700; dacă (entity.body.y < -50) entity.body.y += 700;  if (enemies.length == 0) gameOver();   

Această clasă este destul de lungă (bine, mai mult de 100 de linii!), Deoarece face multe lucruri.

În incepe jocul() creează și configurează 10 asteroizi, nava și nava inamicului și creează și mesajul "CLICK TO START".

start() funcția întrerupe jocul și elimină mesajul, în timp ce joc încheiat Funcția întrerupe jocul din nou și restabilește mesajul. repornire() funcția ascultă pentru un click de mouse pe ecranul Joc peste - când se întâmplă acest lucru oprește jocul și îl pornește din nou.

Actualizați() funcționează prin toate inamicii și răstoarnă pe toți cei care s-au scufundat de pe ecran, precum și pe verificarea condiției de câștig, și anume că nu există dușmani rămași în lista dușmanilor.


Luând-o mai departe

Acesta este un motor destul de gol oase și un joc simplu, așa că acum să ne gândim la modalități prin care am putea să-l extindem.

  • Am putea adăuga o valoare de prioritate pentru fiecare entitate și sortați lista înainte de fiecare actualizare, astfel încât să putem asigura că anumite tipuri de entități se actualizează întotdeauna după alte tipuri.
  • Am putea folosi gruparea de obiecte pentru a reutiliza obiecte moarte (de exemplu, gloanțe) în loc să creăm sute de noi.
  • Am putea adăuga un sistem de cameră pentru a putea derula și mări scena. Am putea extinde componentele corporale și fizice pentru a adăuga suport pentru Box2D sau un alt motor de fizică.
  • Am putea crea o componentă de inventar, astfel încât entitățile să poată purta obiecte.

Pe lângă extinderea componentelor individuale, am putea uneori să extindem IEntity interfață pentru a crea tipuri speciale de entități cu componente specializate.

De exemplu, dacă facem un joc pe platformă și avem o componentă nouă care se ocupă de toate lucrurile foarte specifice pe care un personaj de joc de platformă are nevoie - sunt pe teren, ating un perete, cât timp au fost în aer, pot să sară dublu etc. - și alte entități ar putea avea nevoie să acceseze aceste informații. Dar nu face parte din API-ul Entității de bază, care este păstrat în mod intenționat foarte general. Așadar, trebuie să definim o nouă interfață, care oferă acces la toate componentele entității standard, dar adaugă acces la PlatformController component.

Pentru aceasta, am face ceva de genul:

 pachet platformgame import engine.IEntity; / ** * ... * @author Iain Lobb - [email protected] * / interfață publică IPlatformEntity extinde IEntity set funcțional platformController (valoare: PlatformController): void; funcția get platformController (): PlatformController; 

Orice entitate care are nevoie de funcționalitate "platforming" pune în aplicare această interfață, permițând altor entități să interacționeze cu PlatformController component.


concluzii

Chiar dacă mă îndrăznesc să scriu despre arhitectura jocurilor, mă tem că mă amestec într-un cuib de opinie - dar asta e (cel mai adesea) întotdeauna un lucru bun și sper că cel puțin te-am făcut să te gândești cum te organizezi cod.

În cele din urmă, nu cred că ar trebui să fiți prea agățat de modul în care structurați lucrurile; indiferent de ce ai de făcut pentru a-ți face jocul este cea mai bună strategie. Știu că există sisteme mult mai avansate decât cea pe care o prezint aici, care rezolvă o serie de probleme dincolo de cele pe care le-am discutat, dar ele pot avea tendința de a începe să se uite foarte necunoscute dacă sunteți obișnuiți cu o arhitectură tradițională bazată pe moștenire.

Îmi place abordarea pe care am sugerat-o aici, deoarece permite codul să fie organizat în funcție de scop, în clase mici concentrate, oferind în același timp o interfață extensibilă tipărite static și fără a se baza pe caracteristici dinamice ale limbajului Şir . lookups Dacă doriți să modificați comportamentul unei anumite componente, puteți să extindeți componenta respectivă și să înlocuiți metodele pe care doriți să le modificați. Clasele tind să rămână foarte scurte, așa că niciodată nu mă trezesc prin mii de linii pentru a găsi codul pe care îl caut.

Cel mai bun lucru este că pot să am un singur motor care să fie suficient de flexibil pentru a fi utilizat în toate jocurile pe care le fac, economisind o mare cantitate de timp.