DEV Community

loading...
Cover image for The Unknown Design Pattern

The Unknown Design Pattern

ovid profile image Ovid ・4 min read

If you care about software, you have probably heard about design patterns. MVC, decorators, factories, etc. But there's an awesome design pattern that’s perfect for modeling complex behaviors, but it’s not well-known outside of the game industry.

We’ve built a narrative sci-fi MMORPG named Tau Station (it’s free to play; check it out!). Like many devs, we were quickly hit with the problem of developing complex items. At first, it looked like we just wanted to create an Item class and inherit from that.

But what if you want to create a military grade combat suit? Yes, it functions as armor. But it can shoot back, so it functions as a weapon. And it can patch up minor wounds, so it also functions as a medkit.

Tau Station Combat Suit

Have you ever wondered how games handle complex objects with multiple behaviors? Many older gaming articles recommended multiple inheritance, despite the well-known problems. Today, games frequently use the Entity-Component-System (ECS). It’s ridiculously powerful and often you find that creating a new, complex class is simply adding a few entries into a database.

The Entity-Component-System

ECS was first developed for the award-winning game Thief: The Dark Project. It’s a powerful way of introducing complex object behavior without complex programming. There are three parts to it:

  • Entities—Guns, Cars, Trees, and so on.
  • Components—Armor, Weapon, Medical, or whatever things might change behavior
  • System—The system that responds to the components of entities

Note that in ECS, you’re dealing with data, not objects. That’s very important to remember.

Now imagine you’re building a game for children where they are photographing animals in the forest. Each “entity” might be an animal, a plant, a trap, or any combination (a Venus Fly Trap could be both a plant and a trap). The core game loop might look like this (code examples in Perl):

my $game = My::Awesome::Game->new(%parameters);
while ( $game->is_running ) {
    $game->get_user_input;
    $game->update_state;
    $game->draw;
}
Enter fullscreen mode Exit fullscreen mode

The update_state method might look like this:

sub update_state($self) {
    $self->hide_from_danger;
    $self->forage_for_food;
    $self->move;
    $self->photograph_available_animals;
}
Enter fullscreen mode Exit fullscreen mode

And finally, the forage_for_food method might look like this:

sub forage_for_food ($self) {
    foreach my $entity ( $self->local_entities ) {
        if ( is_animal($entity) ) {
            $self->search_for_food($entity);
        }

        # we can't use "elsif" here because more than
        # one entity type might apply to the entity
        if ( is_plant($entity) ) {
            $self->react_to_sunlight($entity);
        }

        if ( is_trap($entity) ) {
            $self->trigger_trap($entity);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Entities are merely bundles of data and the helper functions, such as is_animal, is_plant, and is_trap identify if an entity has a particular component.

But we’re a web-based game. Our game is driven by Tau Station citizen’s clicking on links, not on a game loop. So how do we handle this?

ECS for the Web

The Database

Tau Station doesn’t have a game loop, but that’s not really a problem. Instead, we check behavior as needed and the main concern is how we create our entities. For that, we have a simple view which pulls all related data in a single SQL query:

         SELECT me.item_id, me.name, me.mass, ...   -- global
                ar.component_armor_id,   ...        -- armor
                md.component_medical_id, ...        -- med
                wp.component_weapon_id,  ...        -- weapon
           FROM items me
LEFT OUTER JOIN component_armors   ar ON ar.item_id = me.item_id
LEFT OUTER JOIN component_medicals md ON md.item_id = me.item_id
LEFT OUTER JOIN component_weapons  wp ON wp.item_id = me.item_id
          WHERE me.slug = ?
Enter fullscreen mode Exit fullscreen mode

There are a few things to note about the SQL. First, all items have a unique, human-readable “slug” that we use in the URL. The “Combat Suit” slug, for example, is combat-suit. That’s used in the bind parameter (the question mark) in the SQL.

The LEFT OUTER JOIN parts mean the component columns will be NULL if the row is not found.

Finally, for many items, this SQL could be extremely inefficient. However, all of our items are immutable, meaning that we cache the results of the above query and player inventories contain “item instances” which might have more information (such as damage level).

The Code

The code to use the above data is fairly simple. In our strategy, we have global properties such as “name”, “mass”, and so on. That’s in our items table. Each item might have an armor component (component_armors table), a weapon component (component_weapons table), and so on. We have an Item class (it should have been named Entity) and though it’s a class, it only has read-only accessors for fetching item properties, and predicate methods for testing components:

sub is_armor  ($self) { defined $self->component_armor_id  }
sub is_weapon ($self) { defined $self->component_weapon_id }
sub is_med    ($self) { defined $self->component_med_id    }
Enter fullscreen mode Exit fullscreen mode

Because we used LEFT OUTER JOINs in our code, the is_armor predicate method will return false if the component_armor_id is NULL. This means, finally, that if someone wants to equip armor, we can easily check this:

sub equip_armor($self, $item_slug) {

    # logs and throws an exception if item not found
    my $item = $self->resolve_entity($item_slug);

    if ( $item->is_armor ) {
        # equip it
    }
    else {
        # don't equip it
    }
}
Enter fullscreen mode Exit fullscreen mode

It’s a shame ECS isn’t well-known outside of the gaming industry, but after I gave a talk about Tau Station in Amsterdam, one London-based finance company realized it was a perfect way of modeling complex financial instruments. I’ve seen plenty of codebases where ECS would provide tremendous benefit. It’s “composition over inheritance” and it’s data-driven. It’s a huge win.

If you’d like to learn a bit more about Tau Station and how it was built, here’s a presentation I gave in Amsterdam.

Also, the code in the cover image for this post is from the game!

Feel free to leave questions in the comments and I’ll try to answer them.

Discussion (2)

pic
Editor guide
Collapse
rojoca profile image
Rory Casey

Thanks for the refresher on ECS. Behavior as data is so powerful. I remember first hearing about it when the light table editor was being developed. Managed to find the original post: chris-granger.com/2012/12/11/anato...

Collapse
ovid profile image
Ovid Author

That's a great link, Rory. Thanks! Too bad the markdown rendering is broken.

The video it mentions is here (it doesn't show up in the post):