Lag et enkelt asteroider spill ved hjelp av komponentbaserte enheter

I den forrige veiledningen opprettet vi et bare-ben-komponentbasert Entity-system. Nå skal vi bruke dette systemet til å lage et enkelt Asteroids-spill.


Endelig resultatforhåndsvisning

Her er det enkle Asteroids spillet vi skal skape i denne opplæringen. Det er skrevet med Flash og AS3, men de generelle begrepene gjelder for de fleste språk.

Full kildekoden er tilgjengelig på GitHub.


Klasseoversikt

Det er seks klasser:

  • AsteroidsGame, som utvider basen spill klassen og legger til logikken spesifikk for vår space shoot-'em-up.
  • Skip, som er tingen du kontrollerer.
  • Asteroid, som er tingen du skyter på.
  • Kule, som er den tingen du brenner.
  • Våpen, som skaper disse kulene.
  • EnemyShip, som er en vandrende fremmed som bare er der for å legge litt variasjon i spillet.
  • La oss gå gjennom disse enhetstyper en etter en.


    De Skip Klasse

    Vi starter med spillerens skip:

 pakke asteroider import com.iainlobb.gamepad.Gamepad; importer com.iainlobb.gamepad.KeyCode; import engine.Body; import engine.Entity; importere engine.Game; importere motor. Helse; import engine.Physics; import engine.View; importer flash.display.GraphicsPathWinding; importer flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / offentlig klasse Skip utvider Entity protected var gamepad: Gamepad; Offentlig funksjon Ship () body = new Body (this); body.x = 400; body.y = 300; fysikk = ny fysikk (dette); fysikk.drag = 0,9; view = new View (dette); 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]), vektor.([-7,3, 10,3, -5,5, 10,3, -7,6,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); helse = ny helse (dette); health.hits = 5; health.died.add (onDied); våpen = ny pistol (dette); gamepad = ny Gamepad (Game.stage, false); gamepad.fire1.mapKey (KeyCode.SPACEBAR);  overstyre offentlig funksjon oppdatering (): void super.update (); body.angle + = gamepad.x * 0,1; physics.thrust (-gamepad.y); hvis (gamepad.fire1.isPressed) weapon.fire ();  beskyttet funksjon onDied (enhet: Entity): void destroy (); 

Det er ganske detaljert detalj detaljer her, men det viktigste å legge merke til er at i konstruktøren vi instantierer og konfigurerer Kropp, fysikk, Helse, Utsikt og Våpen komponenter. (De Våpen komponent er faktisk en forekomst av Våpen i stedet for våpenbasen.)

Jeg bruker API-tegningene for Flash-grafikk for å lage mitt skip (linjer 29-32), men vi kunne like enkelt bruke et bitmap-bilde. Jeg lager også en forekomst av min Gamepad-klasse - dette er et åpen kildebibliotek jeg skrev for noen år siden for å gjøre tastaturinngangen enklere i Flash.

Jeg har også overstyrt Oppdater Funksjon fra baseklassen for å legge til litt tilpasset oppførsel: Etter utløsing av all standard oppførsel med super.update () Vi roterer og skyver skipet basert på tastaturinngangen, og brann våpenet hvis brannnøkkelen trykkes.

Ved å lytte til døde Signal av helsekomponenten, vi utløser onDied Fungerer hvis spilleren går tom for treffpunkter. Når dette skjer, forteller vi bare skipet om å ødelegge seg selv.


De Våpen Klasse

Neste, la oss fyre opp det Våpen klasse:

 pakke asteroider import engine.Entity; importere engine.Weapon; / ** * ... * @author Iain Lobb - [email protected] * / offentlig klasse Gun utvider våpen offentlig funksjon Gun (enhet: Entity) super (entity);  overstyre offentlig funksjon brann (): 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 (bullet); super.fire (); 

Dette er en fin kort en! Vi overstyrer bare Brann() funksjon for å lage en ny Kule når spilleren brenner. Etter å ha passet posisjonen og rotasjonen av kulen til skipet, og stikker den i riktig retning, sender vi den entityCreated slik at den kan legges til spillet.

En flott ting om dette Våpen klassen er at den brukes av både spilleren og fiendtlige skip.


De Kule Klasse

EN Våpen skaper en forekomst av dette Kule klasse:

 pakke asteroider import engine.Body; import engine.Entity; import engine.Physics; import engine.View; importer flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / offentlig klasse Bullet utvider Entity public var alder: int; offentlig funksjon Bullet () body = new Body (this); body.radius = 5; fysikk = ny fysikk (dette); view = new View (dette); view.sprite = new Sprite (); view.sprite.graphics.beginFill (0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius);  overstyre offentlig funksjon oppdatering (): void super.update (); for hver (varmål: Enhet i mål) if (body.testCollision (target)) target.health.hit (1); ødelegge(); komme tilbake;  alder ++; hvis (alder> 20) view.alpha - = 0.2; hvis (alder> 25) ødelegger (); 

Konstruktøren instanser og konfigurerer kropp, fysikk og visning. I oppdateringsfunksjonen kan du nå se listen som heter mål komme til nytte når vi løper gjennom alle tingene vi ønsker å slå og se om noen av dem krysser kulen.

Dette kollisjonssystemet ville ikke skalere til tusenvis av kuler, men er bra for de fleste casual spill.

Hvis kulen får mer enn 20 bilder, begynner vi å fade den ut, og hvis den er eldre enn 25 rammer, ødelegger vi den. Som med Våpen, de Kule brukes av både spilleren og fienden - eksemplene har bare en annen målliste.

Når vi snakkar om det…


De EnemyShip Klasse

La oss se på dette fiendtlige skipet:

 pakke asteroider import engine.Body; import engine.Entity; importere motor. Helse; import engine.Physics; import engine.View; importer flash.display.GraphicsPathWinding; importer flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / offentlig klasse EnemyShip utvider Entity protected var turnDirection: Number = 1; offentlig funksjon EnemyShip () body = new Body (this); body.x = 750; body.y = 550; fysikk = ny fysikk (dette); fysikk.drag = 0,9; view = new View (dette); view.sprite = new Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawPath (Vector.([1, 2, 2, 2, 2]), vektor.([0, 10, 10, -10, 0, 0, -10, -10, 0, 10]), GraphicsPathWinding.NON_ZERO); helse = ny helse (dette); health.hits = 5; health.died.add (onDied); våpen = ny pistol (dette);  overstyre offentlig funksjon oppdatering (): void super.update (); hvis (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();   

Som du kan se, er det ganske lik spillerskipsklassen. Den eneste virkelige forskjellen er at i Oppdater() funksjon, i stedet for å ha spillerkontroll via tastaturet, har vi litt "kunstig dumhet" for å gjøre skipet vandre og brann tilfeldig.


De Asteroid Klasse

Den andre enheten skriver spilleren kan skyte på, er selve asteroiden:

 pakke asteroider import engine.Body; import engine.Entity; importere motor. Helse; import engine.Physics; import engine.View; importer flash.display.Sprite; / ** * ... * @author Iain Lobb - [email protected] * / offentlig klasse Asteroid utvider Entity (offentlig funksjon Asteroid () body = new Body (this); body.radius = 20; body.x = Math.random () * 800; body.y = Math.random () * 600; fysikk = ny fysikk (dette); physics.velocityX = (Math.random () * 10) - 5; physics.velocityY = (Math.random () * 10) - 5; view = new View (dette); view.sprite = new Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius); helse = ny helse (dette); health.hits = 3; health.hurt.add (onHurt);  overstyre offentlig funksjon oppdatering (): void super.update (); for hver (varmål: Enhet i mål) if (body.testCollision (target)) target.health.hit (1); ødelegge(); komme tilbake;  beskyttet funksjon onHurt (enhet: Entity): void body.radius * = 0.75; view.scale * = 0.75; hvis (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);   

Forhåpentligvis blir du vant til hvordan disse enhetsklassene ser ut nå.

I konstruktøren initialiserer vi våre komponenter og slår fast posisjon og hastighet.

I Oppdater() funksjon vi sjekker om kollisjoner med mållisten vår - som i dette eksemplet bare vil ha et enkelt element - spillerens skip. Hvis vi finner en kollisjon, skader vi målet og ødelegger asteroiden. På den annen side, hvis asteroiden er skadet selv (det vil si at den blir rammet av en spillerkule), krymper vi den og lager en andre asteroide, noe som skaper den illusjonen at den har blitt sprengt i to deler. Vi vet når du skal gjøre dette ved å lytte til helsekomponentens "skadede" signal.


De AsteroidsGame Klasse

Til slutt, la oss se på AsteroidsGame-klassen som styrer hele showet:

 pakke asteroider import engine.Entity; importere engine.Game; importer flash.events.MouseEvent; importer flash.filters.GlowFilter; importer flash.text.TextField; / ** * ... * @author Iain Lobb - [email protected] * / offentlig klasse AsteroidsGame utvider spillet public var players: Vector. = Ny Vector.(); offentlige var fiender: Vector. = Ny Vector.(); public var messageField: TextField; offentlig funksjon AsteroidsGame ()  overstyre beskyttet funksjon startGame (): void var asteroid: Asteroid; for (var jeg: 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; hvis (entity.body.x < -50) entity.body.x += 900; if (entity.body.y > 650) entity.body.y - = 700; hvis (entity.body.y < -50) entity.body.y += 700;  if (enemies.length == 0) gameOver();   

Denne klassen er ganske lang (vel, mer enn 100 linjer!) Fordi det gjør mange ting.

I start spill() den skaper og konfigurerer 10 asteroider, skipet og fiendens skip, og skaper også meldingen "KLIKK TIL START".

De start() funksjonen avbryter spillet og fjerner meldingen, mens spillet er slutt funksjonen pauser spillet igjen og gjenoppretter meldingen. De omstart() funksjon lytter til et museklikk på skjermbildet Game Over - når dette skjer, stopper spillet og starter det på nytt.

De Oppdater() fungere looper gjennom alle fiender og warps noen som har drevet av skjermen, samt sjekke for vinnevilligheten, som er at det ikke er noen fiender igjen i fienderlisten.


Tar det videre

Dette er en ganske bare beinmotor og et enkelt spill, så nå la vi tenke på måter vi kunne utvide den på.

  • Vi kan legge til en prioritetsverdi for hver enhet, og sortere listen før hver oppdatering, slik at vi kan sørge for at enkelte typer enheter alltid oppdateres etter andre typer.
  • Vi kan bruke objektbassing slik at vi gjenbruker døde gjenstander (for eksempel kuler) i stedet bare å skape hundrevis av nye.
  • Vi kunne legge til et kamerasystem slik at vi kan bla og zoome på scenen. Vi kunne utvide komponentene Body and Physics for å legge til støtte for Box2D eller en annen fysikkmotor.
  • Vi kan opprette en beholdningskomponent, slik at enheter kan bære elementer.

I tillegg til å utvide de enkelte komponentene, kan vi til tider trenge å forlenge IEntity grensesnitt for å lage spesielle typer enheter med spesialiserte komponenter.

For eksempel, hvis vi lager et plattformspill, og vi har en ny komponent som håndterer alle de svært spesifikke tingene som en plattformspillkarakter trenger - er de på bakken, er de på en vegg, hvor lenge har de vært i luften, kan de doble hoppe etc. - andre enheter kan også trenge tilgang til denne informasjonen. Men det er ikke en del av kjernen Entity API, som holdes med vilje veldig generell. Så vi må definere et nytt grensesnitt, som gir tilgang til alle standard enhetskomponenter, men legger til tilgang til PlatformController komponent.

For dette ville vi gjøre noe som:

 pakke plattformspill import engine.IEntity; / ** * ... * @author Iain Lobb - [email protected] * / offentlig grensesnitt IPlatformEntity utvider IEntity funksjonssett platformController (verdi: PlatformController): void; funksjonen få platformController (): PlatformController; 

Enhver enhet som trenger "plattform" -funksjonalitet, implementerer deretter dette grensesnittet, slik at andre enheter kan interagere med PlatformController komponent.


konklusjoner

Ved å våge å skrive om spillarkitektur, er jeg redd for at jeg rører en hornets hekker - men det er (det meste) alltid en god ting, og jeg håper i det minste at jeg har fått deg til å tenke på hvordan du organiserer kode.

Til syvende og sist tror jeg ikke på at du bør bli for hengt opp på hvordan du strukturerer ting; det som virker for deg å få spillet gjort er den beste strategien. Jeg vet at det er langt mer avanserte systemer som jeg skisserer her, som løser en rekke problemer utover de jeg har diskutert, men de kan pleie å begynne å se veldig ukjent hvis du er vant til en tradisjonell arvsbasert arkitektur.

Jeg liker tilnærmingen jeg har foreslått her, fordi den gjør at koden kan organiseres med hensikt, i små fokuserte klasser, samtidig som det gir et statisk skrevet, utvidbart grensesnitt og uten å stole på dynamiske språkfunksjoner eller string oppslag. Hvis du vil endre oppførselen til en bestemt komponent, kan du utvide komponenten og overstyre metodene du vil endre. Klasser har en tendens til å holde seg veldig kort, så jeg finner aldri meg selv å bla gjennom tusenvis av linjer for å finne koden jeg leter etter.

Best av alt, jeg er i stand til å ha en enkelt motor som er fleksibel nok til å bruke på tvers av alle spillene jeg lager, og sparer meg mye tid.