Stop getting screwed by the video game engine or framework

This is part 0 of the multipart code design and architecture series based off my game Trap Labs.

I wrote an extensive article on effective unit testing in Unity which implicitly covered this topic. I recommend you give that a read especially if you want to see some code as the entire project files and tests are provided unlicensed.

When I use a framework the first thing that comes to my mind is this

What I’m describing are two problems.

Firstly, the use of any framework immediately puts restrictions on the architecture of your code. Regardless the efforts you put into decoupling from the framework it will still put implicit restrictions the code's architecture, as you’ll about to see. This means that if the framework changes your game changes. If one day the folks at Unity decides you to screw you over then guess what there’s nothing you can do about it.

This is usually a non-issue because the nice framework developers know that in order keep us happy they have to keep their public interfaces more or less the same. And if they need to overhaul a component they’ll provide us with a temporary wrapper or adapter and warn us that, "Hey this function has since been deprecated. Please try and use this new interface going forward. Thanks man!” @!&@^#&!!!!

The second problem is the one worth noting and much more severe. The consequence of coupling your code to frameworks makes your software extremely difficult to test.

I’m not talking about asking your friends to play the level that you made over and over until he/she finds a game breaking bug and you pull your hair out trying to fix the issue. I’m talking about automated unit tests, you know the one you probably didn’t write?

Remember how I said my articles will be test heavy? If practices like TDD is new to you, I would recommend checking out my code to get an idea what test-first is about. There are also a ton of great tutorials on the Internet that covers unit testing. In simple terms, unit tests allow you automate testing of all logic in your code in matter of seconds. Every time you make a change to your code you run the tests to verify correctness and catch regressions immediately.

Just to give you some perspective, the entire Trap Labs project has over 400 units tests, and counting. The core engine has about 130 test that runs under a second every time I compile. As a result, I don’t spend a lot of time debugging due to the high degree of correctness, and I focus on design and developing features. Writing unit tests and test-first keeps code defects low and development velocity high. To do that with a video game you first need to decouple from the framework/game engine.

Decoupling from the framework

So what is coupling? Coupling is the amount of dependency one piece of code has on another. Any form of dependency is a form of coupling such as reference, inheritance, polymorphism, generics etc. The more dependencies you have the tighter the coupling.

For example you might write a Cannon class that subclasses cocos2d::Node. This is considered tight coupling due to inheritance. On the other hand decoupled or loosely coupled modules mean that they are designed in such a way that they do not directly reference each other. They either do so through composition or an intermediary like a host class or interface:

If there is anything in software design and architecture you should take away, it is that you should always aim for lossely coupled class/modules. Loosely coupled modules implies that there is a clear separation of responsibilities between the modules. Furthermore, it signficantly reduces the ability of breaking a module to do a change from a another module. Tight coupling is a common problem among amateur developers. Have you experienced a bug where you made a change and it broke something completely unrelated? This is most likely the cause.

Regardless of what effort you put into decoupling your modules, you will always end up with some form of coupling because your modules need to communicate with each other somehow. For good architectures you want to ensure your couplings are very lean and clean. This usually means you need to clearly establish boundaries of the modules upfront and define where coupling should exist. I would recommend doing so at the planning stage of your classes. Common boundaries are things like databases, UI, controls, OS and other hardware devices. For video games for example you might want to separate score keeping and gameplay such that the scoring module observes the gameplay and applys scoring without the gameplay affecting the scoring logic. Clearly esablishing boundaries within your game will help separate responsibilities and keep modules cohesive.

Decoupling from the framework not only makes your code much easier to test, it makes your software somewhat framework agnostic. You can design your game such that if you decided to switch engines, you can do so in a short time, and not breaking anything. However, I need to stress that it’s generally not pragmatic to completely decouple from the framework. If you really decide to switch frameworks in the middle of a project it’s going to require some man/woman power to do the switch properly. The amount of coupling you should aim for is what is sufficient to effectively test your game.

The annoying part of decoupling video game framework is that they are multi-domain. For example, decoupling a database is easy because database is data query. You can decouple your business logic from the database such that switching between a SQL database or a file with minimal effort. Video game engines however touch multiple domains such audio, video, physics, networking, file IO, and more. Sure they are convenient features, but super restrictive on the game's architecture. If you tightly couple to Unity you are pretty much stuck with Unity forever. 50% of marriages end in divorces, so coupling long term is generally a bad idea. My jokes are awesome.

Allow me to reiterate something I said in the introduction of this series. Decoupling framework does somewhat defeats the purpose of using a framework, as you’ll see some modules needs to be abstracted or need custom implementation that are already provided by the framework. Developers that are making small games will probably not see the benefits. But I assure you that the benefits will become clear for projects that have any scale.

Oh you’re still here? Good I like your style. Let’s continue.

Write as much code as possible without using the framework first

