Test-Driving the Game Loop pt. 2

Test-Driving the Game Loop pt. 2

Eric Smith
Eric Smith

October 14, 2014

When we last saw our Test-Driven Game Loop, we hadn’t really made it very far, at least in terms of production code. This is what we had:


while (Game.Running)
{
				Game.Update();
				Game.Draw();
}

This game loop is really incomplete. For starters, it doesn't take input, so it's not very fun to play. More concerning is that updates and draws are as fast as the computer can make them. While that might sound good intuitively, it means that the game will actually be harder on fast computers. I'm not sure that's what it means to be charitable, so I want to make the updates a fixed length of 1/60th of a second.

Let’s take another look at the pseudocode we’re working toward, if only to refresh our memory:


double previous = getCurrentTime();
double lag = 0.0;
while (true)
{
				double current = getCurrentTime();
				double elapsed = current - previous;
				previous = current;
				lag += elapsed;

				processInput();

				while (lag >= MS_PER_UPDATE)
				{
								update();
								lag -= MS_PER_UPDATE;
				}
				render();
}

We’re test-driving toward an algorithm we already know, and when this happens it can be very difficult to avoid writing the entire thing before the tests require it—after all, it’s already in front of you. Indeed, why write the tests at all? It’s clearly more code than if we were to just copy this algorithm and tweak it to fit our context.

There are many reasons I could list, including professional ethics and reducing the debugging time, but the best reason to write these tests is to understand the algorithm better. My process is to try and identify a simple part of the pseudocode, write a test that would fail given those conditions, and then implement just enough code to make that pass. Eventually, I’ll have my own version of the code that I can understand and modify without fear. In case you think I’m never going to modify this code, because everybody uses the same game loop, let me direct you to the Unity3D version, which is far more complicated than this one. I would not want to modify that code without tests.

Take the next test as an example. Currently, our game loop does not take in any input, but obviously we need it. Our pseudocode has a "processInput()" call and implies that there is a global input state, but I don't want that. Instead, I want to ensure that the input is polled and is passed along to the update call[1]. Now I can write a test:


[Test]
public void ItPassesTheInputResultsToUpdate()
{
				var game = new TestGame();
				var inputHandler = new StubInputHandler();
				var inputState = new InputState();
				inputHandler.ReturnedInput = inputState;

				var gameLoop = new GameLoop
				{
								Game = game,
								InputHandler = inputHandler
				};

				gameLoop.Run();

				// Note Are SAME not EQUAL
				Assert.AreSame(inputState, game.UpdatedWith);
}

Take a look at that game.UpdatedWith on the last line. This must mean I changed the TestGame yet again, and I have to log the InputState that is passed into the Update. In fact, this meant a change to the interface:


public interface Game
{
				bool Running { get; }
				void Update(InputState input);
				void Draw();
}

I first changed the TestGame to just compile. I won’t reproduce that here, but it’s important to note that part of my flow was to not only see the failure to compile, but to also see the test fail, pausing at both steps. It’s very easy to write a test with a compiler failure, then fly right through to making the test pass even though you never actually ran the test! That’s how you accidentally write tests that don’t actually fail when there’s a bug. Once again let’s see how the code test was made to pass:


public void Run()
{
				while (Game.Running)
				{
								Game.Update(InputHandler.CurrentState);
								Game.Draw();
				}
}

That makes the code pass, but it has a problem. At this point InputHandler is a fake, but it’s pretty clear to me that I’m going to need to poll the input each time CurrentState is called. Right now that’s harmless because I’m only calling CurrentState once per update, but we will be returning to this problem. Why didn't I deal with this problem immediately? Because I didn't notice this problem immediately! Even us fancy pants Software Craftsfolks make mistakes.

So, blissfully unaware of my command/query separation[2] problem, I began cleaning up my tests a bit. The input test now looks like:


[Test]
public void ItPassesTheInputResultsToUpdate()
{
				var game = new TestGame();
				game.EnqueRunningAnswers(true, false);
				var inputHandler = new StubInputHandler();
				var inputState = new InputState();
				inputHandler.ReturnedInput = inputState;

				var gameLoop = new GameLoop(game, inputHandler);

				gameLoop.Run();

				Assert.AreSame(inputHandler.ReturnedInput, game.UpdatedWith);
}

The main thing you'll see here is EnqueRunningAnswers, which demonstrates two things. One, I can't spell enqueue; and two, I've added some functionality to my TestGame class. The "running answers" are the answers to the IsRunning "question" in the game loop. By saying true then false, I ensure that the loop runs once and only once. I've also switched to a constructor with parameters instead of using the named initializer syntax. If you pay close attention, you'll notice I can't make my mind up about this.

