I den forrige veiledningen opprettet vi et bare-ben-komponentbasert Entity-system. Nå skal vi bruke dette systemet til å lage et enkelt Asteroids-spill.
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.
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.
Skip
KlasseVi 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.
Våpen
KlasseNeste, 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.
Kule
KlasseEN 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…
EnemyShip
KlasseLa 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.
Asteroid
KlasseDen 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.
AsteroidsGame
KlasseTil 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.
Dette er en ganske bare beinmotor og et enkelt spill, så nå la vi tenke på måter vi kunne utvide den på.
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.
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.