Let’s say you are making a shoot’em up game like space invaders, with very simple physics (fixed step in x direction based on key pressed). How much do you think the code can be written with without touching the game engine? The correct answer is: almost all of it. OMG What are you mad? How do I verify the plane is flying correctly? You do that through unit tests.

Just think about it for a moment. Do the visuals affect how spaceship moves?

“Of course! If I press the left arrow key the cannon will move 10 pixels to the left,” you said.

“So how are you going to verify that the cannon moved 10 pixels to the left?” I asked.

“I’ll build and run my game and test it out,”

“So you are gonna manually test every little feature you add to the game? You are going to compile, link, run, go through a bunch of menu to play the game, then see if the feature works?”

“Isn’t that what QA and play testers are for?”

“No that’s what slaves are for,” I replied.

So how do we correctly and very simply verify whether the cannon correctly moved? We write a freaking unit test:

TEST_CASE("Cannon moving left or right should move by 10 pixels")
{
    Cannon cannon;
    REQUIRE(cannon.GetXPosition() == Approx(0.0f));
    cannon.MoveLeft();
    REQUIRE(cannon.GetXPosition() == Approx(10.0f));
    cannon.MoveRight();
    REQUIRE(cannon.GetXPosition() == Approx(0.0f));
}

The syntax is based on Catch unit testing framework in C++. Since we got a good idea of how the cannon behaves, we can write the Cannon class to make this test pass:

class Cannon
{
public:
    float GetXPosition();
    void MoveLeft();
    void MoveRight();
    ...
private:
    float xPosition;
    ...
}

I only included the definition because the implementation should be trivial. Compile this as a separate library and link against Catch (or your favorite test framework) and run. And voila you just verified correctness of your movement logic.

“Oh that’s cool, but this is useless I can’t play a library!” you said.

“Sure let’s plug this into our game engine, Cocos2d-x, and see what happens.”

class Cannon2D : public cocos2d::Sprite
{
public:
    Cannon2D();
    static Cannon2D* create();
    void update(float deltaT)
    {
        this->setPositionX(m_cannon.GetXPosition());
    }

    bool OnKeyReleased(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* e)
    {
        switch(keyCode)
        {
        case EventKeyBoard::KeyCode::KEY_ARROW_LEFT:
            m_cannon->MoveLeft();
            break;
        case EventKeyBoard::KeyCode::KEY_ARROW_RIGHT:
            m_cannon->MoveRight();
            break;
        }
    }
    ...
private:
    Cannon m_cannon;
    ...
}

Notice the dependency on Cannon is now broken from Cocos through Cannon2D. Cannon is now unit testable and the dependency is inverted. Aside from the construction fluff required from inheriting Cocos’ Sprite class, the update and key handler functions are now trivial. You can eyeball the code to verify correctness without the need to run the game.

This is fundamentally how you would write a game logic completely decoupled from the framework/game engine. You want to keep the logic testable in its own component, and the part coupled to the framework class as tiny as possible. You have to design your modules such that framework now is more heavily depend on your library, rather than the other way around.

“Wow this is pretty neat. But I have two questions. One, there’s a switch statement handling the key presses in the Cannon2D class, isn’t that untested logic? I’d still have to run the game to verify my keys are hooked up correctly. Two, what if my cannon played fancy animation every time it moved? How do I verify that in unit tests?”

“Great questions young grasshopper. To test logic that is tightly coupled to the framework, you need to go a bit further to create an interface to help you…” I said.

Before I move on to the next section I would also argue that specifically in this case the switch statements are simple enough to verify by inspection, and writing a test would be overkill. But I want to show you that EVERYTHING can be decoupled if you want them to be testable. How aggressively you want to verify correctness is for you to decide for your project.

Video Game Engine Public Interfaces

Video game engines often provide a single accessor object to access all of its features. In Unity its GameObject, in Cocos2d-x its cocos2d::Node (and it’s derivatives).

In Cocos it’s a complete mess. Almost every feature from visual to physics and controls are all accessible from this node. Some see this as convenience, I see this as disaster waiting to happen. Unity does this better as it uses a generic to separate components into their own classes, but it does not eliminate the dependency from MonoBehavior. Thus the key to effective decoupling from the framework is to decouple from the engine's accessor functions.

Decoupling Video Game Engines

This is another reminder that I wrote an extensive article on decoupling Unity. For this section I’ll continue to use Cocos2d-x and you’ll see that it’s a bit different from Unity.

It wasn’t pragmatic break the Node class into components like what I did with Unity. The reason is because once I subclass a Node or Sprite I immediately inherit the interface anyways, so it doesn’t help with the dependency problem.

The solution is actually quite simple. For Trap Labs I created two types of mediators:

  1. NodeMediator – an protocol class/interface that allows me to wraps around the implementation of Cocos’ Node (things like position, size, scale belong in the interface)
  2. VisualMediator – created specifically for handling animation states of game elements

The mediator pattern is a class that help two different classes communicate with each other without the two classes knowing anything about each other.

