Extensible components of Trap Labs

This is part 4.4 of the multipart Trap Labs Code Design and Architecture Series

We now circle back to the purpose of a game engine: extensibility. As every game is different so there is not much use of diving too deep into the design of specific game components. What I’ll focus on are the key parts of Trap Labs where I deem needed to be extensible, and provide you with some examples of how to make them extensible. Hopefully this will give you some ideas on how to keep your own game extensible as well.

Extensible in-game elements

I went over the MapElement class in part 2 of the series. The idea is similar for the interactor classes. I figured out some base classes that define common behavior for a category of elements, and derived off specific implementations for individual elements.

Here’s an example of the element hierarchy use in Trap Labs:

Notice that each category of the element is abstract. This is important because you want to fulfill ISP and DIP as much as possible. Elements like Door and Lever does not need to be constantly updated. Their behavior changes only when interacted upon, so there were no reason for them to have an Update() function. On the other hand, traps constantly updates, which meant that they needed Update(). Traps also had behaviors that were common only to themselves, which was why trap has its own base abstract class as well. Lastly notice ElevatorPad is an element that operated somewhere in between, it needed an update function because elevator has timing constraints which required the Update() function, but didn’t use any of the trap behaviors so it simply implements the UpdatableMapElement base class.

Don’t be afraid of extracting classes. As long as two concrete classes are sharing significant behavior then it’s probably a good idea to extract an abstract base class. If you find yourself having trouble extracting a base class even though the two classes do share common behavior, then it usually means you’ve violated SRP. Try and extract private components into their own classes first then try again. If you properly architected your extensible layers this way, you’ll find that your game engine is easily extensible and will have almost no repeating code.

Trap Lab’s event system

Every action that happens in Trap Labs aside from steering physics is a result of the event system. An event consists of a trigger and one (or many) actions. For example, an event in plain English should be something like “If player arrives at grid (3,7), then open door number 7”. Events that are not explicitly described, such as the kill effect of a trap, is also performed as part of the event system i.e. “If player walks ontop of the trap and the trap is live, apply kill effect”, or “If the player is killed by trap number 7, respawn at nearest checkpoint”. A good event system is the key to create modular events that are infinitely extensible.

Actions

Actions are basically just commands in disguise. They are commands that can be queued into the game engine if required, but often runs independently. Certain actions require datum or conditions that can be met during gameplay to triggered before this action can execute. In this sense, many actions are triggered through an ActionFactory rather than queued into the command chain. I won’t go over actions because as long as you know how to use the command pattern this is essentially the same implementation.

Triggers

Triggers are what ultimately drives the event system. Triggers are the condition(s) for actions to happen. Examples of triggers are GAME_START, PLAYER_KILLED, PLAYER_SPAWNED, ARRIVED_AT, DOOR_OPENED, etc.

Trigger definitions are pure entity classes, and I went over this in part 2 if you want to go back and take a look. For example, ARRIVED_AT is a position based trigger, and the usage is something like this:

void SetupArrivedAtTrigger()
{
    PositionTrigger arrivedAt;
    arrivedAt.id = TriggerID::ARRIVED_AT;
    arrivedAt.position = Point(7,7);
}

One way you can evaluate how good your event system is to ask yourself "can events be chained by each other effectively within your system without your code being blown up"? For example, you create two events:

  1. On ARRIVED_AT point (7,7), open door #7
  2. On DOOR_OPENED #7, enable trap #7

This effectively means that event #1 can trigger event #2 even though they are separate events uses different triggers. And a good event system should be able to process events chains like this as long as it wants, even if it is an infinite loop. This was important for Trap Labs because I wanted to give the player great control over the map maker to make really interactive and engaging maps. Of course the caveat of this type of system is players can go nuts and produce glitchy levels. How to prevent that is a topic for another discussion but one quick way to defeat this is to set a trigger limit so that events can’t be infinitely triggered.

So how do we create an event system this powerful and extensible yet not allowing the code to become a complete mess?

