In de vorige zelfstudie hebben we een op naam gebaseerd component-gebaseerd entiteitssysteem gemaakt. Nu zullen we dit systeem gebruiken om een eenvoudig Asteroids-spel te maken.
Dit is de eenvoudige Asteroids-game die we in deze zelfstudie gaan maken. Het is geschreven met behulp van Flash en AS3, maar de algemene concepten zijn van toepassing op de meeste talen.
De volledige broncode is beschikbaar op GitHub.
Er zijn zes klassen:
AsteroidsGame
, die de basisspelklasse uitbreidt en de logica toevoegt die specifiek is voor onze space shoot-'em-up.Schip
, wat is het ding dat je bestuurt.Asteroïde
, dat is het ding waar je op schiet.Kogel
, wat is het ding dat je ontslaat.geweer
, die deze kogels maakt.EnemyShip
, wat een zwervende alien is die er gewoon is om een beetje afwisseling toe te voegen aan het spel.Laten we deze entiteitstypen één voor één bekijken.
Schip
KlasseWe beginnen met het schip van de speler:
pakket asteroïden import com.iainlobb.gamepad.Gamepad; import com.iainlobb.gamepad.KeyCode; import engine.Body; import engine.Entity; importeer de engine. Game; import engine.Health; import motor. Fysica; import engine.View; import flash.display.GraphicsPathWinding; import flash.display.Sprite; / ** * ... * @auteur Iain Lobb - [email protected] * / public class Ship breidt Entity uit protected var gamepad: Gamepad; public function Ship () body = new Body (this); body.x = 400; body.y = 300; fysica = nieuwe natuurkunde (this); physics.drag = 0.9; view = new View (this); view.sprite = nieuwe Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawPath (Vector.([1, 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); gezondheid = nieuwe gezondheid (dit); health.hits = 5; health.died.add (onDied); wapen = nieuw pistool (dit); gamepad = nieuwe 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 (); beschermde functie onDied (entity: Entity): void destroy ();
Er zijn nogal wat implementatiedetails hier, maar het belangrijkste om op te merken is dat we in de constructor instantiëren en configureren Lichaam
, Fysica
, Gezondheid
, Uitzicht
en Wapen
componenten. (De Wapen
component is in feite een instantie van geweer
in plaats van de klasse van de wapenbasis.)
Ik gebruik de Flash-grafische tekening-API's om mijn schip te maken (regels 29-32), maar we kunnen evengoed een bitmapafbeelding gebruiken. Ik maak ook een exemplaar van mijn Gamepad-klasse - dit is een open-sourcebibliotheek die ik een paar jaar geleden heb geschreven om het invoeren van het toetsenbord in Flash gemakkelijker te maken.
Ik heb ook het bijwerken
functioneer vanuit de basisklasse om wat aangepast gedrag toe te voegen: na het triggeren van alle standaardgedrag met super.update ()
we roteren en stuwen het schip op basis van de toetsenbordinvoer en schieten op het vuur als de vuurknop wordt ingedrukt.
Door te luisteren naar de ging dood
Signaal van de gezondheidscomponent, we activeren de onDied
functie als de speler geen hit points heeft. Wanneer dit gebeurt, vertellen we het schip om zichzelf te vernietigen.
geweer
KlasseLaten we vervolgens dat opstarten geweer
klasse:
pakket asteroïden import engine.Entity; import engine.Wapen; / ** * ... * @auteur Iain Lobb - [email protected] * / public class Gun extends Weapon public function Gun (entity: Entity) super (entity); 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 (kogel); super.fire ();
Dit is een leuke korte! We overschrijven gewoon de brand()
functie om een nieuwe te maken Kogel
telkens wanneer de speler vuurt. Nadat we de positie en rotatie van de kogel in het schip hebben afgestemd en het in de goede richting hebben afgelegd, verzenden we entityCreated
zodat het kan worden toegevoegd aan het spel.
Een geweldige zaak hierover geweer
klasse is dat het wordt gebruikt door zowel de speler als vijandelijke schepen.
Kogel
KlasseEEN geweer
maakt hier een instantie van Kogel
klasse:
pakket asteroïden import engine.Body; import engine.Entity; import motor. Fysica; import engine.View; import flash.display.Sprite; / ** * ... * @auteur Iain Lobb - [email protected] * / public class Bullet breidt Entity uit public var age: int; public function Bullet () body = new Body (this); body.radius = 5; fysica = nieuwe natuurkunde (this); view = new View (this); view.sprite = nieuwe Sprite (); view.sprite.graphics.beginFill (0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius); override public function update (): void super.update (); voor elk (var-doel: Entiteit in doelen) if (body.testCollision (doel)) target.health.hit (1); vernietigen(); terug te keren; leeftijd ++; als (leeftijd> 20) viewa = - 0,2; als (leeftijd> 25) vernietigen ();
De constructor maakt een instantisatie en configureert het lichaam, de fysica en het zicht. In de updatefunctie kunt u nu de opgeroepen lijst bekijken doelen
van pas komen, terwijl we door alle dingen lopen die we willen raken en kijken of een van hen de kogel doorsnijdt.
Dit botsingssysteem zou niet schalen naar duizenden kogels, maar is prima voor de meeste casual games.
Als de kogel meer dan 20 frames oud wordt, beginnen we het uit te faden en als het meer dan 25 frames is, vernietigen we het. Zoals met de geweer
, de Kogel
wordt gebruikt door zowel de speler als de vijand - de instanties hebben gewoon een andere doelenlijst.
Over wat gezegd ...
EnemyShip
KlasseLaten we nu eens kijken naar dat vijandelijke schip:
pakket asteroïden import engine.Body; import engine.Entity; import engine.Health; import motor. Fysica; import engine.View; import flash.display.GraphicsPathWinding; import flash.display.Sprite; / ** * ... * @auteur Iain Lobb - [email protected] * / public class EnemyShip breidt Entity uit protected var turnDirection: Number = 1; openbare functie EnemyShip () body = new Body (this); body.x = 750; body.y = 550; fysica = nieuwe natuurkunde (this); physics.drag = 0.9; view = new View (this); view.sprite = nieuwe 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); gezondheid = nieuwe gezondheid (dit); health.hits = 5; health.died.add (onDied); wapen = nieuw pistool (dit); 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();
Zoals je ziet, is het redelijk vergelijkbaar met de klasse van de speler. Het enige echte verschil is dat in de bijwerken()
functie, in plaats van controle over de speler via het toetsenbord, hebben we wat "kunstmatige domheid" om het schip te laten dwalen en willekeurig te vuren.
Asteroïde
KlasseHet andere entiteitstype waar de speler op kan schieten is de asteroïde zelf:
pakket asteroïden import engine.Body; import engine.Entity; import engine.Health; import motor. Fysica; import engine.View; import flash.display.Sprite; / ** * ... * @auteur Iain Lobb - [email protected] * / public class Asteroid breidt Entity uit public function Asteroid () body = new Body (this); body.radius = 20; body.x = Math.random () * 800; body.y = Math.random () * 600; fysica = nieuwe natuurkunde (this); physics.velocityX = (Math.random () * 10) - 5; physics.velocityY = (Math.random () * 10) - 5; view = new View (this); view.sprite = nieuwe Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius); gezondheid = nieuwe gezondheid (dit); health.hits = 3; health.hurt.add (onHurt); override public function update (): void super.update (); voor elk (var-doel: Entiteit in doelen) if (body.testCollision (doel)) target.health.hit (1); vernietigen(); terug te keren; beschermde functie onHurt (entiteit: Entiteit): 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);
Hopelijk raak je eraan gewend hoe deze entiteitslessen er nu uitzien.
In de constructor initialiseren we onze componenten en randomiseren we de positie en snelheid.
In de bijwerken()
functie controleren we op botsingen met onze doelenlijst - die in dit voorbeeld slechts één item zal hebben - het schip van de speler. Als we een botsing vinden, beschadigen we het doelwit en vernietigen we de asteroïde. Aan de andere kant, als de asteroïde zelf beschadigd is (d.w.z. hij is geraakt door een spelerskogel), krimpen we deze en creëren we een tweede asteroïde, waardoor de illusie wordt gecreëerd dat deze in twee stukken is opgeblazen. We weten wanneer we dit moeten doen door te luisteren naar het "pijn" -signaal van de Health-component.
AsteroidsGame
KlasseLaten we tot slot kijken naar de klasse AsteroidsGame die de hele show bestuurt:
pakket asteroïden import engine.Entity; importeer de engine. Game; import flash.events.MouseEvent; import flash.filters.GlowFilter; import flash.text.TextField; / ** * ... * @auteur Iain Lobb - [email protected] * / public class Asteroids Game verlengt Game public var players: Vector.= nieuwe Vector. (); openbare var vijanden: Vector. = nieuwe Vector. (); public var messageField: TextField; openbare functie AsteroidsGame () override-beschermde functie startGame (): void var asteroid: Asteroid; for (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; if (entity.body.x < -50) entity.body.x += 900; if (entity.body.y > 650) entity.body.y - = 700; if (entity.body.y < -50) entity.body.y += 700; if (enemies.length == 0) gameOver();
Deze les duurt behoorlijk lang (nou ja, meer dan 100 lijnen!) Omdat het een heleboel dingen doet.
In start het spel()
het maakt en configureert 10 asteroïden, het schip en het vijandelijke schip en maakt ook het bericht "KLIK VOOR START".
De begin()
functie zet het spel ongedaan en verwijdert het bericht, terwijl het spel is over
functie pauzeert het spel opnieuw en herstelt het bericht. De herstarten()
functie luistert naar een muisklik op het Game Over scherm - wanneer dit gebeurt, stopt het spel en begint het opnieuw.
De bijwerken()
De functie loopt door alle vijanden en vervormt diegene die van het scherm zijn afgegooid, evenals het controleren op de win-situatie, wat betekent dat er geen vijanden meer zijn in de lijst met vijanden.
Dit is een vrij kale bottenmotor en een eenvoudig spel, dus laten we nu eens nadenken over manieren waarop we het kunnen uitbreiden.
Naast uitbreiding van de afzonderlijke componenten, kunnen we soms ook de IEntity
interface om speciale typen entiteiten te maken met gespecialiseerde componenten.
Als we bijvoorbeeld een platformgame maken en we een nieuw onderdeel hebben dat alle zeer specifieke dingen behandelt die een karakter van een platformspel nodig heeft - liggen ze op de grond, raken ze een muur, hoe lang zijn ze al in de lucht, kunnen ze dubbelspringen, enz. - andere entiteiten moeten mogelijk ook toegang krijgen tot deze informatie. Maar het maakt geen deel uit van de belangrijkste Entity API, die opzettelijk heel algemeen wordt gehouden. We moeten dus een nieuwe interface definiëren die toegang biedt tot alle standaard entiteitscomponenten, maar toegang tot de PlatformController
bestanddeel.
Hiervoor zouden we iets doen als:
pakket platformspel import engine.IEntity; / ** * ... * @auteur Iain Lobb - [email protected] * / openbare interface IPlatformEntity breidt IEntity uit functie set platformController (waarde: PlatformController): ongeldig; function get platformController (): PlatformController;
Elke entiteit die een platformfunctie nodig heeft, implementeert deze interface en stelt andere entiteiten in staat om te communiceren met de PlatformController
bestanddeel.
Door zelfs te durven schrijven over gamearchitectuur, ben ik bang dat ik een horzelnest van mening roer - maar dat is (meestal) altijd een goede zaak, en ik hoop dat ik je op zijn minst heb laten nadenken over hoe je je kunt organiseren code.
Uiteindelijk geloof ik niet dat je te veel moet ophouden met hoe je dingen structureert; alles wat voor u werkt om uw spel gedaan te krijgen, is de beste strategie. Ik weet dat er veel geavanceerdere systemen zijn die ik hier beschrijf, die een aantal andere problemen oplost dan degene die ik heb besproken, maar die de neiging hebben om er heel onbekend uit te zien als je gewend bent aan een traditionele overervingsarchitectuur.
Ik hou van de aanpak die ik hier heb voorgesteld, omdat code hierdoor doelgericht kan worden georganiseerd, in klassen met kleine gerichtheid, terwijl een statisch getypeerde, uitbreidbare interface wordt geboden en zonder afhankelijk te zijn van dynamische taalfuncties of Draad
lookups. Als u het gedrag van een bepaald onderdeel wilt wijzigen, kunt u dat onderdeel uitbreiden en de methoden overschrijven die u wilt wijzigen. Klassen hebben de neiging om erg kort te blijven, dus ik merk dat ik nooit door duizenden regels scrol om de code te vinden die ik zoek.
Het beste van alles is dat ik in staat ben om een enkele engine te hebben die flexibel genoeg is om te gebruiken in alle spellen die ik maak, waardoor ik enorm veel tijd kan besparen.