Picnic Defender (2011)

Picnic Defender was a browser-based tower defense game making heavy use of HTML5 technologies, including canvas, audio and websockets. Picnic Defender was featured on Chrome Experiments, where it was well-received (rated 4.5 out of 5 with over 150 votes).

I was solicited to write a chapter in the book HTML5 Games Most Wanted when the publisher saw Picnic Defender on Chrome Experiments. My chapter was a tutorial for creating a real-time multiplayer game using websockets in the browser and node.js on the server. For more information, see the project page.

You can still try Picnic Defender. This is a slightly stripped-down version of the game. Features which rely on a server (high scores, achievements, multiplayer) are all disabled, but the single player gameplay is fully intact.

Story

In 2011, I started to hear about the HTML canvas, so I experimented with it. In the course of an evening, I made some rotating shapes, and then some keyboard-controlled objects moving around, and then a very basic tower defense game. I've always liked tower defense games, but there were (are?) no good ones on the web, so I decided to flesh out the project. It was particularly exciting that I could make a web game while avoiding Flash, with its Adobe spyware, security holes, tendency to steal keyboard focus from the browser, etc. At that time, I had only a cursory knowledge of Javascript, so the project also represented an opportunity to learn the language.

With Firefox 4 and Chrome ~10, web browsers were just starting to come into their own, and canvas was just barely supported. Hardware acceleration of the canvas element has improved steadily since then, but canvas performance was always a concern when I was working on Picnic Defender. I employed various tricks to improve drawing performance, such as layering several canvases (so that the background would never have to be redrawn, for example) and only drawing at integer pixel coordinates to avoid unintended (and unconfigurable) anti-aliasing. Had I known then what I know now (2014), I would have made better use of non-canvas DOM elements.

Picnic Defender had a globally viewable leaderboard. In order to block spurious entries, it was necessary to validate the scores submitted from the browser. Any player with designs on cheating needed only to open the browser's Javascript console to inspect the source code and mess with the game's internals. The solution I eventually came up with was to send, not just the score, but a log of the user's actions to the server, and then to score the game in the unperturbed server environment. The log entries, when decoded, told a story like "at game frame 73, user placed a red tower at coordinates (5, 8)". There were typically only a few (<100) of these entries in a given session, and they defined the outcome of the game. Furthermore, there was no way to cheat your way to a high score without having access to a legitimately good game log.

The server-side scoring mechanism was simply to run the game while taking the actions dictated by the user's log. For the sake of simplicity and maintainability, it was important that the server-side game code be the same as the game code that ran in the browser. The idea of running Javascript code in a non-browser environment was (unbeknownst to me) really picking up steam at that time. I stumbled upon node.js but didn't understand, then, how to use it in my application. Next, I found Mozilla Rhino, which was incredibly slow and relied on Java. Finally, I found Google V8 and it was perfect. I used V8 to implement my scheme to protect against cheaters.

Multiplayer

I was eventually inspired to add a multiplayer component to the game. I wanted two players to be able to share a map, with both players capable of placing and selling towers in real time. This turned out to be among the most interesting and challenging projects I've set for myself.

How does one show the same thing to the two players? It's a very delicate thing, not simply a matter of having the same list of towers on the two game boards. If my tower shows up on my screen one tick of the game clock before it shows up on yours, it may kill an enemy that escapes for you. The game must be deterministic and synchronized.

The mechanism I settled on was an authoritative server to which both clients would report. When I went to build a tower, the game would tell the server "this guy wants to build a tower". The server would then send a message to all players saying "at tick 85, there's a new tower", where 85 is the time on the "real" game clock running on the server. If I receive that message from the server and my game time is 75, this works nicely. If my game clock reads 86, my client has to rewind to 85 and follow the server's directions (this is lag). In this way, the state is kept consistent across all players.

There were a few towers in Picnic Defender with randomized effects (e.g. dealing a random amount of damage, or shooting at a random enemy). In order to synchronize that randomness across multiple players, the server picked a seed integer and sent it to the clients, and the clients generated pseudo-random sequences according to a linear congruential generator.

Just before I was going to release the multiplayer feature, I played some long games to test it out. In spite of all the effort I had put in to keep things synchronized, the clients would still diverge if the game went long enough! This was very difficult to debug. I ended up keeping verbose logs of what each player's game was seeing and doing, playing a 10-minute game until something disagreed, and then painstakingly comparing the logs. In the end, I identified the root cause: enumerating the keys of an object in Javascript does not happen in a guaranteed order. I had been keeping a hash table of enemies, and towers would enumerate that list in order to pick their targets. When my browser enumerated the list in one way and your browser enumerated the list another way, they may pick different enemies to attack and thus change the outcome of the game.

Last updated 18 October 2014

comments powered by Disqus