Input Simulator

Jan 27 2017

Some of you already know that I’ve strong roots in Agile practices. One of them is TDD, and up until now, I haven’t been able to do TDD in video games in a productive way. In Nubarron we actually started the development doing TDD, but Unity Test Tools are too slow and unreliable (failing tests because of frame drops caused by who knows what, and things like this) so we ended up abandoning it.

Now that I’m working on my thing, I wanted to make automated testing a first-class citizen of my workflow and games. For this reason, I started implementing an in-game testing system. I already have automated tests for several things and it’s been productive so far. One of the most important requirements for such system in a video game setting is the ability to simulate input.

The idea is that inside the test I can tell my game to press some key, move the mouse or move an axis in the controller, force some action, and test its results. This is not only useful as a regression insurance policy, this will be a central feature for bug reporting and fixing when I have input recording and replay.

The approach I took for the input system is to have snapshots (called Steps) of how the input memory should look like at a given frame, and create macros that will setup these snapshots, so it’s not a surprise that a Step looks similar to what I explained in the previous post:

struct Step
  U64 frame = 0;
  bool active = true;

  // Keyboard
  bool keys_down[KEYBOARD_KEY_COUNT];
  bool shift = false;
  bool control = false;
  bool alt = false;
  bool super = false;

  // Mouse
  F64 mouse_x;
  F64 mouse_y;
  F32 mouse_wheel;
  F32 mouse_wheel_buffer;
  bool mouse_down[3];

  // Controllers
  bool controller_connected[CONTROLLER_COUNT];
  bool controller_was_connected[CONTROLLER_COUNT];

  I32 controller_axis_count[CONTROLLER_COUNT];
  F32 controller_axis_status[CONTROLLER_COUNT][CONTROLLER_AXIS_MAX];
  F32 controller_axis_previous_status[CONTROLLER_COUNT][CONTROLLER_AXIS_MAX];
  F32 controller_axis_delta[CONTROLLER_COUNT][CONTROLLER_AXIS_MAX];

  I32 controller_button_count[CONTROLLER_COUNT];
  I32 controller_button_status[CONTROLLER_COUNT][CONTROLLER_BUTTON_MAX];
  I32 controller_button_previous_status[CONTROLLER_COUNT][CONTROLLER_BUTTON_MAX];

So, the step is basically the memory struct plus the frame and a bool that says if this Step was executed or not.

Regarding the Data structure for this subsystem, here it is:

struct Data
  Step steps[MAX_INPUT_STEPS];
  I32 step_count = 0;
  bool simulating = false;
  U64 initial_frame = 0;

I create a certain amount of steps (for now this is enough. I’ll probably need something dynamic here later for the input recording/playback), and then the rest is used to control the state of the simulator.

The update function looks something like this (removed the update code since doesn’t provide anything special)

bool has_active = false;

  for(int i = 0; i < data->step_count; ++i)
    Step step = data->steps[i];

    has_active = has_active ||;

    if( && (PERMANENT->frame - data->initial_frame) >= step.frame)
    { = false;
      // Set current memory to be equal
    data->steps[i] = step;


I know, I could remove the steps that are not used and blah, but that would probably be slower than just iterating through a certain amount of steps, what I’ll do in the future is have these steps sorted, so I can exit the for loop when I reach a step that belongs to a future frame, and keep the index of the last step I ran, to avoid the steps that belong to the past.

If I don’t have steps that are active, I stop the simulation.

Now, for the step definition, I created several macros

#define CURRENT_STEP PERMANENT->input_simulator->steps[PERMANENT->input_simulator->step_count]

#define SET_FRAME(frame_number) CURRENT_STEP.frame = frame_number;
#define SET_KEY_DOWN(key) CURRENT_STEP.keys_down[key] = true;
#define SET_KEY_UP(key) CURRENT_STEP.keys_down[key] = false;

#define SET_MOUSE_POS(x, y) CURRENT_STEP.mouse_x = x; CURRENT_STEP.mouse_y = y;
#define SET_MOUSE_DOWN(key) CURRENT_STEP.mouse_down[key] = true;
#define SET_MOUSE_UP(key) CURRENT_STEP.mouse_down[key] = false;

#define CONTROLLER_CONNECTED(id, value) CURRENT_STEP.controller_connected[id] = value;
#define SET_AXIS(id, axis, value) \
  if(CURRENT_STEP.controller_axis_count[id] <= axis) \
    CURRENT_STEP.controller_axis_count[id] = axis + 1; \
  CURRENT_STEP.controller_axis_status[id][axis] = value;

#define SET_BUTTON(id, button, value) \
  if(CURRENT_STEP.controller_button_count[id] <= button) \
    CURRENT_STEP.controller_button_count[id] = button + 1; \
  CURRENT_STEP.controller_button_status[id][button] = value;

#define END_STEP() (++PERMANENT->input_simulator->step_count);
#define SIMULATE() InputSimulator::start_simulation();

This is pretty simple, CURRENT_STEP is just a shortcut to the first free step in the steps list.

Each of the SET_* methods takes care of setting the correct attribute in the Step struct.

When you call END_STEP, it will move the pointer to the next free step.

So.. I’ll show you an example of a test now so that you see this in context.

I have an editor feature that I call Quick Commands, which I use for rapidly executing stuff: I hit Control+P, and it opens a modal with a text input, you start typing and it autocompletes using a list of possible commands. You can use the arrows to select an option and hit enter to execute it. Instead of having lengthy menus, I just use this to open windows and execute repetitive tasks. When the modal is open, you can always hit escape to close it.

This is the test for the close functionality:

TEST(QuickCommands, ClosesWithEscape)
  Editor::memory->quick_commands->open = true;





  ASSERT_EQ(false, Editor::memory->quick_commands->open);

In the first line, I mess with the memory of the QuickCommands module to open the modal manually.

Then I set two key simulations, in the first frame of the simulation, I hold escape down, in the second frame I release escape. Then I run the simulation and wait for 3 frames. After that, the editor should be closed.

So, this is pretty much it, not a complicated feature. It took me a couple of hours to implement and it’s already been really useful!

What do you think about it?

See you next time!

comments powered by Disqus