Here's another test, post-cleanup:


[Test]
public void ItRunsADrawAfterUpdate()
{
				var game = new TestGame();
				game.EnqueRunningAnswers(true, false);

				var gameLoop = new GameLoop(game, new NullInputHandler());

				gameLoop.Run();

				Assert.AreEqual(1, game.DrawCount);
}

Now you can see I've added a NullInputHandler, which does exactly what you might expect. Now you might wonder why I didn't add a method to TestGame called RunTimes(1) instead of forcing the user to explicitly set the return values for IsRunning. It would be simpler to read and understand after all, especially without those vague booleans.

Truthfully, it's because you didn't point that out until right now. Yup, it's your fault. Next test!


[Test]
public void ItWillUpdateOneExtraTimeToCatchUpWhenTheLagIsTooLarge()
{
				var game = new TestGame();
				game.EnqueRunningAnswers(true, false);
				var time = new FakeTimer();
				time.Times = new Queue<float>();
				time.Times.Enqueue(0);
				time.Times.Enqueue(17);

				var gameLoop = new GameLoop
				{
								Game = game,
								InputHandler = new NullInputHandler(),
								Timer = time
				};

				gameLoop.Run();

				Assert.AreEqual(2, game.UpdateCount);
				Assert.AreEqual(1, game.DrawCount);
}

Whew! That's a big test that's gonna need some love. What am I trying to do here? Well if we look at the pseudocode above, we'll see that we are trying to run what is called a "fixed-step" game loop.

The way that works is the game objects assume a fixed frame rate for each update, typically 1/60th of a second. Naturally, computers don't always have a fixed frame rate. Sometimes players do things like start up iTunes in the middle of the game and starve the CPU, or Norton AntiVirus starts running, or your game has an update that takes a while. In that event your update will take longer than 1/60th a second. Other games wouldn't bother timing loops and would go as fast as the computer would allow, making them too hard on faster hardware.

Our solution, and a common one that is shared by some commercial game engines, is to use a fixed-step loop. What we do is assume every update only takes 1/60th of a second. If we hit the update too quickly, we go ahead and skip an update. If we hit it too slowly, we run a few to "catch up" before drawing. This can cause problems. If you've ever played a baseball game and seen the pitch jump from the pitcher's hand all the way to the catcher's mitt, you know what I'm talking about; but it works well in many applications.

Returning to our test, we start by setting our game to run one loop before quitting (the true, false in EnqueRunningAnswers). We now set up a FakeTimer. Once again we're building an interface through fakes to our own domain objects, which don't exist yet. Time is always a gnarly beast, and if you think you can make it work with the real system time... well, you're wrong. You will need an abstraction, or you'll end up with flaky tests that fail every once in a while because of differences of a few milliseconds.