So far, Trap Labs has three categories of triggers that is bindable to all in-game elements:

  1. PositionEventTrigger - any in game element that has its behavior depended on the player’s position is checked agents this type of trigger. For examples, most traps are based off this trigger as you can only kill a player if they step on the trap at the trap’s position.
  2. TimerEventTrigger - "Run action after x time”. This type of trigger can be bound to timer based events.
  3. BindableEventTrigger - This is the most interesting trigger as it allows you to bind triggers to anything that doesn’t belong in the above two categories. This is how chaining events is possible. For example, when mapping an explicit event with ENABLE_TRAP as the action, an implicit trigger is internalized as TRAP_ENABLED bound to ENABLE_TRAP action.

Position and timer triggers are constructed through importable elements and events. For example, positions of traps are used to construct the position trigger, and GAME_START events automatically registers with the timer trigger. All *EventTriggers are derived off of Updatable, which is then updated in batches in the server simulation as discussed in the previous subpart. Bindable triggers are more interesting in the sense that they need to be bound to commands and actions in order for something to trigger.

I’ll just state upfront here that I’m not entirely happy with what I came up with here with BindableEventTrigger because it somewhat violates ISP and is a bit messy. But I’ll show it anyways and I’ll update this article as I improve the design. Here’s the BindableEventTrigger UML:

BindableEventTrigger uses multiple inheritance to implement multiple trigger types. The reason for this is that events needs to be able to trigger each other so the “cross-triggering” needs to be aware of each other’s trigger categories. For example, an InputEventTrigger such as CLOSE_DIALOG (happens when user taps on the dialog) can trigger ActionEventTrigger ON_DIALOG_CLOSED. It’s two different categories of trigger that needs to be aware of each other ‘s existence.

The reason I’m not entirely happy with this is that coupling is too tight and there might exist new categories of triggers in the future which means I’ll have to inherit another interface. As of now I have three different interfaces that BindableEventTrigger implements which is sufficient to handle all trigger cases. So until I can come up with something better, this’ll have to stick for now.

However, something that I learn to do right is to group the bindables into elements that is identified by ID. You’ve probably notice by now that I labeled in game elements (such as doors, levers, and traps) with a number. You can use a different value such as a hash value, as long as the value is unique to each item. This will significantly simply the trigger logic. For example, TRAP_ENABLED, DOOR_OPENED, LEVER_FLIPPED_ON, KILLED_BY_TRAP are all identified by this unique number, such that when it’s time to process these triggers you can simply use a switch statement much like this:

switch(triggerID)
{
    ...
    case TriggerID::TRAP_ENABLED:
    case TriggerID::DOOR_OPENED:
    case TriggerID::SWITCH_FLIPPED_ON:
    case TriggerID::KILLED_BY_TRAP:
        AddNumberActionEvents(mapEventData); break;
    ...
}

Then you can index the event by the element number, and when that number is interacted upon you can quickly retrieve this event and creation the appropriate action through the action factory for delivery.

Extensible Physics

Trap Labs uses steering as its physics system. Physics is one of those components that needs to be constantly adjusted to find the right feel for the game. For example, feel of the acceleration, the smoothness of the turns, collisions behaviors etc. So it was vital for me to make physics system that was stable but at the same time easily adjusted.

The problem when I was implementing physics was that I had a hard time locking down its behavior. This is because I was relatively inexperienced in developing physics system and couldn’t establish proper requirements upfront. I initially tried to lock down a few interfaces but soon realized this didn’t work. Every time I had to introduce a new steering concept to enhance the physics such as a stopping force, or external force, the interfaces would change thus defeating their purpose. This problem was compounded by the fact that physics sits relatively low on the component dependency hierarchy, so whenever it changes many things could potentially be affected.

What I ended up doing was I used the pimpl idiom and try my best to keep the steering interface stable. I also tuned physics in batches, which means that whenever I need to adjust steering only steering components would be touched. So even though higher level components may still be affected, it was still fairly difficult to break unrelated components of the game.

If you are already experience in developing physics system, then you can probably establish good interfaces for your game to operate on. If you’re like me then I would simply suggest do what I do and tune it in batches. Please let me know if you have better methods of extending physics.

If you like this article and would like to support me please leave comments and criticism below. You can also find me on twitter @codensuch. Thanks!
Blog Comments powered by Disqus.