Read it like a book – What to do when Centralized Control does not work? (Part II)

In the previous post of ‘Read it like a book’ series we talked about Centralized Control. In this basic object-oriented concept, high-level classes call methods on lower-level ones. Lower-level classes call events that the high-level classes listened and reacted to. But it is not always the best or even a possible solution. Observers are great, but there are cases where using them leads to classes that cause some nasty problems. In the next two posts of the series we’ll introduce design patterns that’ll help you to handle foreign services (databases, analytics), multi-platform development (analogical, but separate services for Android and iOS, like In-App Purchases or Leaderboards) and such everyday things like Audio or Input Managers.

‘Read it like a book’ series

Part I – Centralized Control
Part II – What to do when Centralized Control does not work? (this)
Part III – Painless way of programming Game Modes and Skills
Part IV – Soon

Content:

A few more information about Centralized Control
Devil’s favor of the Singleton
What did they tell you about Inversion of Control?
Just pass it

A few more information about Centralized Control

1. Remember about the cohesive abstraction. If your UIController operates on too low-level objects, consider creating a class in the middle. If, for example, the UIController turns on and off whole game screens and, on the other hand, keeps track of player’s score value and Text component that displays that value, something smells bad. You can create ScoreUI class with AddPoints(int) interface method, which will do the same, but it will hide it behind a single method call. If you add animation after the score updates, you have to update only that method. Having abstraction in mind somehow forces you to write more methods, and the more named pieces of code, the better. You get greater flexibility and easier codebase maintenance.

2. Cohesive abstraction helps to maintain a beautiful tree of a class hierarchy, but it is quite difficult to keep that abstraction tight. Mainly because in game development we often want to program smaller features fast. This is where the little bugs come from. Sometimes it is tempting to add the score handling to the UIController because it is faster than creating a new class for it. Especially if you just want to add points. If you create a ScoreUI class with only AddPoints() procedure, you may “feel” like you HAVE TO add the whole interface right away. Reset(), GetScore(), Save(), Subtract() and maybe a few more. You know what? You don’t have to. Add the ScoreUI class with the interface you need RIGHT NOW. If you or someone else will need new API later, the right script is already there.

Devil’s favor of the Singleton

Singleton pattern arouses many emotions. It is a class that can only has one instance, and it can be used from every corner of the codebase – it is globally available. In game development it’s so popular, that we can see it almost everywhere. Audio Manager? Singleton. Input Manager? Singleton. Analytics Controller? Singleton. It is not bad on its own, but it is often used very dangerously. Robert Nystrom has already written an excellent piece about it, and I strongly advise you to read it.

You can use it just like that:
AchievementsController.Instance().UnlockAchievement(Achievements.SINGLETON_ARTICLE_READ);

If you do not see why this is bad, read the above article first with a strong emphasis on Why We Regret Using It section. I wish to add one more thing to the What We Can Do Instead part.

Let’s take an Achievement Controller as an example. To avoid coupling everything that unlocks an achievement to a singleton class you can consider events. Did a player jump off from a gigantic cliff? Send PlayerFeltFromHeight(float) event and allow Achievement Controller to decide if the achievement should be unlocked or not. Some of the events may seem like they exist for only that particular reason, so what? You lose nothing and gain decoupled classes. You may consider this solution whenever you need to call something, and a return value is not expected (send analytics data, post leaderboards score, update database and so on.). Of course, it’s not always possible, but we’ll get to it.

A more tricky situation is when you need something FROM the Singleton class.
IAPController.Instance().GetPrice(Items.BAZOOKA);
or
InputController.GetActionDown(Actions.JUMP);
I intentionally left off the ‘Instance()‘ in the second example. Here, the Input Controller does not have an instance; it is a simple static class. It is often a better choice because you do not always need an object at all. In the above cases, we’ve got a few options. But first, let me ask you a question.

What did they tell you about Inversion of Control?

You may have heard of it as a term interchangeable to Dependency Injection, but it is not accurate. Dependency Injection is a one way to achieve Inversion of Control (IoC). Do you remember the scheme from the previous post with a class tree? IoC is when the arrow goes up, instead of going down (still, only one arrow connects these two classes). Instead of letting the high-level class to decide where to call low-level methods, we allow low-level classes to do the opposite. The control is inverted.

