• Followers 0

# Managing Decoupling

General and Gameplay Programming

The only way of staying sane while writing a large complex software system is to regard it as a collection of smaller, simpler systems. And this is only possible if the systems are properly decoupled. Ideally, each system should be completely isolated. The effect system should be the only system manipulating effects and it shouldn't do anything else. It should have its own update() call just for updating effects. No other system should care how the effects are stored in memory or what parts of the update happen on the CPU, SPU or GPU. A new programmer wanting to understand the system should only have to look at the files in the effect_system directory. It should be possible to optimize, rewrite or drop the entire system without affecting any other code. Of course, complete isolation is not possible. If anything interesting is going to happen, different systems will at some point have to talk to one another, whether we like it or not. The main challenge in keeping an engine "healthy" is to keep the systems as decoupled as possible while still allowing the necessary interactions to take place. If a system is properly decoupled, adding features is simple. Want a wind effect in your particle system? Just write it. It's just code. It shouldn't take more than a day. But if you are working in a tightly coupled project, such seemingly simple changes can stretch out into nightmarish day-long debugging marathons. If you ever get the feeling that you would prefer to test an idea out in a simple toy project rather than in "the real engine", that's a clear sign that you have too much coupling. Sometimes, engines start out decoupled, but then as deadlines approach and features are requested that don't fit the well-designed APIs, programmers get tempted to open back doors between systems and introduce couplings that shouldn't really be there. Slowly, through this "coupling creep" the quality of the code deteriorates and the engine becomes less and less pleasant to work with. Still, programmers cannot lock themselves in their ivory towers. "That feature doesn't fit my API," is never an acceptable answer to give a budding artist. Instead, we need to find ways of handling the challenges of coupling without destroying our engines. Here are four quick ideas to begin with:

## 1. Be wary of "frameworks".

By a "framework" I mean any kind of system that requires all your other code to conform to a specific world view. For example, a scripting system that requires you to add a specific set of macro tags to all your class declarations. Other common culprits are:
• Root classes that every object must inherit from
• RTTI/reflection systems
• Serialization systems
• Reference counting systems
Such global systems introduce a coupling across the entire engine. They rudely enforce certain design choices on all subsystems, design choices which might not be appropriate for them. Sometimes the consequences are serious. A badly thought out reference system may prevent subsystems from multithreading. A less than stellar serialization system can make linear loading impossible. Often, the motivation given for such global systems is that they increase maintainability. With a global serialization system, we just have to make changes at a single place. So refactoring is much easier, it is claimed. But in practice, the reverse is often true. After a while, the global system has infested so much of the code base that making any significant change to it is virtually impossible. There are just too many things that would have to be changed, all at the same time. You would be much better off if each system just defined its own save() and load() functions.

## 2. Use high level systems to mediate between low level systems.

Instead of directly coupling low level systems, use a high level system to shuffle data between them. For example, handling footstep sounds might involve the animation system, the sound system and the material system. But none of these systems should know about the others. So instead of directly coupling them, let the gameplay system handle their interactions. Since the gameplay system knows about all three systems, it can poll the animation system for events defined in the animation data, sample the ground material from the material system and then ask the sound system to play the appropriate sound. Make sure that you have a clear separation between this messy gameplay layer, that can poke around in all other systems, and your clean engine code that is isolated and decoupled. Otherwise there is always a risk that the mess propagates downwards and infects your clean systems. In the BitSquid Tech we put the messy stuff either in Lua or in Flow (our visual scripting tool, similar to Unreal's Kismet). The language barrier acts as a firewall, preventing the spread of the messiness.

## 4. Use IDs to refer to external objects.

At some point one of your systems will have to refer to objects belonging to another system. For example, the gameplay layer may have to move an effect around or change its parameters. I find that the most decoupled way of doing that is by using an ID. Let's consider the alternatives.  Effect *, shared_ptr  A direct pointer is no good, because it will become invalid if the target object is deleted and the effect system should have full control over when and how its objects are deleted. A standard shared_ptr won't work for the same reason, it puts the life time of Effect objects out of the control of the effect system.  Weak_ptr, handle  By this I mean some kind of reference-counted, indirect pointer to the object. This is better, but still too strongly coupled for my taste. The indirect pointer will be accessed both by the external system (for dereferencing and changing the reference count) and by the effect system (for deleting the Effect object or moving it in memory). This has the potential for creating threading problems. Also, this construct kind of implies that external systems can dereference and use the Effect whenever they want to. Perhaps the effect system only allows that when its update() loop is not running and want to assert() that. Or perhaps the effect system doesn't want to allow direct access to its objects at all, but instead double buffer all changes. So, in order to allow the effect system to freely reorganize its data and processing in any way it likes, I use IDs to identify objects externally. The IDs are just an integers uniquely identifying an object, that the user can throw away when she is done with them. They don't have to be "released" like a weak_ptr, which removes a point of interaction between the systems. It also means that the IDs are PODs. We can copy and move them freely in memory, juggle them in Lua and DMA them back-and-forth to our heart's content. All of this would be a lot more complicated if we had to keep reference counts. In the system we need a fast way of mapping IDs back to objects. Note that the following is not a fast way!  std::map  But there are a number of possibilities. The simplest is to just use a fixed size array with object pointers:  Object *lookup[MAX_OBJECTS];  If your system has a maximum of 4096 objects, use 12 bits from the key to store an index into this array and the remaining 20 bits as a unique identifier (i.e., to detect the case when the original object has been deleted and a new object has been created at the same index). If you need lots of objects, you can go to a 64 bit ID.
Reprinted with permission from The Bitsquid blog.
0

Followers 0

## User Feedback

You need to be a member in order to leave a review

## Create an account

Register a new account

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0

• 5

0