Snake-duino: Hey look, a side project!
My desk has a new ornament on it: an Arduino powered lamp. And why does that merit a blog post? Because I built it.
After combining the Rainbowduino kit with some 600-ish points of solder, I found myself ruler of a 16MHz processor, 30Kb of instruction storage, 2Kb of data storage, and 64 tricolor pixels.
And what does one do with 64 pixels, no inputs, and processing power reminiscent of a vintage entertainment console? You write snake.
Snake, or “QBert Terrance Bloodthirster” as he was named by committee1, weighs in at roughly 1,200 lines of code, available here.
Debugging
Starting out I had suspected that the main issue during development would be the meager amount of memory available. As it turned out, that never became an issue. By far, the largest issue was attempting to debug.
As I wasn't aware of avr-gdb at the time, debugging was a slow and arduous process. Rather than the familiar process of following execution in a debugger, I had regressed to the age of log-driven debugging.
Thus, the process of trying to follow execution looked something like this:
Serial.println("Attempting to move outside of game grid.");
Serial.println(String(pos.z) + ", " + String(pos.x) + ", " + String(pos.y));
As if that wasn't enough fun, if a crash occurred on another line before the message finished flushing, the output would get truncated, resulting in messages like:
Attempting to mo�
Debugging was not fun2.
Snake Behavior
Writing a basic game of snake is rather simple. Making it visually interesting is considerably more of a challenge.
If the snake always wins or always loses, you can't feel any degree of investment. If he appears too predictable, he is a lifeless machine. If he appears too chaotic, he is unaware and unintentional.
In short, to remain interesting, the snake must appear intentional, but imperfect.
To balance these criteria, the snake has been given a number of different preferences. For instance “somewhat prefer straight lines", or “strongly avoid the dot if it is still fading in.” On each tick, his preferences are applied as weights to each potential move. He then moves in the direction of the highest weighted choice. This has the effect of making him appear unpredictable yet deliberate.
Display
If the behavior is interesting but the presentation is lackluster, the end result will be a rather unimpressive display. At first glance, it might seem that the draw cycle doesn't really provide us much opportunity to increase interest, but it turns out that there were actually several things we could do here to improve our game.
Draw Cycle Time Slicing
One of the first things that showed up was that the time it took to draw a pixel was non-negligible. As more pixels were drawn a noticeable delay was introduced. This caused the snake to move slower as he gots longer.
I avoided this by taking rendering time into account when delaying between frames.
uint32_t start = millis();
display->draw();
int32_t delay_time = FRAME_DELAY_MILLIS - (millis() - start);
int32_t safe_delay_time = min(max(delay_time, 0), FRAME_DELAY_MILLIS);
if (delay_time < 0) {
Serial.println("Can't keep up!");
}
delay(safe_delay_time);
With this change, the draw cycle would always take a fixed amount of time, in my case, 20 milliseconds.
Double Buffering
Another change with the display was to double buffer all drawings rather than to immediately draw them to the display. This allowed layers to be drawn on top of each other without causing a flickering effect. Previous to this, a considerable amount of logic was used to prevent overdrawing. As there was no longer a penalty for that, I was able to remove a great deal of the logic I had added.
This also allowed me the opportunity to draw only pixels that had changed since the previous frame. As only the head, tail, and dot ever changed, there would only ever be three pixels drawn in a given frame. This allowed the frame-rate to greatly increase.
Tweening Between Frames
This might seem like a minor detail, but I found it actually had a considerable effect on appeal: Since double buffering had drastically decreased the time spent drawing, I could actually draw extra transitions between frames without the game appearing any slower than I would like.
For each game tick, I added 10 draw cycles, each gradually fading any pixels that had changed. This immediately gave a more fluid, continous feel for the motion.3
Color Choices
Another useful change was making the snake multicolored. Apart from just making it look cooler, it also made it easier to visualize the snake's layout on the game grid whenever he would double back next to himself. This helped him to keep his appearance as an entity, rather than an amorphous blob.
Ooh… Shiny…
So, was it a success? Has it earned the prestigious place on my desk between the Legos and Minecraft figurines? I think so. I loved assembling it, and writing the software for it. It has been the subject of a few conversations, and I've had at least one coworker stop by and stare into it. I've even found myself relaxing with it too. So, yeah, I'd say that pretty successful by desk toy standards.
Footnotes
I had two friends over at the house and showed them what I was working on. We tried to decide what to name him, so we each contributed a name. I chose Terrance. Kids, beware of “Design by Committee”. ↩︎
The odd character at the end of the line is intentional. It looks like it died in mid-character. ↩︎
Unfortuantely, the attached video does not do a very good job of showing this. I'm guessing this is related to me turning the brightness way down on the recording. ↩︎