This is the fifth post of the Game Programming Patterns book summary series. The first and second posts are about some selected patterns from the Gang of Four book. The third one covers the Sequencing Patterns. The fourth post is about Behavioural Patterns. This post condenses the Decoupling Patterns section.
Book Summary: Game Programming Patterns
- Patterns Revisited, Part 1
- Patterns Revisited, Part 2
- Sequencing Patterns
- Behavioural Patterns
- Decoupling Patterns
- Optimisation Patterns
These are some of the common ways of keeping code changes contained.
When inheritance is not enough for modelling the functionality reuse of entities, the Component pattern is a good alternative. An entity’s functionality spans multiple domains. Isolate the domains by encapsulating those functions into their own Component classes. The entity is now a mere container of Components.
Keep in mind each entity is now a group of somethings. Communication requires more indirection. In a non-garbage collected language, memory allocation for the entities and their Components becomes more complex. Consider Data Locality pattern to ensure an entity isn’t loaded too far away in the memory from their Components.
- How do entities know which Components are theirs?
- The entity instantiates the Components, so the reference is known. An entity can ensure it has all the Components needed but we have to modify and recompile the entity class’s code to add and remove Components.
- External data handles the entities Component composition. Similar to Unity’s Scene and Prefabs. Authoring entities is faster but we need to create the system to read the configuration data.
- How do the Components communicate?
- The entity controls the communication by providing state data to be modified by Components. This way, the Components stays decoupled but the entity is now full of data that should have been owned by one Component but needed to be read by other Components. The evaluation order of the state data is also important.
- The Components directly calls each other. It’s simple and fast, but the Components becomes coupled not only to the entity but with other Components as well.
- Use the Mediator pattern. Components now communicate by sending messages. The message may contain a reference to the target Component, the function to be called, and probably a few attributes. The entity acts as the mediator of the messages.
Decouple sender with the receiver of a message in terms of time. This enables the receiver to delay executing something if it takes to many resources to do it right now.
Keep in mind that the central messaging queue is a global variable, so the state is shared with multiple senders from different threads. A message can be received or dropped so we can’t be sure and circular messaging may happen.
- Reify the message so it can be stored in a data structure.
- Make a queue. For better memory allocation, implement a circular buffer. The head and tail of the queue move instead of allocating new memory for each new message.
- Aggregate request. Search for similar messages and delete duplicates. For faster search use hash table instead of an array. However, the search still takes resources.
- Make the functions that can be called via an Event Queue as thread-safe as possible because it’s a good chance that the caller and sender are in different threads.
- What goes in the queue? Is it an event broadcast with multiple listeners, or a message for a specific object.
- Who can read the queue?
- Single cast. The Event Queue sends the message to a single object, making it only a feature of another logical system. The complexity of the Event Queue itself can be hidden and the message senders seem to be simply calling a function.
- Broadcast. Every event in the queue is received by all listeners. Wrong broadcast setup? Listeners might get too many events and an event might end up with no listener.
- Work queue. There might be multiple listeners for an event, but every event has its own set of listeners. There are some scheduling algorithms to be chosen for this queue. The common one is round-robin.
- Who can write the queue?
- One writer. This way, the Event Queue is like the Observer pattern + time independence.
- Multiple writers. We might need to send the sender’s reference to the receiver and manually avoid unwanted looped/circular messaging.
- Who owns a message in a non-garbage collected language? What happens if the sender is destroyed?
- The Sender creates the message and then passes ownership of the pointer to the queue.
- Use something like
- The queue handles the memory allocation from the beginning.
The Event Queue is similar to the Observer pattern in terms of having multiple objects triggered by the same event. The difference is in the execution time. The Event Queue is common in a large distributed system where everything is working in their own threads so they are independent in time. We can also use an Event Queue to handle input if we use the State pattern so that duplicate inputs are handled and their order is preserved.
The Service Locator is a good alternative to a Singleton. The pattern consists of a Service interface, a Service Provider class and a Service Locator class. The Service interface, providing an abstract list of virtual methods. The Service Provider implements the interface. The Service Locator class has a static function that returns a reference to the instance of the Service Provider class.
Just like Singleton, the Service Provider instance doesn’t know who will be using its services and when they are called, making it hard to maintain. But unlike Singleton, the Service Provider instance needs to be assigned manually. The Service interface also helps to decouple the service user classes to the Service Provider class, e.g. they don’t have to be re-compiled together every time.
It’s a good idea to implement a Null Service class from the Service interface. A Null Service instance can be returned to turn off a service or to handle the case when a service is called before any Service Provider instance is assigned.
- How services are assigned:
- Outside code instantiate the Provider class and assign it to the Locator. This way, the code can change the instance at runtime, but the Locator is dependant to this outside code.
- Bind at compile time with a preprocessor macro. This approach is good for multiplatform code. The Provider instance is guaranteed to be available but it can’t be changed in runtime.
- Create a system to read an outside config file. Reconfiguration can be done in runtime but assigning the Provider the first time is probably slower because we need to read a file.
- What happens if a Service Provider can’t be located:
- Let the user handle it. Just return
- Halt. If the Service is critical, just stop and make sure this doesn’t happen in the release build.
- Return a Null Service object. The user still has to figure out if the service is not yet available or simply turned off.
- Let the user handle it. Just return
- The scope of the Service:
- Global. It’s a fancy Singleton.
- Restricted to a class. The Service is coupled to a specific class, and the Service Provider instance might be duplicated.
That’s all for the Decoupling Patterns section. Next and the last chapter to be summarised is Optimisation Pattern–that is available here.