VVVVVV open sourced

On January 11, 2020, Terry Cavanagh announced at AGDQ that he would be open sourcing his well-known puzzle platformer, VVVVVV, in celebration of the 10-year anniversary of its release.

Reddit finds the state machine

Shortly after the announcement, a Reddit user by the name of "sevenseal" posted this Reddit comment:

VVVVVV Reddit Post Screenshot

Here's the link if you want to browse the code yourself:

https://github.com/TerryCavanagh/VVVVVV/blob/master/desktop_version/src/Game.cpp#L597

Discord inquiry

A user named "infinitive" in TheCherno's Discord server asked:

How do u make a story game that doesnt have an 8 thousand case switch statement like vvvvvv? What's the proper way to do it?

After asking a few clarifying questions, I determined that what our friend infinitive meant by "story game" was really anything that requires a large number of states with a moderately complex graph of events connecting those states.

I had heard about the source release of VVVVVV, but hadn't yet had time to read the code. I figured I may as well take this opportunity to check it out.

Code review

I looked at the first few cases of the switch statement:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
switch(state)
{
case 0:
    //Do nothing here! Standard game state
    break;
case 1:
    //Game initilisation
    state = 0;
    break;
case 2:
    //Opening cutscene
    advancetext = true;
    hascontrol = false;
    state = 3;
    dwgfx.createtextbox("To do: write quick", 50, 80, 164, 164, 255);
    dwgfx.addline("intro to story!");
    //Oh no! what happen to rest of crew etc crash into dimension
    break;
case 4:
    //End of opening cutscene for now
    dwgfx.createtextbox("  Press arrow keys or WASD to move  ", -1, 195, 174, 174, 174);
    dwgfx.textboxtimer(60);
    state = 0;
    break;

// ...

}

The first thing I look for when considering of a chunk of code should be refactored is repetition. There is very little repetition in this code. Case 2 and 4 both have a call to dwgfx.createtextbox, but with different text. So far, this is hardly worth refactoring.

