Entity-Component game programming using JRuby and libGDX – part 6

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:

  1. Cause gravity to affect the vertical velocity of anything that is sensitive to gravity — i.e. has the GravitySensitive component,
  2. 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…

2 thoughts on “Entity-Component game programming using JRuby and libGDX – part 6

  1. 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 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s