I’ll talk about VisualMediator in more detail in Part 4 of the series as I go over Trap Labs’ game engine. For now, NodeMediator will be sufficient to get my point across.

The NodeMediator class looks something like this:

class NodeMediator
{
public:
    virtual TLFPoint GetPosition() = 0;
    virtual void SetPosition(const TLFPoint &point) = 0;
    virtual TLFSize GetSize() = 0;
    virtual float GetScale() = 0;
    virtual void KeyDown(TLKeyCode keyCode) = 0;
    virtual void KeyUp(TLKeyCode keyCode) = 0;
    ...
}

NodeMediator is a protocol class, or more commonly referred to as an interface in C# or Java’s terminology. It’s purely virtual. You’ll also notice that there are two data structures TLFPoint and TLFSize, and an enum TLKeyCode. They are custom implementations of Point and Size classes created for the sake of testability. This is one if the pitfalls of decoupling from a game engine. Sometimes you need to recreate certain data structures from the framework in order to test your game because these data structures are locked in the game engine’s namespace. If the engine you are using comes with header only includes or linkable libraries that contain these classes then you don’t need to rewrite them. In my case they were simple enough to recreate.

Let’s go back to the space invaders example. Knowing we can use a mediator, we can now create a testable Cannon2D:

TEST_CASE("Cannon2D should move right/left on keypress")
{
    Cannon cannon;
    MockNodeMediator mockMediator;
    Cannon2D cannon2D(&cannon, &mockMediator);
    REQUIRE(mockMediator.GetPosition().Approx(TLFPoint(0.0f, 0.0f)));
    cannon2D.OnKeyDown(TLKeyCode::LEFT);
    cannon2D.Update();
    REQUIRE(mockMediator.GetPosition().Approx(TLFPoint(10.0f, 0.0f)));
    cannon2D.OnKeyDown(TLKeyCode::RIGHT);
    cannon2D.Update();
    REQUIRE(mockMediator.GetPosition().Approx(TLFPoint(0.0f, 0.0f)));
}

class Cannon2D
{
public:
    Cannon2D(Cannon *cannon, NodeMediator *mediator)
        : m_cannon(cannon), m_mediator(mediator)...;
    void Update(float deltaT)
    {
        m_mediator->SetPosition(TLFPoint(m_cannon->GetXPosition(), 0));
    }
    void KeyDown(TLKeyCode keyCode)
    {
        switch(keyCode)
        {
        case EventKeyBoard::KeyCode::KEY_ARROW_LEFT:
            m_cannon->MoveLeft();
            break;
        case EventKeyBoard::KeyCode::KEY_ARROW_RIGHT:
            m_cannon->MoveRight();
            break;
        }
    }
    ...
private:
    Cannon *m_cannon;
    NodeMediator *m_mediator;

}

Notice this class now looks a lot like the previous Cannon2D, but testable! For those that are new to unit testing, MockNodeMediator is a naïve implementation of NodeMediator that saves the position when SetPosition() is called to a member variable and returns it when GetPosition() is called. Now the actual class that is coupled to Cocos looks like this:

class CocosCannon2D : public cocos2d::Sprite
{
public:
    CocosCannon2D();
    static CocosCannon2D* create();

    void update(float deltaT)
    {
        m_cannon.Update(deltaT);
    }

    bool init()
    {
        ...
        m_mediator = std::make_unique<CocosNodeMediator>(this);
        m_cannon2D = std::make_unique<Cannon2D>(&m_cannon, m_mediator.get());
        ...
    }

    bool OnKeyReleased(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* e)
    {
        m_cannon2D.KeyUp(CocosToTLKeyCode(keyCode));
    }
    bool OnKeyPressed(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* e)
    {
        m_cannon2D.KeyDown(CocosToTLKeyCode(keyCode));
    }
    ...
private:
    Cannon m_cannon
    std::unique_ptr<Cannon2D> m_cannon2D;
    std::unique_ptr<CocosNodeMediator> m_mediator;
    ...
}

Notice how the dependency is still pointing towards MyGame module, yet the key presses are now testable. What you end up getting is a subclass where each utility method is one-line long. They’re all tested externally and it’s very difficult to have a bug in this code that is closely coupled to Cocos. In my opinion testing key controls like this a bit aggressive if the ship only moves in one direction. As I explained earlier the switch statement from the old Cannon2D is sufficient without a test. But if you have more mechanism built on top of key presses, such as a secondary weapon, then it’s definitely worth doing something like this to ensure testability.

“See Johnny? This is how you work with frameworks and flip the bird the other way at the same time! Since most video game engine have similar interfaces such as update() and keypress() if one day you decide to switch engines, you just have to rewrite the mediator implementation and nothing about your game would change! If you don’t touch it, you won’t break it!” I exclaimed.

“My name is not Johnny,” you said.

This is fundamentally how you would decouple your game from the game engine. Implementations may vary depending on the engine or the framework, but I hope you get the idea. With this out of the way, we can now explore the amazing world of Trap Labs’ architecture in Part 1.

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.