In Part 1 we explored EC nomenclature to set the stage for this part: learning how to work with entities, components and systems using an Entity Manager. (And if you are finding this post via Google or another entry, you can find the index to the full series in Part 1.)
As you’ll recall, your game-world “things” (Entities) are not traditional OO classes. EC Entities are skinny beings that exist merely to have a unique ID. Only after the Components are “attached” to an entity does it attain any personality.
Are you ready for your first entity? OK, here goes:
Don’t look at me with that dubious expression. You’re wondering, where’s the entity? That is it right there: a unique ID (in the form of a UUID in this case, but it could even just be an integer if you wanted). That’s a Tank, or Blue Player Base, or an Alien.
I can imagine you thinking, “All right wise guy, where’s the Entity class? Surely there’s an entity class to hold the ID attribute, right? After all, how can you expect to instantiate instances of the very game objects without an entity class?”
I remind you again, this is not OO, this is EC. In EC there is no need — no need — for an Entity class. Entities exist purely as IDs within the EntityManager.
The Entity Manager
The EntityManager is a class that governs the EC system. It is the fundamental kernel of EC. Its responsibilities include:
- Create and kill entities
- Maintain a list of all known entities
- Map entities to their components
- Retrieve entities’ component functionality on demand
In other words, the EntityManager is a bookkeeping device that serves as the master authority of all entities and their functionality.
[Note: yes, the EntityManager itself is a class, as are Components and Systems. I did promise that EC was a departure from OO, and no, using OO for the foundation framework doesn’t make me a liar.]
Here is a very basic start of our EntityManager:
class EntityManager def create_basic_entity uuid = java.util.UUID.randomUUID().to_s return uuid end end
You do actually instantiate an EntityManager; in fact, you are not limited in how many you instantiate, but you must have one. We only need one for our basic game but the flexibility is there if your game’s complexity demands it.
@entity_manager = EntityManager.new element=@entity_manager.create_basic_entity
Now, let’s agree that for convenience sometimes it’s nice to be able to grab a chunk of entities that are similar. It would be nice if we had a way to “tag” entities in some way, yes? We might want all the entities of a particular genre, or on a certain side. Let’s add this functionality. The tag is simply a free-form text you can use for entity grouping. That’ll come in handy.
class EntityManager def initialize(game) @id = java.util.UUID.randomUUID().to_s @ids_to_tags = Hash.new @tags_to_ids = Hash.new end def create_basic_entity uuid = java.util.UUID.randomUUID().to_s return uuid end def create_tagged_entity(human_readable_tag) raise ArgumentError, "Must specify tag" if human_readable_tag.nil? uuid=create_basic_entity @ids_to_tags[uuid]=human_readable_tag if @tags_to_ids.has_key? human_readable_tag @tags_to_ids[human_readable_tag]<<uuid else @tags_to_ids[human_readable_tag]=[uuid] end return uuid end end
You’ll notice that so far we are creating entities but the EntityManager isn’t managing or maintaining them at all. For this we need to establish a data structure to hold them. It’ll be a hash-of-hashes called @component_stores.
The Component Stores data structure is a hash-of-hashes. Each inner hash is called a component store. In aggregate, component stores.
This is one of those situations where a picture is worth a thousand words:
The outer hash — the hash with the purple braces — is keyed by component class. In this contrived example there are two Components that an entity could have: Gun, and GravitySensitive. As you can see, this outer hash could allow us to discover which entities have a particular component by querying on that component’s class name.
Each component store itself — the hash with the gray braces, circled in the shaded oval — maps the entities to their associated components. Remember, each entity is an ID, so it would be tautological to say “entity IDs” here, and certainly incorrect to refer to them as “entity instances”. (Recall that entities are not classes and therefore are not instantiable. While you are learning EC you can be forgiven for thinking of them as “entity instances” if it helps make the transition, but try to discard that habit as soon as you can. Repeat to yourself the mantra: entities are IDs and nothing else.)
On the other hand, components (which are simple unintelligent bags of data, you’ll recall) are instantiable.
Consulting the picture you can easily see that Entity 12456 has two Gun components (two gun instances) whereas Entity 23456 has but one. The actual gun data itself — shots per second, damage per hit, accuracy, name, type — all live inside the Gun component instances. The Component Stores structure merely links components to their entities.
Expanding the Entity Manager
Let’s expand our EntityManager to utilize a component_stores data structure.
class EntityManager def initialize(game) @id = java.util.UUID.randomUUID().to_s @game = game @ids_to_tags = Hash.new @tags_to_ids = Hash.new @component_stores = Hash.new end # [ Snip... ] def add_component(entity_uuid, component) raise ArgumentError, "UUID and component must be specified" if entity_uuid.nil? || component.nil? # Get the store for this component class. # If it doesn't exist, make it. store = @component_stores[component.class] if store.nil? store = Hash.new @component_stores[component.class]=store end if store.has_key? entity_uuid store[entity_uuid] << component unless store[entity_uuid].include? component else store[entity_uuid] = [component] end end def has_component_of_type(entity_uuid, component_class) raise ArgumentError, "UUID and component class must be specified" if entity_uuid.nil? || component_class.nil? store = @component_stores[component_class] if store.nil? return false # NOBODY has this component type else return store.has_key?(entity_uuid) && store[entity_uuid].size > 0 end end def has_component(entity_uuid, component) raise ArgumentError, "UUID and component must be specified" if entity_uuid.nil? || component.nil? store = @component_stores[component.class] if store.nil? return false # NOBODY has this component type else return store.has_key?(entity_uuid) && store[entity_uuid].include?(component) end end
We have expanded our EntityManager functionality. At creation time we instantiate a blank component_stores instance variable. We can now link a component with an entity. Furthermore, we can now query whether a particular entity has a specific component or a component of a certain type (e.g. Gun).
It is worth noting that this data structure is extremely fast to work with. The lookups are speedy and inexpensive. The entity keys are small, being mere ID values, and the components are agreeably small too since they are simple data.
Concluding this post, consider this parting thought: the EntityManager data structure represents the entirety of your game state. Let that sink in for a moment, and marry that with your knowledge of Ruby’s built-in data serialization. Saving and loading game state just became trivial, a topic that I will address in a later post of this series…
In the next installment we’ll learn about components…