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

Introduction

In part 1 and part 2 we introduced nomenclature and explored the construction of Entities using the EntityManager. Now it’s time to breathe life into your entities with components (for data) and systems (to act upon the data). In this post we’ll discuss components and in the subsequent post, systems.

The Component

Recall that we never store any data or features of any kind inside an entity. (Actually the point is moot: we cannot store anything in an entity since entities aren’t even object instances, they are merely numeric IDs managed in the EntityManager.) This is where we truly depart from OO as a design paradigm. Attributes, aspects, data, behaviors are no longer embedded in the objects but are “bolted on” by attaching component instances to the entities.

Here’s a very basic start to a Component base class from which all components will derive:

class Component
  attr_reader :id

  def initialize
    @id = java.util.UUID.randomUUID().to_s
  end

  def to_s
    "Component {#{id}: #{self.class.name}}"
  end
end

Every component gets a unique ID for tracking and logging purposes; it is a very basic class.

Defining Components

Philosophically speaking, how do we define components? What should they be?

A component should encapsulate the data necessary to drive a behavior, an aspect, or a feature of the entity who owns it. Remember, we’re striving for composition over inheritance, so ask yourself: what are the things that the entity in question is composed of?

In this blog post series we are working our way toward a “Lunar Lander” type game. So let’s begin by thinking about our lander entity. What are some of the behaviors or features it should have? What is a lander composed of?

  • It should be sensitive to gravity
  • It should have an engine
  • It needs fuel to burn in the engine
  • It needs an X-Y position
  • It needs a graphic to render
  • It needs to have velocity

This list, while not exhaustive, embraces a number of good features that indeed describe a lunar lander. One very nice thing about Entity-Component systems is their flexibility and adaptability to new information. Game programming via OOP is, by contrast, terribly unforgiving of poor initial planning, and our experience with this pain pressures us to do exhaustive, comprehensive architecture at the outset of an OOP-based project. Refreshingly, E-C is delightfully versatile in this regard, and as you gain experience with E-C you’ll feel much less pressure to plan everything in advance.

Your First Component

Let’s pick an easy one to start with: let’s make a SpatialState component:

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

This is one way to do it; I’ve put the X and Y position variables in this component’s data, and I’ve included the X and Y velocities for good measure. Could those have been in a separate component? Absolutely. I could have created a Position component and a Velocity component, but I think this single-component grouping is fine for now. That will be an easy refactor later if our needs change.

I’ll point out again that the SpatialState component is thoroughly unintelligent, as all components must be. It is a pure data store, with not a bit of algorithmic knowledge. Just an initializer and public accessors for the contained data. Making the getter/setter access public at this point is fine, as the data need to be publicly readable by any and all Systems who desire access. If later circumstances require, we can lock the access permissions down.

Speaking of “any and all systems who desire access”…what systems will want access? We don’t know yet, and we don’t care. That’s one of the elegant things about E-C design: we’re free to architect the data components now without over-thinking their ramifications. Data is just data, and later we’ll define Systems to act on the data.

Another Component

Let’s add another component. We discussed above that one of the “features” of a lunar lander is that it has fuel to burn. Let’s add a component for that:

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

That’s a fairly straightforward component. Of interest is the burn() method. I have emphasized that components have no logic, no brains. This burn() method flirts with imbuing the component with some sort of intelligence, but no: burn() is nothing more than a rudimentary setter. This usage in no way violates the spirit of E-C.

One more component. Another discussed feature of a lunar lander is that it’s sensitive to gravity. Let’s make a component for that:

class GravitySensitive < Component
end

Components don’t come much simpler that that. You’ll note, of course, that there is no data. There is none because none is required. How will this component work, then? The mere presence of this component with our lunar lander entity will attach this component’s behavior to the lander. This will become clearer in the next post when I discuss Systems; for now put your skepticisms aside and you’ll soon see how these components are combined with the entity to achieve functionality.

A parting thought: it’s conceivable that our Lunar Lander game will be set on different “worlds”, each with a different G (gravity constant). Maybe that will end up living in the GravitySensitive component, maybe it will end up somewhere else. E-C is supremely adaptable and we can delay that decision until we need to make it.

A Thought Experiment

Let’s take a detour. Put the Lunar Lander game aside and think more generally. Let’s think in terms of, say, a wargame with a 2-D battlefield map of some kind.

Our first iteration of the game design stipulates a land-based game: tanks, armies, howitzers, minefields, cities, ammo dumps, etc. (These are all good candidates for entities, by the way.) Our first component might be MapPosition:

class WorldPosition < Component
attr_accessor :latitude, :longitude

  def initialize(lat, lon)
    super()
    @latitude = lat
    @longitude = lon
  end
end

Pretty straightforward.

tank_entity = @entity_manager.create_tagged_entity(‘blue_side’)
@entity_manager.add_component(tank_entity, WorldPosition.new(45.123,–123.234))

Weeks of development go by and things are going swimmingly until a decision is made: this wargame is set in World War One, we need airplanes!

Do we need to perform drastic, time-consuming refactors to accommodate this change? No! We need a new Component. Let’s call it Flight:

class Flight < Component
  attr_accessor :altitude
  def initialize(alt)
    super()
    @altitude=alt
  end
end

Now we can make a number of airplane entities in the EntityManager, and these entities will get the Flight component. In very simplistic terms this might look like this:

tank_entity = @entity_manager.create_tagged_entity(‘blue_side’)
@entity_manager.add_component(tank_entity, WorldPosition.new(45.123,–123.234))

air_entity = @entity_manager.create_tagged_entity(‘red_side’)
@entity_manager.add_component(air_entity, WorldPosition.new(47.5,–121.3))
@entity_manager.add_component(air_entity, Flight.new(3000))

Although something of a contrived example, this accurately demonstrates the flexibility of E-C design. It was not necessary to massively refactor a carefully-crafted yet eggshell-fragile class hierarchy to accommodate airplanes. We only needed to add a new component and some new systems to act on entities possessing this flight component. (I’ll cover systems in the next post.)

Imagine yourself in the OO world for a moment. You’ve just emerged, victorious, from your weekend-long refactoring hack session, and you’re proud to show off your new Airplane class. The manager lays his hand on your shoulder and breaks the bad news to you: the game needs ships now, too. You must go back to the drawing board and embark upon another perilous hack session to accommodate these objects that can go on water but not land. And what about submarines that can go under the water? Hopefully you anticipated those…

Coming Up

In the next installment we’ll learn about systems…

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

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