Snake cljs

2015-05-31

Interactive game development is awesome

Inspired by the Flappy bird demo, I wanted to try my hand on interactive game development with ClojureScript and Figwheel myself. It turns out interactive development truly is a game changer.

Here is the result, you play with the arrow keys (sorry, no touch support):

The source code can be found on Github.

This is what I learned

You need some way to restart the game state during development

Without user intervention, the snake will eventually hit a wall and die. You can either automatically restart the game or add a reset function that you manually call from the REPL. I opted for the latter, while also slowing down the game speed.

Interactive programming is awesome

Having the game update whenever the code changes is an eye-opener. Combined with the ability to mold new code snippets in the REPL, while evaluating them against the current state of the game makes for the shortest feedback loop I've ever experienced.

Canvas vs the DOM

The game is canvas based, which means that the entire game area is re-rendered at every frame. The graphics are simple square shapes which could easily have been drawn with HTML and CSS instead. Then I could have relied on React instead to render the game more efficiently. While it is fun to code against the canvas API, it is still at a lower level than the DOM, making you do more yourself.

Tests

In Bruce Haumann's Clojure west presentation he demonstrated running tests automatically in a separate tab and using the test tab's favicon color to indicate if tests failed or not. This worked great out of the box, so I'll definitely use that more in the future. Having tests is still valuable in an interactive programming environment, especially for a dynamic language such as ClojureScript.

Separating rendering and logic

When writing games, you want your game logic to be time-based instead of based on the frame rate. You want the game to run at a consistent speed across all devices. There are several ways to achieve this effect. With the snake game I render based on a getAnimationFrame loop. The game logic is in a separate core.async loop that is timeout-based. If the core.async timeout was consistent and reliable, this might have worked out great. Ideally, it enables me to avoid having to pass the delta time between two render invocations into the game logic function. But the game appears laggy at times, which makes me think this game loop separation was not a good idea. I'll have to look further into this issue.

Reloadable code

In order to have interactive programming, your code needs to be reloadable. With ClojureScript, this is way easier than with imperative languages, but there are still pitfalls to be aware of. In the snake game, the game loop runs through a core-async go-loop block. I had to add a designated stop channel in order to stop the go-loop when reloading the game.

(defn game-loop [stop-chan]
  "Separate game loop"
  (go-loop []
           (alt! (timeout 40) (do (swap! model update-model) (recur))
                 stop-chan (println "Stopping game loop"))))

;; Designated channel for stopping the game from Figwheel when reloading code
(defonce stop-game-chan (chan))                             
;; Start the game
(game-loop stop-game-chan)

When figwheel reloads the code, a message is put on the stop-channel which ends the go-loop. Without it, a new game loop would be added every time the code was reloaded.

Mutation from a distance

All of the game state lives in a single atom and mostly updated in the game loop. However, the keyboard events are fed in to a core.async channel and end up in a function that swaps the state of the atom from a distance. This is a pragmatic decision that works well in a small game. However, to be purely functional, changes to the atom should be isolated to a single place in the game loop.

Next

In my next game I'd like to try to avoid mutation from a distance by using the techniques from the excellent Purely functional retrogames series by James Hague.

CVTwitterLinkedinstackoverflowKodemakerFlickr500pxRSS