Don't trust yourself. Visualise.

By Greg Goltsov

I noticed an interesting pattern in my programming practice. Coming from theoretical disciplines such as maths and physics I found myself being less interested in details of what I'm implementing. The language of mathematics is as beautiful as it is expressive, so C kind of hits you on the head with a shovel. I still seem to have this false sense of security that, after writing an algorithm/function/method, it's all done -- it's going to work perfectly, because, well, I thought about it. "Yeah, this struct will be populated correctly", "sure, the edge cases will never be met, so let's assert for them", "hey, why would I need to be checking for little endian? C'mon, it's 2012".

Therefore, it never became a habit for me to be checking, questioning my algorithms, methods, variables. And every time I'd get a bug, I'd be puzzled and start thinking. Thinking deeply about my algorithm (which actually is useful, without a doubt). But then, if that doesn't lead me anywhere, I'd get frustrated. I'd get bored.

I'd get stuck.

One of my recent examples (and now, in retrospective, my stubbornness to get unstuck there feels almost childish) was in my level generator for the ninja platformer game we're currently working on. I will dedicate a full post or to to the system I've implemented, but let me focus on one small portion of it here -- just for illustrative purposes.

So, the problem I was solving was how to efficiently generate a cohesive platformer level that can have its difficultly dynamically adjusted during gameplay. I decided to be constructing the level (here I have to point out that the problem was somewhat relaxed by the fact that we only need one traversable path -- no forks or multi-level platforms) from pre-designed chunks called rhythm groups (or rGroups/groups for short). This is what they look like in the editor (I'm kind of abusing the great Tiled editor).

By abusing I mean I am only using the tiles metaphor to place the objects (referred to as blocks) within the group. The discretisation of the space was to simplify the creation of the groups and partially to avoid having to write a level editor. The named tiles (like the Arch building and Pipe that you see) get replaced by the 3D models when the game actually processes the groups, and the dots are fillers for the designer to more easily visualise the dimensions of the blocks -- the process of placing them is manual (and the size has to be read from the main block, i.e. the Arch block is 9 by 23).

Now, given a library of groups, the generator appends them into one traversable level. You can literally think of it as a linked list diagram.

You can see the groups separated by the dashed line. And, schematically, we have the player occupying one tile. Except such a level would be really boring -- there is simply no variation in height, no distances between the groups. Any difficulty would have to be "baked in" the groups -- a very static solution.

What we really want is some kind of deviation between the groups, and some nice mechanism of controlling that. Ideally, something that would produce something like this.

This is a screenshot from the game with the outlines of the groups (shown in red) enabled. Notice how the distances between them vary -- they are not simply appended to one another.

In that screenshot you can see that in the game we're dealing with objects in 3D in continuos space. So how do we translate this tiles metaphor into the realm of continous space? Well, I took the inspiration from linked lists diagrams and introduced a simple concept of links between the groups -- their entry and exit points. They are simply metadata about the groups, specifying safe locations within the group where the player will enter and exit the groups. These are to be provided by the level designer. I would simply use them to be on the same level and this would guarantee that the player can easily jump between the groups.

This is where the problem was. I simply asked to provide the metadata (easily accessible in Tiled) with numbers indicating which tiles are entry and exit points. I asked for numbers since they were easy to process. Simple hash table -- we've all been there, done that. This is what it looked in the editor:

So, everything should've been working? The algorithm for calculating the required compensation to lift the next group to be appended so its entry point would be on the same level as the last group's exit point is straightforward high school maths. Nothing hard.

However, it wasn't working!

All the compensations would come out right, the offsets were great! Except the level was a mess. I was reading through my code over and over again -- no bugs! And then I decided to look into the metadata. My assumption that it was perfect (because, well, "the designer should know what he's doing!") was wrong. Count was starting from 0, going out of boundaries, not specified, etc. And this was in a relatively small-sized groups library of 30.

I simply didn't want to go through all of them and compare the numbers, so I did what I should've done from the start -- visualise these control points! After a two minute job of changing the spritesheet, it was all ready:

You can see the entry and exit control tiles on the building. Simple. Instantly visible and verifiable. Another five minutes of placing these new tiles on all the groups in the library and we're done.

And it worked! Flawlessly. You can see the groups (red rectangles) have their exit and entry points (azure and magenta, respectively) aligned, meaning the level is guaranteed to be traversable by the player (although not too interesting).

So, write tools to visualise what's happening. Make it so it would be impossible not to see bugs. Make it beautiful. Make it dynamic. This will provide so much visual and intellectual feedback, it's possible you will get ideas you would never have thought of. "What if I tweak player's jump velocity?". "What if I swap the sign for gravity? Will I fly through the ceiling or the collisions will handle that?" "What if I could change models in real time?"

What if...?

This question might be one of the main driving forces behind programming - the drive to experiment.