Scrolling down a bit farther in the file (again, we're still only looking for obvious repetition), I noticed this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
switch(state)
{

// ...

case 300:
    startscript = true;
    newscript="custom_"+customscript[0];
    obj.removetrigger(300);
    state = 0;
    break;
case 301:
    startscript = true;
    newscript="custom_"+customscript[1];
    obj.removetrigger(301);
    state = 0;
    break;
case 302:
    startscript = true;
    newscript="custom_"+customscript[2];
    obj.removetrigger(302);
    state = 0;
    break;

// ...

case 336:
    startscript = true;
    newscript="custom_"+customscript[36];
    obj.removetrigger(336);
    state = 0;
    break;

// ...

}

Ah, now that's repition if I've ever seen it! Cases 300-336 are identical except two incrementing integers. removetrigger takes the state as an argument, and customscript[] is indexed by an incrementing integer starting at 0. So, how can we improve this?

My first stab at a refactor for cases 300-336 would be as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
case 300: case 301: case 302: case 303: case 304: case 305: case 306: case 307:
case 308: case 309: case 311: case 312: case 313: case 314: case 315: case 316:
case 317: case 318: case 319: case 320: case 321: case 322: case 323: case 324:
case 325: case 326: case 327: case 328: case 329: case 330: case 331: case 332:
case 333: case 334: case 335: case 336:
    startscript = true;
    newscript="custom_"+customscript[state - 300];
    obj.removetrigger(state);
    state = 0;
    break;

There are certainly ways to simplify this further, and prevent having to type out all of the cases, but this is the safest way in the context of a switch statement. For instance, we could remove all but case 300, use an if statement to check if the state was within the range of custom scripts, then intentionally fall-through to the next case instead of breaking, but this allows you to make subtle mistakes like have an additional 301 case somewhere else in the switch. When you name all of the cases explicitly, it enables the compiler to complain if there are duplicates elsewhere in the switch.

The remainder of the cases in the switch statement don't appear to have any aggregious repetition. So let's consider other ways we might simplify or improve the readability of this code.

The next thing I would do to make this code easier to read and maintain, is replace all of the case statement magic numbers with an enumeration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// NOTE: These state names are totally arbitrary for the purpose of example and
// have nothing to do with the original VVVVVV code in the case statements.
enum game_states {
    ROOM_0024_ENTER     = 3086,
    ROOM_0024_OPEN_DOOR = 3087,
    ROOM_0024_EXIT      = 3100,
};

switch(state)
{
// ...
case ROOM_0024_ENTER:
    if (dwgfx.fademode == 1) state++;
    break;
case ROOM_0024_OPEN_DOOR:
    map.finalmode = false;
    startscript = true;
    newscript="regularreturn";
    state = 0;
    break;
case ROOM_0024_EXIT:
    if(dwgfx.fademode == 1)    state++;
    break;
// ...
}

This would have prevented the developer from having to (per the Reddit comments in the screenshot above) "[keep] a notepad nearby with the important numbers written down". If you know what you're looking for, you can simply search for it by name. If you don't remember the name, you can scroll through the enum at the top of the file looking for the state you're interested in, then quickly search for it in the giant state machine once you've reminded yourself of the name. Performance-wise, this is identical to having the state ids directly in the switch statement. The only downside is having to come up with a name for each state, which is hardly a downside considering the obvious benefits it provides in the long run.

The next possible step to abstract this code further, and potentially improve readability if done sensibly, is to move the state-specific logic into functions per state. These could then be organized in code however you like, including in separate files if that were beneficial to readability and maintainability (e.g. a file called area3.cpp could be created to contain all of the state handlers related to "area 3", if that were a sensible unit of logic for your game).

Here is an example of what that might look like for the arbitrary names I chose as examples in the code snippet above:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
enum game_states {
    ROOM_0024_ENTER     = 3086,
    ROOM_0024_OPEN_DOOR = 3087,
    ROOM_0024_EXIT      = 3100,
    GAME_STATE_COUNT
};

// NOTE: I'm assuming that fademode == 1 means a fade completed
static void next_state_if_fade_complete()
{
    if (dwgfx.fademode == 1)
        state++;
}

// NOTE: Copy/pasted from VVVVVV source, probably has nothing to do with
// opening a door. Someone who understands what the code does would
// obviously come up with better names.
static void room_0024_open_door()
{
    map.finalmode = false;
    startscript = true;
    newscript="regularreturn";
    state = 0;
}

void game_update_state()
{
    typedef void (*state_handler)();
    static state_handler state_handlers[GAME_STATE_COUNT] = {
        [ROOM_0024_ENTER]     = next_state_if_fade_complete,
        [ROOM_0024_OPEN_DOOR] = room_0024_open_door,
        [ROOM_0024_EXIT]      = next_state_if_fade_complete,
        // etc.
    };

    // The if statement checks if this state has a handler in the lookup table.
    // If we ensured every state had a handler, this is not necessary.
    if (state_handlers[state]) {
        state_handlers[state]();
    }
}

You'll notice that I took the liberty to refactor ROOM_0024_ENTER and ROOM_0024_EXIT out into a single function, since their logic was identical. I've named this next_state_if_fade_complete based on the assumption that this is what fademode == 1 means in the VVVVVV source, but there's definitely some guesswork hidden in that assumption. Nevertheless, it nicely demonstrates an additional way to alleviate duplication and make the code more readable.

There are of course many more generic approaches, most of which depend on you recognizing patterns of repetition in the state handler logic and finding creative ways to refactor that repetition out into a useful data representation. For example, let's assume for a moment that, using the same strategy as we did for ROOM_0024_ENTER and ROOM_0024_EXIT above, we could distill all of the state logic down into a small number of handlers (e.g. "start_game", "start_fade", "next_state_if_fade_complete", "victory"). Rather than maintain the lookup table in code as a state array, you may want to refactor it out into a data file so that your fancy GUI editor could be used to modify which handler gets called from each state. This would be as simple as loading the lookup table from e.g. a CSV file:

3086,next_state_if_fade_complete

3087,room_0024_open_door

3100,next_state_if_fade_complete

You could store the state enum values as strings if you wanted, and it may even make more sense since you'd have to generate a lookup table from game_state integer values to string names anyway if you want a user-friendly GUI editor.

Any attempts to further optimize or compress this data are probably counter-productive, at least during the development phase. Having an easily readable and hand-editable data format while iterating through different design ideas is a powerful tool, not to be underestimated. Besides, if you go crazy with compressing state logic into a custom binary file format, then you may end up optimizing yourself out of the ability to quickly try out cool new handlers that you didn't foresee when designing the hyper-optimized storage format.

Closing remarks

Overall, while the VVVVVV state machine does have a few blatantly obvious cases of repetition, and some clear opportunities for readability improvements, I don't think it should be brushed off as an irreparable mess. It does the job that a state machine is meant to do, and the developers did care enough to keep the cases sorted by increasing state values, which is more than you can say for a lot of codebases. There are certainly benefits to having all of the state logic in one place, and, with the very minor adjustment of adding an enum in place of the magic numbers, this code would be fairly readable and maintainable.

The most important thing is that VVVVVV is stable, it shipped, and it brought both joy and misery (the good kind) to the hearts of thousands of players around the world.

If you somehow missed out on this one, it is a fantastic game that stands up very well against the test of time, and you should go check it out on Steam:

You can find the developer, Terry Cavanagh, on Twitter: https://twitter.com/terrycavanagh

Happy coding!