Game Engine Design – Component Based Entities 1

Component based entities are becoming increasingly popular. They are a design pattern that can greatly improve the maintainability and flexibility of your game code. This tutorial will describe what component based entities are and how to design them. A Java implementation is provided which can easily be translated to other languages.

Object Hierarchies

Traditionally, game entities used object hierarchies. Each layer added some amount of functionality. At the root was some form of generic object, and as you proceeded down the branches, the classes got increasingly specific. This tree like structure is a logical organization for objects that have a clear supertype-subtype relationship. In games, however, this can be very limiting. Consider the following example:

This object hierarchy is simple enough. The shack, mansion, and bank all fit in well defined categories. What happens if an new object, such as a apartment, needs to be added?

It is not clear what the apartment should inherit from. It has properties of both a a home and a business. The object hierarchy could always be refactored, but depending on its complexity, that may be easier said then done.

Composition

The alternative to object hierarchies is a concept called composition. The “is-a” relationship of inheritance is converted to a “has-a” relationship. The game entities become containers for various components, which determine its state and behavior. In the above example, an apartment may contain a building, home, and business object.

There are several advantages to this design pattern. First, the various components can remain loosely coupled. With a deep object hierarchy, a change near the root can have far reaching and unexpected consequences. In a composition, only the components that directly communicate are affected by changes to one another.

Composition can also make game entities more flexible. Adding additional behavior is as simple as adding a new component to the entity. In the above example, if banks and apartments now require rent, a new rent component can be added to those two components. The rent component could then ask the respective tenant components to pay their rent. This ability to quickly add and remove behavior from entities is a great advantage during development.

A third advantage is that the program becomes easier to understand. It is clear which behaviors and properties an entity has, simply by looking at its list of components.

Communication

The difficulty of a composition is communication. To build more complex behavior, the various components must be able to relay their state and events to each other. One possible way of doing this is by making the game entities store a reference to every type of component. At creation, an entity initializes the components it needs and leaves the remaining ones empty. When one component needs to communicate with another, it looks up the other component’s reference, and performs whatever actions are needed. This design works, but can become awkward when there is a large number of components, or when components are often added and removed from an entity (such as during development).

On the other end of the spectrum is the abstract approach. The entity knows nothing of the components it contains. It views them as abstract objects. Communication is facilitated via messages. When a component’s state changes, a message is created. This message is then forwarded to all of the other components in the entity. They may choose to handle it however they want. The problem with this approach is that there is a non-trivial amount of overhead when sending these messages. For may applications, this overhead is not important, and the messaging system offers a great deal flexibility.

A Hybrid Approach

The implementation described here lies somewhere between the two previous methods. When a component is added to an entity, all of the other components in that entity are informed. It is then their responsibility to keep a reference to the new component or not. Similarly, the components are informed whenever a component is removed from the entity.

This method has both advantages and disadvantages. The entity does not need to know anything about the components contained within, but the messaging overhead occurs only when components are added or removed (most commonly during entity construction). The components also have the option of communicating in whatever manner is most appropriate, whether it be through events, polling, or direct access.

This primarily comes at the cost of memory. Because each component must hold its own references, if multiple components depend on the same object, those references become redundant. Object creation can also become expensive. The number of messages passed at object creation is the square of the number of components.

Reducing the memory overhead would be difficult using this design, but the object creation cost can be reduced. First, the components can be divided into two categories, lightweight and heavyweight. Lightweight components do not need to know about other components. They are strictly data providers. This does not mean that they can not have behaviors of their own, they just do not need to know about the other components in the entity. The complexity of entity construction is now dependent primarily on the number of heavyweight components. It can contain many lightweight components without issue.

The second optimization is allowing for sub-entities. Entities can be added to other entities as if they were components. This creates a tree structure. Events are propagated horizontally and away from the root the tree. This creates an isolation between various components. For example, a physics entity can be created that will hold all of the physics related entities. Externally, other components may only need to be aware of the objects position, but internally there can be many components influencing its behavior. These two design choices will allow for the performance of the entity to be tuned, as well as grant some additional organizational options.

Implementation

Provided below is a minimalist implementation of the described entity system. The code is written in Java, but depends on very little on Java specific functionality.

-Eric

Component Interface:

public interface Component
{
    void componentAdded(Component c);
    void componentRemoved(Component c);
    boolean isLightweight();
}

Entity Class:

public class Entity implements Component
{
    private ArrayList<Component> heavyComps = new ArrayList<Component>();
    private ArrayList<Component> lightComps = new ArrayList<Component>();

    public Entity()
    {
        // do nothing
    }

    public final void addComponent(Component comp)
    {
        // safety check
        if(comp == null)
            Log.error(new NullPointerException("component == null"));

        // inform the heavyweight components that a new component has been added
        for(Component c : heavyComps) c.componentAdded(comp);
        
        if(comp.isLightweight())
        {
            // add the new component to the lightweight components
            lightComps.add(comp);
        }
        else
        {
            // inform the new component of the existing components
            for(Component c : lightComps) comp.componentAdded(c);
            for(Component c : heavyComps) comp.componentAdded(c);
            // add the new component to the heavyweight components
            heavyComps.add(comp);
        }
    }

    public final void removeComponent(Component comp)
    {
        // safety check
        if(comp == null)
            Log.error(new NullPointerException("component == null"));

        if(comp.isLightweight())
        {
            // remove the component from the lightweight components
            lightComps.remove(comp);
        }
        else
        {
            // remove the component from the heavyweight components
            heavyComps.remove(comp);
            // inform the other components that this component is being removed
            for(Component c : lightComps) comp.componentRemoved(c);
            for(Component c : heavyComps) comp.componentRemoved(c);
        }

        // inform this component that all the other componets are no longer
        // accesible
        for(Component c : heavyComps) c.componentRemoved(comp);
    }

    public void componentAdded(Component comp)
    {
        for(Component c : heavyComps) c.componentAdded(comp);
    }

    public void componentRemoved(Component comp)
    {
        for(Component c : heavyComps) c.componentRemoved(comp);
    }

    public final boolean isLightweight()
    {
        return false;
    }
}
  • kaan

    I think there is something wrong with the component added/removed communication.

    for(Component c : lightComps) comp.componentRemoved(c);
    for(Component c : heavyComps) comp.componentRemoved(c);

    should be :

    for(Component c : lightComps) c.componentRemoved(comp);
    for(Component c : heavyComps) c.componentRemoved(comp);