Here we once again Enqueue a list of times (you'd think this would have clued me in on the spelling) directly using the .NET Queue construct. It certainly could use some refactoring, but let's get the test working first and then clean up.

We set it up to return a 0 and 17. Well what the heck do those mean? Our maximum amount of time between two updates is 16 time units. Really, it's 16 milliseconds (1/60 is .166666), but once we've abstracted that into our timer it really doesn't matter. We don't use floats since floating point errors will eventually doom our application when we run this quickly.

If the time between updates is more than 16, we should run another update—hence the UpdateCount of 2—but we don't run extra Draws. We'll drop a frame. Here's how I made that pass:


public void Run()
{
				var previousTime = Timer.GetTime();
				while (Game.Running)
				{
								var currentTime = Timer.GetTime();
								var lag = currentTime - previousTime;
								while (lag >= 0)
								{
												Game.Update(InputHandler.CurrentState);
												lag -= 16;
								}
								Game.Draw();
				}
}

Now something might be bothering you about this, or should be. The while loop isn't really necessary, as I could do this with a simple if statement. Aha! I have fallen into the trap of writing the algorithm early, or I have to write the while loop for reasons I don't remember to keep the tests passing. Let's go with option two: makes me sound smarter.

This of course required a bit more code than is being shown here. I have a NullTimer and an interface for Timers. I don't think that little bit of code is instructive, so let's move forward.

I needed to clean up my tests in the next commit to make sure everything had a timer, and from that I learned about a little problem with my code. Once I added a timer to every test I got a failure in a completely different test. The test was:


[Test]
public void ItUpdatesUntilTheGameIsStopped()
{
				var game = new TestGame();
				game.EnqueueRunningAnswers(true, true, false);

				var gameLoop = new GameLoop
				{
								Game = game,
								Timer = new SecondTicker(),
								FrameLength = 1.0f
				};

				gameLoop.Run();

				Assert.AreEqual(2, game.UpdateCount);
}

The failure? Expected 2 but was 3. Why would the loop have updated three times? At this point a lot of developers would immediately go to the debugger, but let's take a moment to reason about this test first. I've created a game loop with a frame length of 1.0 seconds, and a ticker that ticks every second (this can be inferred by the name). Because the frame length and tick length are the same, we should only get one update per tick—yet the test seems to be saying we're doing an extra update. We're playing catch up.

Let's look at the code again, updated to reflect the small refactorings:


public void Run()
{
				var previousTime = Timer.GetTime();
				while (Game.Running)
				{
								var currentTime = Timer.GetTime();
								var lag = currentTime - previousTime;

								while (lag >= FrameLength)
								{
												Game.Update(InputHandler.CurrentState);
												lag -= FrameLength;
								}

								Game.Draw();
				}
}

While the game is running, I get the time and then I calculate the lag between frames to see how many times I should update. Do you see the problem yet? I don't update the previous time—it's always the start time of the application, and my lag is just growing and growing.

This is why I test-drive my code in a nutshell. Had I written this traditionally, I would have written a small game before I could debug anything. The game itself would have likely crashed, or behaved in insane ways, and I would have had dozens of potential broken points. It could have taken hours or days to fix; and since this is a hobby for me, hours and days can translate into weeks and months. You think it's too time-consuming to write tests? I think it's too time-consuming not to!

Let's fix this bug—before it becomes an actual bug:

public void Run()
{
				var previousTime = Timer.GetTime();
				while (Game.Running)
				{
								var currentTime = Timer.GetTime();
								var lag = currentTime - previousTime;

								while (lag >= FrameLength)
								{
												Game.Update(InputHandler.CurrentState);
												lag -= FrameLength;
								}

								previousTime = currentTime;
								Game.Draw();
				}
}

I feel compelled to say that I don't get paid by the word.

It was at this point I realized the error with input I mentioned earlier. The InputHandler will need to poll any devices, such as the keyboard, mouse, or joystick, for changes in input. Since I don't poll explicitly the CurrentState property on the Input Handler would have to do it, a potentially expensive operation and a violation of the Command/Query principle.

In order to ensure that the Poll is explicit, I embed that rule in my StubInputHandler, which looks like so:


public class StubInputHandler : InputHandler
{
				protected bool InputIsReady;

				public InputState ReturnedInput { get; set; }

				public InputState CurrentState
				{
								get
								{
												if (InputIsReady)
												{
																InputIsReady = false;
																return ReturnedInput;
												}
												return null;
								}
				}

				public StubInputHandler()
				{
								InputIsReady = false;
				}

				public void Poll()
				{
								InputIsReady = true;
				}
}

This is a little complicated, so I’ll talk you through it. The StubInputHandler has a CurrentState property, which is what I want the real InputHandler to provide. However that current state isn't valid until I make a Poll call, so I return null in the event that the input hasn't been Polled—hence the boolean InputIsRead.

Huh, I guess it wasn't that complicated.

I don't normally put this much logic into a Stub or Fake, and further changes are going to make me pause and re-think. Some people might be screaming for a mocking framework here, but I'm not convinced that a framework makes this any less complicated. It would prevent the problem I see here, which is that this contract (Poll before CurrentState) is embedded in the Stub and is not clearly visible in the test. A mocking framework might help with that, but so might a different design. I'm willing to accept this for now with a crinkly nose. Passing that is easy:


var lag = currentTime - previousTime;

InputHandler.Poll();
while (lag >= FrameLength) {
				...

It's just the one line where we explicitly poll, and that's....it. I've covered each line of this algorithm and could safely use this in an actual game. So what have we really accomplished? Would it have been better to just start with the game loop we had from a web site then copy/paste/tweak to make it work?

There are trade-offs between the approaches, but the only correct answer is no, we would not be better off just grabbing the code without tests to have a head start. I acknowledge that I've written a LOT more code doing it this way, but it's code I actually understand and that uses OO principles. As I implement those interfaces, I can be reasonably certain that any bugs I find are not in the game loop, and if there are bugs I can understand and debug them because this is my code! If the code is mine I'm responsible for it, so I need to know if it works. That's why I write my tests.

Wanna see the code? It's right here.


[1]: After proofing this blog, I'm not sure that I like passing the results of polling input to the update call. Not all objects care about input, indeed most don't, so it's possible I have a Liskov Substitution violation here. Since I don't have any code behind these methods, I'm not really sure, but it's something I'd keep in mind for the future. Perhaps I'd separate processing input further, and do it all before the update.

[2]: Command Query Separation - but you knew that.