Creare un gioco di asteroidi semplice usando le entità basate sui componenti

Nel precedente tutorial, abbiamo creato un sistema Entity basato su componenti bare-bone. Ora useremo questo sistema per creare un semplice gioco Asteroids.


Anteprima del risultato finale

Ecco il semplice gioco Asteroids che creeremo in questo tutorial. È scritto usando Flash e AS3, ma i concetti generali si applicano alla maggior parte delle lingue.

Il codice sorgente completo è disponibile su GitHub.


Panoramica della classe

Ci sono sei classi:

  • AsteroidsGame, che estende la classe di gioco di base e aggiunge la logica specifica al nostro sparatutto spaziale.
  • Nave, qual è la cosa che controlli.
  • Asteroide, qual è la cosa a cui spari.
  • proiettile, qual è la cosa che spari.
  • Pistola, che crea quei proiettili.
  • EnemyShip, che è un alieno errante che è lì solo per aggiungere un po 'di varietà al gioco.
  • Passiamo attraverso questi tipi di entità uno per uno.


    Il Nave Classe

    Inizieremo con la nave del giocatore:

 pacchetto asteroids import com.iainlobb.gamepad.Gamepad; import com.iainlobb.gamepad.KeyCode; motore di importazione. import engine.Entity; motore di importazione.Gioco; motore di importazione. motore di importazione. Fisica; motore di importazione.Visualizza; import flash.display.GraphicsPathWinding; import flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / public class Ship estende Entity protected var gamepad: Gamepad; funzione pubblica Ship () body = new Body (this); body.x = 400; body.y = 300; fisica = nuova fisica (questo); physics.drag = 0.9; view = new View (this); view.sprite = new Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawPath (Vector.([1, 2, 2, 2, 2, 2, 2, 2, 2, 2]), Vettore.([-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); salute = nuova salute (questo); health.hits = 5; health.died.add (onDied); arma = nuova pistola (questo); gamepad = new Gamepad (Game.stage, false); gamepad.fire1.mapKey (KeyCode.SPACEBAR);  override public function update (): void super.update (); body.angle + = gamepad.x * 0.1; physics.thrust (-gamepad.y); if (gamepad.fire1.isPressed) weapon.fire ();  funzione protetta onDied (entità: Entity): void destroy (); 

C'è un bel po 'di dettagli di implementazione qui, ma la cosa principale da notare è che nel costruttore si istanzia e si configura Corpo, Fisica, Salute, vista e Arma componenti. (Il Arma componente è in realtà un'istanza di Pistola piuttosto che la classe base di armi.)

Sto usando le API di disegno della grafica Flash per creare la mia nave (righe 29-32), ma potremmo altrettanto facilmente usare un'immagine bitmap. Sto anche creando un'istanza della mia classe Gamepad - questa è una libreria open source che ho scritto un paio di anni fa per facilitare l'input da tastiera in Flash.

Ho anche scavalcato il aggiornare funzione dalla classe base per aggiungere un comportamento personalizzato: dopo aver attivato tutto il comportamento predefinito con super.update () ruotiamo e spingiamo la nave in base all'input della tastiera, e spariamo con l'arma se viene premuto il tasto di fuoco.

Ascoltando il morto Segnale del componente di salute, attiviamo il onDied funzione se il giocatore esaurisce i punti ferita. Quando ciò accade, diciamo semplicemente alla nave di distruggere se stessa.


Il Pistola Classe

Avanti, accendiamolo Pistola classe:

 pacchetto asteroids import engine.Entity; motore di importazione. Arma; / ** * ... * @author Iain Lobb - [email protected] * / public class Gun extends Weapon public function Gun (entity: Entity) super (entità);  override public function fire (): void var bullet: Bullet = new 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 (proiettile); super.fire (); 

Questo è un bel corto! Abbiamo appena scavalcato il fuoco() funzione per creare un nuovo proiettile ogni volta che il giocatore spara. Dopo aver abbinato la posizione e la rotazione del proiettile alla nave, e averla spinta nella direzione giusta, la spediamo entityCreated in modo che possa essere aggiunto al gioco.

Una grande cosa su questo Pistola la classe è che è usata sia dal giocatore che dalle navi nemiche.


Il proiettile Classe

UN Pistola crea un'istanza di questo proiettile classe:

 pacchetto asteroids import engine.Body; import engine.Entity; motore di importazione. Fisica; motore di importazione.Visualizza; import flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / public class Bullet estende Entity public var age: int; funzione pubblica Bullet () body = new Body (this); body.radius = 5; fisica = nuova fisica (questo); view = new View (this); view.sprite = new Sprite (); view.sprite.graphics.beginFill (0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius);  override public function update (): void super.update (); per ogni (var target: Entity in target) if (body.testCollision (target)) target.health.hit (1); distruggere(); ritorno;  età ++; se (età> 20) view.alpha - = 0,2; se (età> 25) distruggi (); 

Il costruttore istanzia e configura il corpo, la fisica e la vista. Nella funzione di aggiornamento, ora puoi vedere la lista chiamata obiettivi vieni utile, mentre passiamo in rassegna tutte le cose che vogliamo colpire e vediamo se qualcuno di loro sta intersecando il proiettile.

Questo sistema di collisione non si ridimensiona a migliaia di proiettili, ma va bene per la maggior parte dei giochi casuali.

Se il proiettile ha più di 20 fotogrammi, iniziamo a sbiadirlo, e se è più vecchio di 25 fotogrammi lo distruggiamo. Come con il Pistola, il proiettile è usato sia dal giocatore che dal nemico - le istanze hanno solo una diversa lista di bersagli.

A proposito di ...


Il EnemyShip Classe

Ora diamo un'occhiata a quella nave nemica:

 pacchetto asteroids import engine.Body; import engine.Entity; motore di importazione. motore di importazione. Fisica; motore di importazione.Visualizza; import flash.display.GraphicsPathWinding; import flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / public class EnemyShip estende Entity protected var turnDirection: Number = 1; funzione pubblica EnemyShip () body = new Body (this); body.x = 750; body.y = 550; fisica = nuova fisica (questo); physics.drag = 0.9; view = new View (this); view.sprite = new Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawPath (Vector.([1, 2, 2, 2]), Vettore.([0, 10, 10, -10, 0, 0, -10, -10, 0, 10]), GraphicsPathWinding.NON_ZERO); salute = nuova salute (questo); health.hits = 5; health.died.add (onDied); arma = nuova pistola (questo);  override public function update (): void super.update (); if (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();   

Come puoi vedere, è abbastanza simile alla classe di nave del giocatore. L'unica vera differenza è che nel aggiornare() funzione, piuttosto che avere il controllo del giocatore tramite la tastiera, abbiamo qualche "stupidità artificiale" per far vagare la nave e sparare a caso.


Il Asteroide Classe

L'altro tipo di entità a cui il giocatore può sparare è l'asteroide stesso:

 pacchetto asteroids import engine.Body; import engine.Entity; motore di importazione. motore di importazione. Fisica; motore di importazione.Visualizza; import flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / public class Asteroid estende Entity public function Asteroid () body = new Body (this); body.radius = 20; body.x = Math.random () * 800; body.y = Math.random () * 600; fisica = nuova fisica (questo); physics.velocityX = (Math.random () * 10) - 5; physics.velocityY = (Math.random () * 10) - 5; view = new View (this); view.sprite = new Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius); salute = nuova salute (questo); health.hits = 3; health.hurt.add (onHurt);  override public function update (): void super.update (); per ogni (var target: Entity in target) if (body.testCollision (target)) target.health.hit (1); distruggere(); ritorno;  funzione protetta onHurt (entity: Entity): void body.radius * = 0.75; view.scale * = 0,75; if (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);   

Si spera che ti stia abituando a come appaiono queste classi di entità.

Nel costruttore inizializziamo i nostri componenti e randomizziamo la posizione e la velocità.

Nel aggiornare() funzione controlliamo le collisioni con la nostra lista dei bersagli - che in questo esempio avrà solo un singolo oggetto - la nave del giocatore. Se troviamo una collisione facciamo danni al bersaglio e poi distruggiamo l'asteroide. D'altra parte, se l'asteroide è esso stesso danneggiato (cioè colpito da un proiettile giocatore), lo riduciamo e creiamo un secondo asteroide, creando l'illusione che sia stato fatto saltare in due pezzi. Sappiamo quando farlo ascoltando il segnale "ferito" del componente Salute.


Il AsteroidsGame Classe

Infine, diamo un'occhiata alla classe AsteroidsGame che controlla l'intero spettacolo:

 pacchetto asteroids import engine.Entity; motore di importazione.Gioco; import flash.events.MouseEvent; import flash.filters.GlowFilter; import flash.text.TextField; / ** * ... * @author Iain Lobb - [email protected] * / public class AsteroidsGame estende Game public var players: Vector. = nuovo vettore.(); nemici pubblici: Vector. = nuovo vettore.(); public var messageField: TextField; funzione pubblica AsteroidsGame ()  sostituisce la funzione protetta startGame (): void var asteroid: Asteroid; per (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; se (entity.body.x < -50) entity.body.x += 900; if (entity.body.y > 650) entity.body.y - = 700; se (entity.body.y < -50) entity.body.y += 700;  if (enemies.length == 0) gameOver();   

Questa classe è piuttosto lunga (beh, più di 100 righe!) Perché fa un sacco di cose.

Nel inizia il gioco() crea e configura 10 asteroidi, la nave e la nave nemica e crea anche il messaggio "CLICK TO START".

Il inizio() la funzione scompatta il gioco e rimuove il messaggio, mentre il gioco finito la funzione mette nuovamente in pausa il gioco e ripristina il messaggio. Il ricomincia() la funzione ascolta un clic del mouse sulla schermata Game Over - quando questo accade, interrompe il gioco e lo riavvia.

Il aggiornare() la funzione attraversa tutti i nemici e deforma tutti quelli che si sono allontanati dallo schermo, oltre a controllare la condizione di vittoria, ovvero che non ci sono nemici rimasti nella lista dei nemici.


Prenderlo ulteriormente

Questo è un motore piuttosto semplice e un gioco semplice, quindi ora pensiamo a come possiamo espanderlo.

  • Potremmo aggiungere un valore di priorità per ogni entità e ordinare l'elenco prima di ogni aggiornamento, in modo che possiamo assicurarci che alcuni tipi di Entità si aggiornino sempre dopo altri tipi.
  • Potremmo utilizzare il pool di oggetti in modo da riutilizzare gli oggetti morti (ad esempio i punti elenco), creando invece solo centinaia di nuovi.
  • Potremmo aggiungere un sistema di telecamere in modo da poter scorrere e ingrandire la scena. Potremmo estendere i componenti Body e Physics per aggiungere supporto per Box2D o un altro motore fisico.
  • Potremmo creare un componente di inventario, in modo che le entità possano trasportare oggetti.

Oltre all'estensione dei singoli componenti, a volte potrebbe essere necessario estendere il IEntity interfaccia per creare tipi speciali di Entità con componenti specializzati.

Ad esempio, se stiamo realizzando un gioco platform, e abbiamo un nuovo componente che gestisce tutte le cose molto specifiche di cui un personaggio di un gioco di piattaforma ha bisogno - sono a terra, toccano un muro, per quanto tempo sono stati nell'aria, possono fare il doppio salto, ecc. - anche altre entità potrebbero aver bisogno di accedere a queste informazioni. Ma non fa parte dell'API principale di Entity, che è mantenuta intenzionalmente molto generale. Quindi abbiamo bisogno di definire una nuova interfaccia, che fornisce l'accesso a tutti i componenti di entità standard, ma aggiunge l'accesso a PlatformController componente.

Per questo, faremmo qualcosa come:

 pacchetto platformgame import engine.IEntity; / ** * ... * @author Iain Lobb - [email protected] * / interfaccia pubblica IPlatformEntity estende IEntity function set platformController (value: PlatformController): void; function get platformController (): PlatformController; 

Qualsiasi entità che abbia bisogno di funzionalità "platforming" implementa quindi questa interfaccia, consentendo ad altre entità di interagire con il PlatformController componente.


conclusioni

Se addirittura osassi scrivere sull'architettura del gioco, temo di suscitare l'interesse dei calabroni - ma è (soprattutto) sempre una buona cosa, e spero che almeno ti abbia fatto pensare a come organizzi il tuo codice.

In fin dei conti, non credo che dovresti essere troppo attaccato a come strutturi le cose; qualunque cosa funzioni per te per ottenere il tuo gioco fatto è la migliore strategia. So che ci sono sistemi molto più avanzati di quello che ho delineato qui, che risolvono una serie di problemi oltre a quelli che ho discusso, ma possono tendere a sembrare molto poco familiari se si è abituati a un'architettura basata sull'ereditarietà tradizionale.

Mi piace l'approccio che ho suggerito qui perché consente di organizzare il codice in base allo scopo, in classi focalizzate di piccole dimensioni, fornendo al contempo un'interfaccia estendibile tipicamente e staticamente e senza fare affidamento su funzionalità del linguaggio dinamico o Stringa le ricerche. Se si desidera modificare il comportamento di un particolare componente, è possibile estendere tale componente e sovrascrivere i metodi che si desidera modificare. Le lezioni tendono a rimanere molto brevi, quindi non mi trovo mai a scorrere migliaia di righe per trovare il codice che sto cercando.

Meglio di tutti, sono in grado di avere un singolo motore abbastanza flessibile da poter essere utilizzato in tutti i giochi che faccio, risparmiandomi un'enorme quantità di tempo.