Organising your game code into component-based entities, rather than relying only on class inheritance, is a popular approach in game development. In this tutorial, we’ll look at why you might do this, and set up a simple game engine using this technique.
Introduction
In this tutorial I’m going to explore component-based game entities, look at why you might want to use them, and suggest a pragmatic approach to dip your toe in the water.
As it’s a story about code organisation and architecture, I’ll start by dropping in the usual “get out of jail” disclaimer: this is just one way of doing things, it’s not “the one way” or maybe even the best way, but it might work for you. Personally, I like to find out about as many approaches as possible and then work out what suits me.
Final Result Preview
Throughout this two-part tutorial, we’ll create this Asteroids game. (The full source code is available on GitHub.) In this first part, we’ll focus on the core concepts and general game engine.
What Problem Are We Solving?
In a game like Asteroids, we might have a few basic types of on-screen “thing”: bullets, asteroids, player ship and enemy ship. We might want to represent these basic types as four separate classes, each containing all the code we need to draw, animate, move and control that object.
While this will work, it might be better to follow the Don’t Repeat Yourself (DRY) principle and try to reuse some of the code between each class — after all, the code for moving and drawing a bullet is going to be very similar to, if not exactly the same as, the code to move and draw an asteroid or a ship.
So we can refactor our rendering and movement functions into a base class that everything extends from. But Ship
and EnemyShip
also need to be able to shoot. At this point we could add the shoot
function to the base class, creating a “Giant Blob” class that can do basically everything, and just make sure asteroids and bullets never call their shoot
function. This base class would soon get very large, swelling in size each time entities need to be able to do new things. This isn’t necessarily wrong, but I find smaller, more specialised classes to be easier to maintain.
Alternatively, we can go down the root of deep inheritance and have something like EnemyShip extends Ship extends ShootingEntity extends Entity
. Again this approach isn’t wrong, and will also work quite well, but as you add more types of Entities, you will find yourself constantly having to readjust the inheritance hierarchy to handle all the possible scenarios, and you can box yourself into a corner where a new type of Entity needs to have the functionality of two different base classes, requiring multiple inheritance (which most programming languages don’t offer).
I have used the deep hierarchy approach many times myself, but I actually prefer the Giant Blob approach, as at least then all entities have a common interface and new entities can be added more easily (so what if all your trees have A* pathfinding?!)
There is, however, a third way…
Composition Over Inheritance
If we think of the Asteroids problem in terms of things that objects might need to do, we might get a list like this:
move()
shoot()
takeDamage()
die()
render()
Instead of working out a complicated inheritance hierarchy for which objects can do which things, let’s model the problem in terms of components that can perform these actions.
For instance, we could create a Health
class, with the methods takeDamage()
, heal()
and die()
. Then any object that needs to be able to take damage and die can “compose” an instance of the Health
class — where “compose” basically means “keep a reference to its own instance of this class”.
We could create another class called View
to look after the rendering functionality, one called Body
to handle movement and one called Weapon
to handle shooting.
Most Entity systems are based on the principle described above, but differ in how you access functionality contained in a component.
Mirroring the API
For example, one approach is to mirror the API of each component in the Entity, so an entity that can take damage would have a takeDamage()
function that itself just calls the takeDamage()
function of its Health
component.
class Entity { private var _health:Health; //...other code...// public function takeDamage(dmg:int) { _health.takeDamage(dmg); } }
You then have to create an interface called something like IHealth
for your entity to implement, so that other objects can access the takeDamage()
function. This is how a Java OOP guide might advise you to do it.
getComponent()
Another approach is to simply store each component in a key-value lookup, so that every Entity has a function called something like getComponent("componentName")
which returns a reference to the particular component. You then need to cast the reference you get back to the type of component you want — something like:
var health:Health = Health(getComponent("Health"));
This is basically how Unity’s entity/behaviour system works. It’s very flexible, because you can keep adding new types of component without changing your base class, or creating new subclasses or interfaces. It might also be useful when you want to use configuration files to create entities without recompiling your code, but I’ll leave that to someone else to figure out.
Public Components
The approach I favour is to let all entities have a public property for each major type of component, and leave fields null if the entity doesn’t have that functionality. When you want to call a particular method, you just “reach in” to the entity to get the component with that functionality — for example, call enemy.health.takeDamage(5)
to attack an enemy.
If you try to call health.takeDamage()
on an entity that doesn’t have a Health
component, it will compile, but you’ll get a runtime error letting you know you’ve done something silly. In practice this rarely happens, as it’s pretty obvious which types of entity will have which components (for example, of course a tree doesn’t have a weapon!).
Some strict OOP advocates might argue that my approach breaks some OOP principles, but I find it works really well, and there’s a really good precedent from the history of Adobe Flash.
In ActionScript 2, the MovieClip
class had methods for drawing vector graphics: for example, you could call myMovieClip.lineTo()
to draw a line. In ActionScript 3, these drawing methods were moved to the Graphics
class, and each MovieClip
gets a Graphics
component, which you access by calling, for example, myMovieClip.graphics.lineTo()
in the same way I described for enemy.health.takeDamage()
. If it’s good enough for the ActionScript language designers, it’s good enough for me.
My System (Simplified)
Below I’m going to detail a very simplified version of the system I use across all my games. In terms of how simplified, it’s something like 300 lines of code for this, compared to 6,000 for my full engine. But we can actually do quite a lot with just these 300 lines!
I’ve left in just enough functionality to create a working game, while keeping the code as short as possible so it’s easier to follow. The code is going to be in ActionScript 3, but a similar structure is possible across most languages. There are a few public variables that could be properties (i.e. put behind get
and set
accessor functions), but as this is quite verbose in ActionScript, I’ve left them as public variables for ease of reading.
The IEntity
Interface
Let’s start by defining an interface that all entities will implement:
package engine { import org.osflash.signals.Signal; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public interface IEntity { // ACTIONS function destroy():void; function update():void; function render():void; // COMPONENTS function get body():Body; function set body(value:Body):void; function get physics():Physics; function set physics(value:Physics):void function get health():Health function set health(value:Health):void function get weapon():Weapon; function set weapon(value:Weapon):void; function get view():View; function set view(value:View):void; // SIGNALS function get entityCreated():Signal; function set entityCreated(value:Signal):void; function get destroyed():Signal; function set destroyed(value:Signal):void; // DEPENDENCIES function get targets():Vector.<Entity>; function set targets(value:Vector.<Entity>):void; function get group():Vector.<Entity>; function set group(value:Vector.<Entity>):void; } }
All entities can perform three actions: you can update them, render them and destroy them.
They each have “slots” for five components:
- A
body
, handling position and size. -
physics
, handling movement. -
health
, handling getting hurt. - A
weapon
, handling attacking. - And finally a
view
, allowing you to render the entity.
All of these components are optional and can be left null, but in practice most entities will have at least a couple of components.
A piece of static scenery that the player can’t interact with (maybe a tree, for example), would need just a body and a view. It wouldn’t need physics as it doesn’t move, it wouldn’t need health as you can’t attack it, and it certainly wouldn’t need a weapon. The player’s ship in Asteroids, on the other hand, would need all five components, as it can move, shoot and get hurt.
By configuring these five basic components, you can create most simple objects you might need. Sometimes they won’t be enough, however, and at that point we can either extend the basic components, or create new additional ones — both of which we’ll discuss later.
Next we have two Signals: entityCreated
and destroyed
.
Signals are an open source alternative to ActionScript’s native events, created by Robert Penner. They’re really nice to use as they allow you to pass data between the dispatcher and the listener without having to create lots of custom Event classes. For more information on how to use them, check out the documentation.
The entityCreated
Signal allows an entity to tell the game that there is another new entity that needs to be added — a classic example being when a gun creates a bullet. The destroyed
Signal lets the game (and any other listening objects) know that this entity has been destroyed.
Finally, the entity has two other optional dependencies: targets
, which is a list of entities that it might want to attack, and group
, which is a list of entities that it belongs to. For example, a player ship might have a list of targets, which would be all the enemies in the game, and might belong to a group which also contains any other players and friendly units.
The Entity
Class
Now let’s look at the Entity
class that implements this interface.
package engine { import org.osflash.signals.Signal; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class Entity implements IEntity { private var _body:Body; private var _physics:Physics; private var _health:Health; private var _weapon:Weapon; private var _view:View; private var _entityCreated:Signal; private var _destroyed:Signal; private var _targets:Vector.<Entity>; private var _group:Vector.<Entity>; /* * Anything that exists within your game is an Entity! */ public function Entity() { entityCreated = new Signal(Entity); destroyed = new Signal(Entity); } public function destroy():void { destroyed.dispatch(this); if (group) group.splice(group.indexOf(this), 1); } public function update():void { if (physics) physics.update(); } public function render():void { if (view) view.render(); } public function get body():Body { return _body; } public function set body(value:Body):void { _body = value; } public function get physics():Physics { return _physics; } public function set physics(value:Physics):void { _physics = value; } public function get health():Health { return _health; } public function set health(value:Health):void { _health = value; } public function get weapon():Weapon { return _weapon; } public function set weapon(value:Weapon):void { _weapon = value; } public function get view():View { return _view; } public function set view(value:View):void { _view = value; } public function get entityCreated():Signal { return _entityCreated; } public function set entityCreated(value:Signal):void { _entityCreated = value; } public function get destroyed():Signal { return _destroyed; } public function set destroyed(value:Signal):void { _destroyed = value; } public function get targets():Vector.<Entity> { return _targets; } public function set targets(value:Vector.<Entity>):void { _targets = value; } public function get group():Vector.<Entity> { return _group; } public function set group(value:Vector.<Entity>):void { _group = value; } } }
It looks long, but most of it is just those verbose getter and setter functions (boo!). The important part to look at is the first four functions: the constructor, where we create our Signals; destroy()
, where we dispatch the destroyed Signal and remove the entity from its group list; update()
, where we update any components that need to act every game loop — although in this simple example this is only the physics
component — and finally render()
, where we tell the view to do its thing.
You’ll notice that we don’t automatically instantiate the components here in the Entity class — this is because, as I explained earlier, each component is optional.
The Individual Components
Now let’s look at the components one by one. First, the body component:
package engine { /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class Body { public var entity:Entity; public var x:Number = 0; public var y:Number = 0; public var angle:Number = 0; public var radius:Number = 10; /* * If you give an entity a body it can take physical form in the world, * although to see it you will need a view. */ public function Body(entity:Entity) { this.entity = entity; } public function testCollision(otherEntity:Entity):Boolean { var dx:Number; var dy:Number; dx = x - otherEntity.body.x; dy = y - otherEntity.body.y; return Math.sqrt((dx * dx) + (dy * dy)) <= radius + otherEntity.body.radius; } } }
All our components need a reference to their owner entity, which we pass to the constructor. The body then has four simple fields: an x and y position, an angle of rotation, and a radius to store its size. (In this simple example, all entities are circular!)
This component also has a single method: testCollision()
, which uses Pythagoras to calculate the distance between two entities, and compares this to their combined radiuses. (More info here.)
Next let’s look at the Physics
component:
package engine { /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class Physics { public var entity:Entity; public var drag:Number = 1; public var velocityX:Number = 0; public var velocityY:Number = 0; /* * Provides a basic physics step without collision detection. * Extend to add collision handling. */ public function Physics(entity:Entity) { this.entity = entity; } public function update():void { entity.body.x += velocityX; entity.body.y += velocityY; velocityX *= drag; velocityY *= drag; } public function thrust(power:Number):void { velocityX += Math.sin(-entity.body.angle) * power; velocityY += Math.cos(-entity.body.angle) * power; } } }
Looking at the update()
function, you can see that the velocityX
and velocityY
values are added onto the entity’s position, which moves it, and the velocity is multiplied by drag
, which has the effect of gradually slowing the object down. The thrust()
function allows a quick way to accelerate the entity in the direction it is facing.
Next let’s look at the Health
component:
package engine { import org.osflash.signals.Signal; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class Health { public var entity:Entity; public var hits:int; public var died:Signal; public var hurt:Signal; public function Health(entity:Entity) { this.entity = entity; died = new Signal(Entity); hurt = new Signal(Entity); } public function hit(damage:int):void { hits -= damage; hurt.dispatch(entity); if (hits < 0) { died.dispatch(entity); } } } }
The Health
component has a function called hit()
, allowing the entity to be hurt. When this happens, the hits
value is reduced, and any listening objects are notified by dispatching the hurt
Signal. If hits
are less than zero, the entity is dead and we dispatch the died
Signal.
Let’s see what’s inside the Weapon
component:
package engine { import org.osflash.signals.Signal; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class Weapon { public var entity:Entity; public var ammo:int; /* * Weapon is the base class for all weapons. */ public function Weapon(entity:Entity) { this.entity = entity; } public function fire():void { ammo--; } } }
Not much here! That’s because this is really just a base class for the actual weapons — as you’ll see in the Gun
example later. There’s a fire()
method that subclasses should override, but here it just reduces the value of ammo
.
The final component to examine is View
:
package engine { import flash.display.Sprite; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class View { public var entity:Entity; public var scale:Number = 1; public var alpha:Number = 1; public var sprite:Sprite; /* * View is display component which renders an Entity using the standard display list. */ public function View(entity:Entity) { this.entity = entity; } public function render():void { sprite.x = entity.body.x; sprite.y = entity.body.y; sprite.rotation = entity.body.angle * (180 / Math.PI); sprite.alpha = alpha; sprite.scaleX = scale; sprite.scaleY = scale; } } }
This component is very specific to Flash. The main event here is the render()
function, which updates a Flash sprite with the body’s position and rotation values, and the alpha and scale values it stores itself. If you wanted to use a different rendering system such as copyPixels
blitting or Stage3D (or indeed a system relevant to a different choice of platform), you would adapt this class.
The Game
Class
Now we know what an Entity and all its components look like. Before we start using this engine to make an example game, let’s look at the final piece of the engine — the Game class that controls the whole system:
package engine { import flash.display.Sprite; import flash.display.Stage; import flash.events.Event; /** * ... * @author Iain Lobb - iainlobb@gmail.com */ public class Game extends Sprite { public var entities:Vector.<Entity> = new Vector.<Entity>(); public var isPaused:Boolean; static public var stage:Stage; /* * Game is the base class for games. */ public function Game() { addEventListener(Event.ENTER_FRAME, onEnterFrame); addEventListener(Event.ADDED_TO_STAGE, onAddedToStage); } protected function onEnterFrame(event:Event):void { if (isPaused) return; update(); render(); } protected function update():void { for each (var entity:Entity in entities) entity.update(); } protected function render():void { for each (var entity:Entity in entities) entity.render(); } protected function onAddedToStage(event:Event):void { Game.stage = stage; startGame(); } protected function startGame():void { } protected function stopGame():void { for each (var entity:Entity in entities) { if (entity.view) removeChild(entity.view.sprite); } entities.length = 0; } public function addEntity(entity:Entity):Entity { entities.push(entity); entity.destroyed.add(onEntityDestroyed); entity.entityCreated.add(addEntity); if (entity.view) addChild(entity.view.sprite); return entity; } protected function onEntityDestroyed(entity:Entity):void { entities.splice(entities.indexOf(entity), 1); if (entity.view) removeChild(entity.view.sprite); entity.destroyed.remove(onEntityDestroyed); } } }
There’s a lot of implementation detail here, but let’s just pick out the highlights.
Every frame, the Game
class loops through all the entities, and calls their update and render methods. In the addEntity
function, we add the new entity to the entities list, listen to its Signals, and if it has a view, add its sprite to the stage.
When onEntityDestroyed
is triggered, we remove the entity from the list and remove its sprite from the stage. In the stopGame
function, which you only call if you want to end the game, we remove all entities’ sprites from the stage and clear the entities list by setting its length to zero.
Next Time…
Wow, we made it! That’s the whole game engine! From this starting point, we could make many simple 2D arcade games without much additional code. In the next tutorial, we’ll use this engine to make an Asteroids-style space shoot-’em-up.
No hay comentarios:
Publicar un comentario