Saturday, November 5, 2011

Design Patterns in GHEngine, Part 1

One of the major design goals in our engine is to have as small as a codebase as possible. To do this, we want to re-use as much code as possible. A Golden Hammer in programming is a tool that gets overused and applied to many problems even when it is not a good fit. We have a small set of design tools that are so generic we can use them in all sorts of places. It's a good model if you can have a unified design: avoid duplication by extracting the shared work and making it available to all users. The danger of course is we can end up with the bad definition of golden hammer if we're not careful.

The Controller

class Controller {

virtual update(void);

virtual onActivate(void);

virtual onDeactivate(void);

}

A Controller is something that updates. Our renderer, physics, and input handling are controllers. We can store these controllers in a sorted list for updating, and we can dynamically add or remove controllers on the fly. Instead of having a big update function where we say "do this, then do this, then….", we have a single function that looks like this:

void runFrame() {

for each Controller, call update();

}

Note that Controller does not take any arguments at all. Once you start adding arguments, you start making assumptions about all possible Controller subclasses, and that list can grow to the point where you are passing a lot of stuff that most controllers don't care about. If a Controller needs to access something like the current time, we pass in a time value reference in the constructor.

By having this dynamic list we can avoid iterating over objects that don't need to do anything. If an entity like a player needs an update it can add a Controller to the list. If an entity like a tree does not need to do any work, it can avoid the list. Just touching the memory of each entity in a large world can become a serious performance issue even if no work is being done.

State Machine

class StateMachine {

addTransition(trans, state);

removeTransition(trans, state);

setState(state);

}

class Transition {

virtual activate(void);

virtual deactivate(void);

}

A StateMachine is used to change behavior based on whatever influences we want. Instead of defining each state as a class, we define a state as a set of Transitions into and out of that state. The most common Transition used is a ControllerTransition, which is used to add Controllers to the update list. An example Transition is a MusicTransition, which plays a song while it is active. We can create one of these and add it to the main menu state, then whenever the main menu is shown music plays.

class MusicTransition : public Transition {

activate() { start playing music }

deactivate() { stop playing music }

}

Another use for StateMachine is players. We can use a ControllerTransition to have an input handler active while the player is in the alive state. Then when the player dies we shift him to the dead state, and the ControllerTransition removes the input handler from the update list.

Events

class Message {

identifier messageType

payload messageArguments

}

class MessageListener {

virtual void handleMessage(Message);

}

class EventManager {

addListener(MessageListener, message identifier);

removeListener(MessageListener, message identifier);

broadcastMessage(Message);

}

Events are something that happened, that something else cares about. They are an alternative to having a bunch of different managers that know about the inner workings of some set of objects. We also use them for communicating between threads and for sending information over a network, as they can be stored up and sent later. The real flexibility comes into play when we start data driving message loading, adding listeners that change state to state machines, and allowing out GUI to send loaded messages.

No comments:

Post a Comment