I often struggled with an Audio Controller on multi-scene games (so most of them). I usually had only one Audio Controller with ALL the references from the WHOLE game to listen to events and play sounds according to what happened, but it has a few drawbacks. On a scene change, I had to find the proper references, start to listen to them and stop when the scene changed. When I added (or remove) something “vocal” to the scene, I had to update the Audio Controller script to listen and react to that new thing. The class contained mostly one-line methods that only play a sound when something happens. Reading such a class to see what clips play when is a real pain. The standard solution to that is to have Audio Controller as a singleton and call its methods from the classes that need a sound. To be honest, this is a by far more convenient way to solve this. But not every class needs that possibility, so let’s do something else, what will give us an extra feature and keep the architecture neat.

Let’s assume we’ve got an RPG game, where the player can pick a male or female character. With a standard audio singleton, we would need to put some ‘ifs’ here and there to play a sound depending on a character’s sex. Or we could have two derived classes for male and female that differ only with playing sounds. Or we can use Dependency Injection.

Just pass it

Dependency Injection is a pattern when you pass the necessary reference through the constructor or some kind of setter method (Initialize() for instance) to the class that needs it. In the above example, the Player class could have an Initialize(..., AudioController, ...) method. Then, whenever it needs to shout, it calls an Audio Controller‘s method. The huge advantage of that solution is that we don’t have to expect a concrete class. The method used for injecting audio controller can expect an interface. Like this Initialize(..., ICharacterAudio, ...). Here we did two things. We allowed any class that implements this interface to pass, and we limited the audio only to character’s realm (i.e., screams, groans or steps). Now we are allowed to write something like this:

public interface ICharacterAudio
{
    void Scream();
    void Groan();
    void Step();
}
public class PlayerMaleAudio : ICharacterAudio
{
    void Scream() { AudioController.Instance.PlaySound(PLAYER_MALE_SCREAM); }
    void Groan() { AudioController.Instance.PlaySound(PLAYER_MALE_GROAN); }
    void Step() { AudioController.Instance.PlaySound(PLAYER_STEP); } // The same sound for both sexes.
}
public class PlayerFemaleAudio : ICharacterAudio
{
    // Replace all 'MALE' with 'FEMALE'
}

Now, allow to Gameplay Controller/Character Creator Controller to do this:

ICharacterAudio playerAudio;
if (playerSex == Sex.MALE) // playerSex can be taken for example from the CharacteCreator class or game's save file.
{
    playerAudio = new PlayerMaleAudio();
}
else if (playerSex == Sex.FEMALE)
{
    playerAudio = new PlayerFemaleAudio();
}
[...]

playerCharacter.Initialize(..., playerAudio, ...);

Scheme for this architecture:

By doing it that way, we can have a whole bunch of controllers for every sex, race (it doesn’t have to be a human after all) and age. Each one of them is small and very readable. Even more important, we have only one, very liberal, Player class that doesn’t care about the sex or race.

To achieve even greater flexibility, we can add a setter to the ICharacterAudio to the Player so we can change it at runtime (in the Fable game, our character got older and older, and the voice changed).

Now, we have Audio Controller linked only to the classes strictly responsible for audio. Player uses part of the sounds it needs, and we can add new character’s types very quickly.

There is yet another way to deal with it. Perhaps more straightforward and popular. We don’t have to limit the access to sounds only to the classes that definitely need to play audio. We can just assume that every game object should have that possibility. In this case, we can put the reference to the Audio Controller in the base class of every game object (Actor, GameObject or something similar). Every object that is meant to be put on the scene can play a sound through inheritance. We can even go one step further and make Audio Controller a regular class, create an instance of it in some kind of Game Controller and initialize every Game Object with it through dependency injection. But the utility of that last step somehow depends on the engine you’re using. Remember, the goal is to minimize the number of singletons in your code or get rid of them entirely if possible, but don’t be so rigorous. If removing the singleton will cause more problems than profits, just drop that idea and maybe try to limit access to it to as few classes as possible.

Sometimes passing that dependency may be a problem. In bigger codebases, it may be required to pass the injected object through a bunch of classes just to lead it to the class that really needs it. In the next part, we’ll talk about that and about dealing with multi-platform services (Facebook, analytics) and how to decouple the game code from them.

Share these goodies:

Leave a Reply

avatar
  Subscribe  
Notify of