Thursday, September 05, 2013

So I Made a Game / Troubleshooting


A weekend or so ago I participated in the first programming competition I ever entered*, the Ludum Dare. It's a game programming competition and it works like this:
  • At a specific day and time the theme is announced.
  • You start immediately from nada, zilch, scratch (more or less).
  • 48 hours later you are done.
  • There is no prize.
How did it go? Here's what I remember:
  • On Friday I plan this totally awesome thing I'm going to do and go to sleep.
  • At the end of Saturday I am feeling very depressed and doomed.
  • By the end of Sunday things are... surprisingly okay but not outstanding. By that I mean there is some awesome work that got done by the other competitors. What I did is something that in retrospect is an homage to doing Artificial Intelligence homework assignments in the late 80's / early 90's. Mazes. Depth first search. A little A* search. Maybe a dose of Reactive Action Packages...
In other words I doomed myself and then surprisingly undoomed myself. And I fell back on old behaviors to get me through it.

Problem 1: Basic game math.

To do game graphics at some point you need to do a lot of position + time * velocity X transform... and when it goes wrong I always feel like a broken individual... you're just trying to make stuff move around on the screen but something is always spinning the wrong way, moving too fast / not fast enough. Just plain wrong. Clearly something needs to be negative. Or take the cos. Or sqrt...

Problem 2: Emergent behaviors.

Okay here is the opposite problem. Gamplay logic - goals, states - the fact is you probably can add code blindly until something works. But you are almost always creating cascade effects by doing so - which is proven when your fix is broken (again).

Solution 1: Solve simpler problems

My game has a maze generator to generate the hallways the player runs around in. Now, at one point a long time ago I was interested in maze generators and even wrote a few in the spirit of hobbyist programing, but in the "from scratch" mechanic I felt I should start with a blank slate. Well so I refresh myself on the basics of maze generation and code something up first thing Saturday and it is not doing what I want. Really not what I want. The algorithm is not implemented correctly. I also realize I'm seeing bugs I know I've seen before but that still doesn't help...

Here's when I ask myself two questions:
  1. Don't I have a degree in math from a reputable institution? 
  2. If I can't even get this (probably) simple thing right what good was it?
I did learn one important thing about math in school: solve an easier problem first. If you are struggling with a math problem and you just keep adding more math blindly you are not winning. So take the fancy part that isn't working away and replace it with something that solves an easier problem. Then go back and try again.

So I solve a series of simpler problems: I make smaller and smaller and then bigger and bigger mazes to the point I can see what's broken. And I repair my maze generation algorithm. And realize what I really did wrong was to not distinguish between walls and cells and... actually the repaired generator was at that point not good enough for me to be happy with it but good enough for a 48 hour game and so I kept going.

Solution 2 : Don't Threepeat Yourself

Later on I was implementing features that required me to be pretty far along in the game to test - therefore the test cycle was going quadratic on me. So I got the big idea that I should have a way to restart the game from points further along - checkpointing at the start of each level of the game. I discovered very quickly that this unplanned feature did not work well at all. Restarting at a checkpoint worked but game state was totally confused - player goals weren't being set up correctly and the game would became unwinnable. And a lot of this had to do with not having evolved clear responsibilities over game state, goal states and player states. I needed to fix the bug but I also needed to try to clean up.

It is easy to say you want to plan things so well that new features fall into place naturally. It is also easy to say you want to start with something small and refactor code as you go. But under time pressure you find out if you are a master or a rogue.

Under pressure I try to revert to simple design rules. Rules that always work and are always good even if you are tired. In this case I go with the rule of eliminating triplicates: It is great not to repeat yourself (the so-called DRY principle), but when you do something a third (or fourth or fifth) time you really are doing something provably bad: you are wasting the time you thought you saved not thinking about refactoring the first two instances of code repetition. 
  • There was only one place in the gameplay controller where the checkpoint reset would happen, and I realized I was being a chump and had just cut and pasted some player goal manipulations I was doing in three or four other places in the code - manipulations that needed to be done but weren't the whole story for a checkpoint reset. 
  • So I focused first on adding a method to the player object to take over the checkpoint reset responsibility...
  • THEN I went and cleaned up the other reset instances to use the new abstraction.
  • Deep down I knew there was a more deep seated issue with game state - but I leave it alone to be dealt with after the competition - for better or worse. 
Or so I think, I actually have pretty hazy memory. Like I said I was tired.

Aftermath:

The best test that things moved in the right direction is that on Monday (after the competition) I am recovered enough to port the game to run on the Web instead of having to download it...
  • The player's goal behaviors survive the tweaking process. 
  • Overall game state still does have to get fixed. I leave this for the October challenge (finish the game and put it up on an App Store somewhere) (update ... didn't happen...)
  • The math breaks (again)... in a really interesting way unique to the platform I used (Processing/Processing.js). 
  • I come up with a new rule to avoid this new interesting bug. 
I leave the understanding of this rule up to the user:
  • never (int) when you can floor()
Here's a hint: there is one thing even better than keeping track of what "this" is in Javascript and that is writing Javascript using macros that handle "this" for you (and erase casts to (int)).**
You can see the results and code here.

* except for that other one about Ninjas.
** even better is when you fix your crazy macro language written game and you see things working in a browser. Which means you can now easily run your game on display laptops at the store and let them grind to a halt playing games written in Javascript

No comments: