Introduction
Now that we have laid the entity-component foundation and introduced some necessary libGDX concepts, we can finally get around to putting together a little game. Let’s make a “Lunar Lander” type game to illustrate all the concepts we’ve learned so far.
Remember that the source code for this Entity-Component Framework and the game we’re writing is all available at Github. The Github version is, of course, the “final version” that includes features I might not have addressed so far in the blog series, but if you’re eager to jump ahead…
Entities and Components
For this exercise let’s begin by defining our entities and some of their relevant components.
What are we going to need for this game?
- The lunar lander module
- A platform for it to try to land on
- Ground to collide with
That seems like a fair assessment of our initial entity needs. Now, moving on to components. What are some of the aspects / behaviors / features that we should provide to our entities?
For the lander, here’s a short list of features we’ll address today:
- It has a spatial position and velocity
- It has an engine with thrust
- It has fuel to burn
- It’s sensitive to gravity
- It moves
- It’s renderable
- It responds to user input
Here’s a short list of features we’ll address today for the landing pad:
- It has a spatial position
- It’s renderable
And last, a short list for the ground:
- It has a spatial position
- It’s renderable
There are additional features we’ll address in the next installment. 🙂
Let’s whip up some components to address each of those features in turn…
The SpatialState Component
SpatialState contains position and velocity.
class SpatialState < Component attr_accessor :x, :y, :dx, :dy def initialize(x_pos, y_pos, x_velo, y_velo) super() @x = x_pos @y = y_pos @dx = x_velo @dy = y_velo end end
The Engine Component
The Engine component just knows its thrust, and whether it’s on.
class Engine < Component attr_accessor :thrust attr_accessor :on def initialize(thrust) super() @thrust=thrust @on=false end end
The Fuel Component
The Fuel component only knows an amount-remaining.
class Fuel < Component attr_accessor :remaining def initialize(remaining) super() @remaining=remaining end def burn(qty) @remaining -= qty @remaining = 0 if @remaining < 0 end end
The GravitySensitive Component
GravitySensitive is a bit unusual in that it has no associated data; its mere presence causes “behavior”.
class GravitySensitive < Component end
The Motion Component
Motion is a lot like GravitySensitive: no associated data, but a behavior that is “activated” just by being attached to an entity.
class Motion < Component end
The Renderable Component
Renderable is the most complex of today’s components. It knows how to read an image file from disk and turn it into a libGDX Texture, and it remembers things about that image like rotation and scale.
class Renderable < Component extend Forwardable def_delegators :@image, :width, :height # Its image knows the dimensions. attr_accessor :image, :image_fn, :scale, :rotation def initialize(image_fn, scale, rotation) super() @image_fn = image_fn @image = Texture.new(Gdx.files.internal(image_fn)) @scale = scale @rotation = rotation end def rotate(amount) @rotation += amount end end
The PlayerInput Component
This component permits an entity to respond when the user provides input. Input could be in the form of mouse clicks, a joystick, or key presses. Our lander game is simple and will just respond to keyboard input.
This component stores which keys it should respond to. The rationale here is that one entity could be Player 1’s lander and respond to A-S-D; another lander could be for Player 2 and respond to J-K-L.
class PlayerInput < Component attr_reader :responsive_keys def initialize(keys) super() @responsive_keys=keys end end
E-C gives us some very elegant features, one of which is how component assignment drives behavior. For example, here’s something interesting that might not have occurred to you: later we could make an ArtificialIntelligenceInput component and use it to create a “demo” or “kiosk” mode for our game, or even a computer-controlled Player 2. Our Input System could be modified to recognize entities with this ArtificialIntelligenceInput component and make them behave accordingly.
Putting Them Together
Now we can actually tie our entities and components together. This is done in the show() method of the PlayingState class, which you’ll recall is a libGDX “screen” for the actual playing state of the game.
p1_lander = @entity_manager.create_tagged_entity('p1_lander') @entity_manager.add_component p1_lander, SpatialState.new(580, 430, 0, 0) @entity_manager.add_component p1_lander, Engine.new(0.01) @entity_manager.add_component p1_lander, Fuel.new(250) @entity_manager.add_component p1_lander, Renderable.new(RELATIVE_ROOT + "res/images/lander.png", 1.0, 0) @entity_manager.add_component p1_lander, GravitySensitive.new @entity_manager.add_component p1_lander, Motion.new @entity_manager.add_component p1_lander, PlayerInput.new([Input::Keys::A, Input::Keys::S, Input::Keys::D]) platform = @entity_manager.create_tagged_entity('platform') @entity_manager.add_component platform, SpatialState.new(50, 118, 0, 0) @entity_manager.add_component platform, Renderable.new(RELATIVE_ROOT + "res/images/shelf.png", 1.0, 0) ground = @entity_manager.create_tagged_entity('ground') @entity_manager.add_component ground, SpatialState.new(0, -140, 0, 0) @entity_manager.add_component ground, Renderable.new(RELATIVE_ROOT + "res/images/ground.png", 1.0, 0)
This is pretty straightforward. We are leveraging the EntityManager to create entities then “attach” components to them. If you need a quick refresher on the entity manager and its component storage, please review part 2 of the blog series.
Here’s an illustration of how adaptable E-C is: would you like a Player 2 lander? It’s easy. Maybe his lander has twice the thrust but less fuel:
p2 = @entity_manager.create_tagged_entity('p2_lander') @entity_manager.add_component p2, SpatialState.new(350, 400, 0, 0) @entity_manager.add_component p2, Engine.new(0.02) @entity_manager.add_component p2, Fuel.new(200) @entity_manager.add_component p2, Renderable.new(RELATIVE_ROOT + "res/images/lander.png", 1.0, 0) @entity_manager.add_component p2, GravitySensitive.new @entity_manager.add_component p2, Motion.new @entity_manager.add_component p2, PlayerInput.new([Input::Keys::J, Input::Keys::K, Input::Keys::L])
Systems to Drive Things
Now we have our entities defined and some data attached to them, but nothing could happen yet. This is because we don’t have any systems defined; systems the motors that actually cause things to occur. As you’ll recall, each system is activated once per game tick — or in our case, roughly 60 times per second.
What are some of the systems we’ll need? Let’s begin with these:
- A physics system to make gravity and motion work
- An engine system to make the lander’s engine work
- A rendering system to actually draw the graphics
- An input system to read input (from the player, the AI, whatever) and make things happen
The Physics System
Each game tick our physics system needs to do two things:
- Cause gravity to affect the vertical velocity of anything that is sensitive to gravity — i.e. has the GravitySensitive component,
- Move anything that is movable and positionable
As mentioned previously, this is a system which reaches out to multiple components to retrieve all the data it needs to perform its work:
class Physics < System # This constant could conceivably live in the gravity component... ACCELERATION = 0.005 # m/s^2 DOWN = Math.cos(Math::PI) def process_one_game_tick(delta, entity_mgr) gravity_entities = entity_mgr.get_all_entities_with_component_of_type(GravitySensitive) gravity_entities.each do |e| spatial_component = entity_mgr.get_component_of_type(e, SpatialState) # vertical speed will feel gravity's effect spatial_component.dy += ACCELERATION * delta end moving_entities = entity_mgr.get_all_entities_with_component_of_type(Motion) moving_entities.each do |e| spatial_component = entity_mgr.get_component_of_type(e, SpatialState) # move horizontally according to dx amount = 0.01 * delta * spatial_component.dx spatial_component.x += (amount) # now fall according to dy amount = 0.01 * delta * spatial_component.dy spatial_component.y += (amount * DOWN) end end end
The Engine System
The engine system looks for things with an engine (i.e. with an Engine component) and fuel (i.e. a Fuel component). If the engine is on and fuel remains, a thrust vector is calculated according to the thing’s rotation (from the Renderable component); the X and Y velocities are altered accordingly.
Note that this system doesn’t actually move anything. It just alters dX and dY; someone else is responsible for doing something with dX and dY.
class EngineSystem < System def process_one_game_tick(delta, entity_mgr) engine_entities = entity_mgr.get_all_entities_with_component_of_type(Engine) engine_entities.each do |entity| engine_component = entity_mgr.get_component_of_type(entity, Engine) fuel_component = entity_mgr.get_component_of_type(entity, Fuel) if engine_component.on && fuel_component.remaining > 0 location_component = entity_mgr.get_component_of_type(entity, SpatialState) renderable_component = entity_mgr.get_component_of_type(entity, Renderable) amount = engine_component.thrust*delta fuel_component.burn(amount) current_rotation = renderable_component.rotation x_vector = -amount * Math.sin(current_rotation * Math::PI / 180.0); y_vector = -amount * Math.cos(current_rotation * Math::PI / 180.0); location_component.dy += y_vector location_component.dx += x_vector engine_component.on=false end end end end
The Rendering System
This system only cares about entities that are renderable and have position. It utilizes the TextureBatch that was handed to it to batch up the drawn images.
For a little fun we’ll also make an on-screen text label telling how much fuel remains. Yet again, a system is allowed to reach out to any component it needs to do its work.
class RenderingSystem < System def process_one_game_tick(entity_mgr, camera, batch, font) entities = entity_mgr.get_all_entities_with_components_of_type([Renderable, SpatialState]) entities.each do |e| loc_comp = entity_mgr.get_component_of_type(e, SpatialState) render_comp = entity_mgr.get_component_of_type(e, Renderable) batch.draw(render_comp.image, loc_comp.x, loc_comp.y, render_comp.width/2, render_comp.height/2, render_comp.width, render_comp.height, 1.0, 1.0, render_comp.rotation, 0, 0, render_comp.width, render_comp.height, false, false ) end entities = entity_mgr.get_all_entities_with_component_of_type(Fuel) entities.each_with_index do |e, index| fuel_component = entity_mgr.get_component_of_type(e, Fuel) font.draw(batch, "Fuel remaining #{sprintf "%.1f" % fuel_component.remaining}", 8, 90); end end end
The Input System
Finally, input. Every game tick we look for what keys are pressed and then examine each “inputtable” entity — i.e. each entity with PlayerInput — to see if that entity responds to the pressed key. If so, we take the appropriate action such as activating the engine or turning the lander.
My player 1 keys can’t affect player 2’s lander, nor can I rotate or move the landing pad.
class InputSystem < System # Presumably these would be DRYed into a config file... P1_KEY_THRUST = Input::Keys::S P1_KEY_ROTL = Input::Keys::A P1_KEY_ROTR = Input::Keys::D P2_KEY_THRUST = Input::Keys::K P2_KEY_ROTL = Input::Keys::J P2_KEY_ROTR = Input::Keys::L def process_one_game_tick(delta, entity_mgr) inputtable_entities = entity_mgr.get_all_entities_with_component_of_type(PlayerInput) inputtable_entities.each do |entity| input_component = entity_mgr.get_component_of_type(entity, PlayerInput) if Gdx.input.isKeyPressed(P1_KEY_THRUST) && input_component.responsive_keys.include?(P1_KEY_THRUST) && entity_mgr.has_component_of_type(entity, Engine) engine_component=entity_mgr.get_component_of_type(entity, Engine) engine_component.on=true end if Gdx.input.isKeyPressed(P1_KEY_ROTL) && input_component.responsive_keys.include?(P1_KEY_ROTL) renderable_component=entity_mgr.get_component_of_type(entity, Renderable) renderable_component.rotate(delta * 0.1) end if Gdx.input.isKeyPressed(P1_KEY_ROTR) && input_component.responsive_keys.include?(P1_KEY_ROTR) renderable_component=entity_mgr.get_component_of_type(entity, Renderable) renderable_component.rotate(delta * -0.1) end end end end
Conclustion
These are the basic components and systems to get a decently workable game: the lander will fly around, fuel is burned, things are drawn where they need to be, etc. We haven’t addressed collisions yet, so we don’t know if the lander has touched the landing pad or crashed into the ground. These and other features will be addressed in the next installment…
It would be cool to have a post mortem discussion the potential pitfalls of the entity-component pattern as well as a summarization of the benefits.
Before making my suggestion I have also found your blog posts very entertaining. I also really like the general concepts presented.
I keep looking at your examples and I keep thinking that if an Entity was a simple instance which was the tuple {uuid, entity_manager} then you could leverage a more fluid API for getting components from entities. Here is a napkin example of part of Physics:
Gravity.components_with(SpatialState).each do |spatial|
spatial.dy += ACCELERATION * delta
end
Behind the scenes it would just be retrieving the from the entity manager the entity was created with. At worst there is one level of indirection. On the other hand this could also be an Arel-like API to compose a more minimal set (and then use a more sophisticated engine for maintaining components — like neo4j) which could end up doing a lot less searching. I might be willing to spike this idea as well although I am pretty busy 🙂