This is part 4.2 of the multipart Trap Labs Code Design and Architecture Series.
This is where noob developers really like to argue with me regarding unit tests for video games. Let me be perfectly clear about this: you don’t test how audio or visual looks or feels, you test their interaction and execution logic. This means you don’t test whether whether your HUD is positioned on the top right corner of the screen and it’s using the Arial font displaying the ammo count. You should instead be testing if the ammo count changes, does the ammo display update as expected?
This is a topic that really deserves its own article, which I’ll definitely do a full write-up in the future, but I have to make it clear for those who find writing unit tests or practicing TDD troublesome for video games (or just GUI in general).
I see code in terms of their testability by the amount of logic and the amount of statements. Consider the following code:
cocos2d::Sprite* CreateElevator()
{
auto elevator = Sprite::createWithSpriteFrameName("Elevator_Back.png");
elevator->setPosition(elevator->getContentSize() / 2);
return elevator;
}
The three lines function creates a sprite and sets its position. How do you test this code? You don’t. This function is what I call a series of statements (I’m not really sure if statement is the appropriate word but I’m going to stick with it for now. If you have a more appropriate word let me know). Is there logic in this function? Sure they’re embedded in the function calls down the stack. But notice there are no if-statements, no loops/recursion, and no branches on the surface of the function worth testing. When I write unit tests I use them to test logic, and if there is no logic on the surface of the function I usually don’t test it. And if you respect writing small functions then it's really hard to go wrong with these types of functions. This is also the reason why we don’t usually write unit tests for main
because main
’s logic is usually encapsulated in simple lower level statements.
So how do I verify correctness in this case? I eyeball it (you know, EDD as in Eyeball Driven Development :P). This is not to say that I never test functions like this. As long as the function has some input and returns output then it’s probably worth testing, even if it’s purely made up of statements. Or, if the order of statements is critical to the execution of the program then it's also worth testing. Specifically, what is worthy of tests is up to you, the developer, to figure out. Just remember that blindly writing tests purely for chasing for 100% code coverage is idiotic. Most people who practice TDD well do not chase for 100% code coverage, only the mindless ones do.
Also notice this code is coupled to Cocos2d-x, how can one unit test this when needed? Well I went over this in part 0. So give those a read if haven’t yet already. The basic idea is to create a layer that decouples the framework from your game engine, then you can test pretty much anything. In addition, if you decoupled your game elements properly you should also be able to create a visual sandbox to visually “unit test” your game as well. The map maker for Trap Labs doubles as an element/component tester so I could manually verify in-game elements quickly.
What you need to understand is that you don’t test the look or the feel of the game. That means you don’t test if a piece of HUD is positioned at certain point, or if the text color is red, or if the car’s acceleration feels right. These tests are extremely unstable as the feel and look of any software is constantly adjusted, which means every time you make a change to the game’s look and feel your tests would break. Don’t test artistic and creative aspects of the game. They are for people and creative minds to fiddle and perfect, not for unit tests. Don’t be stupid and stop arguing with me.
What I do want to go over specifically in this section is how to write audio and visual code that is testable. What you want to essentially end up with is most of the logic is decoupled contained within its own standalone library, and only have code that is mostly made up of statements coupled to the framework (again I went over the fundamentals of this in part 0 of my series, so please give that read if you haven’t yet).
This means that all in-game elements that will be used by the game engine to create a visual element needs to have its logical visual states separated from the actual audio and visuals. I call this state based animation. I'm not sure if this code pattern exists but I’m pretty sure it’s been done before. So please let me know if there is a better name for this pattern.
Let’s go through a quick example. Consider a dumbed down version of the trap door from Trap Labs. The trap door open and closes repeatedly on a set interval. Here’s what the TrapDoor
test might looks like:
TEST_CASE("TrapDoor update single interval")
{
TrapDoorElement element;
element.liveDuration = 0.2f;
element.offDuration = 1.0f;
TrapDoor trap(&element);
trap.Enable();
trap.Update(0.9f);
REQUIRE(trap.GetState() == TrapDoorState::OFF);
trap.Update(0.1f);
REQUIRE(trap.GetState() == TrapDoorState::LIVE);
trap.Update(0.19f);
REQUIRE(trap.GetState() == TrapDoorState::LIVE);
trap.Update(0.01f);
REQUIRE(trap.GetState() == TrapDoorState::OFF);
...
}
I hope the code is self-explanatory. I first created a TrapDoorElement
(entity), followed by a TrapDoor
(interactor) that references the element. The trap is then enabled, and Update()
is called repeatedly around the live-off transition boundaries, and the trap’s state if verified via the enum TrapDoorState
. I won’t go over the implementation of the Update()
function, it should be trivial.
Now how do we translate this to code that create some visual using Cocos? Remember the mediator class from part 0? I created a VisualMediator
interface purely to handle animation state changes:
class VisualMediator
{
public:
virtual void PlayStateAnimation(uint32_t state) = 0;
...
}
Then simply implement interface coupled to Cocos with a switch statement.
void PlayStateAnimation(uint32_t state)
{
auto trapState = static_cast<TrapDoorState>(state);
switch (trapState)
{
case TrapDoorState::LIVE:
runAction(m_liveAnimation);
break;
case TrapDoorState::OFF:
runAction(m_offAnimation);
break;
}
}
Even though there is a switch statement in there, the case statements are usually one liners, and made up purely of statements. It is easy enough to eyeball similar to the argument I made in part 0 regarding the controller’s switch statement.
Then if you decoupled your game properly from the framework then test and implementation TrapDoor2D
would look something like this:
TEST_CASE_METHOD(TrapDoorFixture, "TrapDoor2D update single interval")
{
MockNodeMediator mockMediator;
MockVisualMediator mockVisualMediator;
TrapDoor2D trapDoor2D(&trap, &mockMediator, &mockVisualMediator);
trap.Update(0.9f);
REQUIRE(mockVisualMediator.LastCalledState() == (uint32_t) TrapDoorState::OFF);
trap.Update(0.1f);
REQUIRE(mockVisualMediator.LastCalledState() == (uint32_t) TrapDoorState::LIVE);
trap.Update(0.19f);
REQUIRE(mockVisualMediator.LastCalledState() == (uint32_t) TrapDoorState::LIVE);
trap.Update(0.01f);
REQUIRE(mockVisualMediator.LastCalledState() == (uint32_t) TrapDoorState::OFF);
...
}
class TrapDoor2D
{
public:
TrapDoor2D(TrapDoor *trapDoor, NodeMediator* nodeMediator, VisualMediator* visualMediator);
...
void Update(float delta) override
{
auto curState = m_trap->GetState();
if (curState == m_lastState) return;
m_visualMediator->PlayStateAnimation((uint32_t)curState);
m_lastState = curState;
}
...
}
And lastly for the CocosTrapDoor2D
:
class CocosTrapDoor2D : public cocos2d::Sprite
{
public:
CocosTrapDoor2D(TrapDoor2D *trapDoor2D);
...
void update(float deltaT) override
{
m_trapDoor2D->Update(deltaT);
}
...
}
Notice how similar the 2D test looks compared to the previous test? I opted to test it this way you can certainly test it differently if you feel this is repeating code (it’s really not). As long as you can verify that “don’t play state animation if state doesn’t change” then the test is sufficient. For example you can just call Update() and have a spy variable that checks whether PlayStateAnimation()
has been called.
It’s worth noting that the above code is purely for demonstration purposes. In actuality, I extracted a base Trap2D
which has the PlayStateAnimation()
implementation and created a mock to just test that by itself, then any derivative class that does not override PlayStateAnimation()
does not need to be tested again. Remember that there’s probably something common you can extract and test independently whenever you see similar looking code in your tests for two seemingly different classes. This not only simplifies your code but also reduces the amount of tests you have to manage.
What about more complicated visuals? The actual trap door used in Trap Labs has a more complicated visual system. It is comprised of 4 element states (OPENING, CLOSING, LIVE, OFF
) and 2 visibility states (VISIBLE, HIDDEN
). This way I can control the opening and closing animation duration and also be able to show and hide the trap dynamically as needed. In fact, all modular visual elements in Trap Labs has the visibility state and an element state. So my actual VisualMediator interface has few more override-able methods. Another more tip I can give you is that if you want to play transitional animation as well, such as transition between two states then simply change the PlayStateAnimation()
function arguments with a from
and to
state, then override accordingly. Just remember to keep your VisualMediator implementation classes simple and if you do it right then the code coupled to Cocos shouldn’t need to be unit tested.
Extending this concept to handle audio is similar. You can create an AudioMediator
to achieve the same effect. However, I opted for VisualMediator to handle the sound as well because animation and sound usually plays together. In the future if I need to play sound independently of the animation then I would consider using an AudioMediator
.
So what have you learned? How to completely separate visual logic from the visuals itself. If you design your game right such as building the elements out of entities and interactors as descripted in part 1, you can adjust the feel of the game all you want and the tests wouldn’t break. As you saw from the tests above, I can adjust the timings of the trap door animation all I want for the final production code and the tests wouldn’t have to be changed because they are created out of entities and the values are adjustable. Do you understand now why you don’t test the look or the feel of the game?
There is one very important additional advantage in coding your game visuals this way besides testability. You end up with a passive game client. The client does not have to know any game logic to run. It simply syncs game states from the server and interpolate the states. In the next subpart I talk about why this is significant in the client and authoritative server model.