Clojure 2D Snake Game
Foreword
In this Tutorial we will make a snake game in Clojure with only 43 lines of code.
The right drawing library
We will need a way to draw our graphics onto the screen. There are a lot of options like using the Java Canvas or OpenGL. But let's use the Quil library in this tutorial. It wasn't designed for games, but it's good enough.
Quil is very easy to set up, all we have to do is add this line to our Leiningen project:
[quil "1.7.0"]
And then require it in our namespace:
(ns noobtuts.core
(:import [java.awt.event KeyEvent])
(:use [quil.core :as q]))
The KeyEvent class will be needed later, so we might as well import it already.
The Game Loop
There are two basic things that happen in our snake game. Mechanics and game logics are calculated ('updated') and things are then drawn on the screen, so let's create a draw and update function.
At first we will set up a quil sketch that works with our draw and update functions. We will also add a key-pressed function so we get noticed when the user presses key:
; update
(defn update []
; nothing to update yet..
)
; draw
(defn draw []
; set background color to dark gray, draw color to white
(q/background-float 0x20))
; input
(defn key-pressed []
; nothing to check yet...
)
; run
(q/defsketch snake
:title "noobtuts.com 2d snake game"
:size [450 200]
:setup (fn [] (q/smooth) (q/no-stroke) (q/frame-rate 60))
:draw (fn [] (update) (draw))
:key-pressed key-pressed)
This is more or less the common quil setup. The only unusual thing is that quil usually doesn't have a update function. Instead we kinda added it into our :draw property:
; usually it looks like this:
:draw draw-function-here
; but we run update and draw every time:
:draw (fn []
(update)
(draw))
Helper Functions
Okay so we are almost ready to start working on the game play. We do however need two little functions that will make our lives a lot easier later on:
; add two vectors
; (v+ [1 2] [1 1]) => [2 3]
(def v+ (partial mapv +))
; given a percent chance, decide if a event happens
; (askoracle 0.5) => true
; (askoracle 0.5) => false
(defn askoracle [n]
(< (rand) n))
The v+ (also known as vector-add) function adds two vectors together. An in-depth explanation can be found in our Clojure Vector-2, Vector-3 & Vector-N Tutorial.
The askoracle function is just basic probability. It will be used to decide whether or not a certain event happened. It's like rolling a dice (in which case we would do (askoracle 1/6)), or like asking an oracle like our ancestors used to do.
Alright those are all the helper functions that we will need, time for some game play.
Spawning Food
Food as Position
We will have several food units (in other words: meals), that will be in the game world. As usual there are all kinds of representations, but all we really need to know about a meal is the position in the game world. And since we want to make a 2D game, we will just use a vector with an x and y coordinate like this:
; a meal at position x=5 y=10:
[5 10]
The Food Container
Now to keep track of all the meals, we will need some kind of data structure. Clojure offers a variety of Data Structures, so let's think about which one suits our needs here.
Our food doesn't really need any specific order, so we don't really need a vector or a list here. Our only requirements are:
- put a meal in
- find out if there is a meal at a certain position
- remove a meal
A set allows us to do exactly that! Here is how we use a Clojure set:
; create a set with the values 1 and 2
#{1 2}
; add the value 3
(conj #{1 2} 3) ; => #{1 2 3}
; check if it contains the value 3
(contains? #{1 2 3} 3) ; => true
; remove the value 3
(disj #{1 2 3} 3) ; => #{1 2}
Defining the Food
Alright so let's add food to our game. As mentioned above, our food is just a set. But we also need the ability to 'change' it (by spawning more food), so we will use an atom:
; food (as set of points)
(def food (atom #{}))
This is how we work with atoms:
; we can access atoms like this:
@food
; find out if it contains a meal at position [0 0]
(contains? @food [0 0])
; add a meal at position [2 3]
(swap! food conj [2 3])
Spawning the Food
Thanks to our askoracle function, spawning food will be very easy. All we have to do is add two lines of code to our update function:
; update
(defn update []
; spawn food
(when (askoracle 0.005)
(swap! food conj [(rand-int 450) (rand-int 200)])))
Given a 0.5% chance, this spawns food at a random position where x is between 0 and 450 and y is between 0 and 200 (which equals our screen's width and height).
Drawing the Food
Time to throw some stuff at the screen. Here is how we modify our draw function to show the food in a blue color:
; draw
(defn draw []
; set background color to dark gray
(q/background-float 0x20)
; draw food
(q/stroke 0x00 0xff 0xff)
(doseq [[x y] @food] (q/point x y)))
Note: this works because q/stroke sets the point draw color to the rgb code 0x00ffff which is some blue tone, and then we draw each meal by running through @food with doseq, where we use q/point to draw each point.
If we run the game and wait a while, we will eventually get a whole lot of food on our screen:
The Snake
Representation
Just like our food, the Snake will be a bunch of points in our 2D world. The head will be the first point, then there will be a few more points and at the end there is the tail, which is yet another point.
This time we do need some kind of order in our data structure, because whenever the snake eats something, we want to add it to the front of the snake (not anywhere in between).
The most common Clojure data structure with some kind of order is the vector, which will work just fine for us.
Let's define the snake (the vector of points):
; snake (as vector of points because we want to append)
; => obviously needs a start position
(def snake (atom [[50 50]]))
Note: we used an atom again because we need to change the snake every now and then.
The snake already consists of one point at the position [50 50]. This is important because the snake head is always in the game from the beginning.
Our snake will move into a certain direction all the time, so let's also define a direction of type [x y]:
; snake movement direction
(def snake-dir (atom [1 0]))
The initial direction is [1 0] which means that in every update call it should go 1 step towards the x direction and 0 steps towards the y direction. Or in other words: it walks to the right.
Snake Movement
If our snake already ate a meal three times, then it would look like this (as text version):
o
o
o
o
If the player decides to move the snake to the right then it would look like this the next time:
oo
o
o
When we think about how to do this algorithmically, the obvious thing that comes to mind is to first moves the snake's head to the new position and then make every other entry in our snake vector follow the snake's head by one step.
But this sounds like a complicate thing to do, so we will apply a little trick: we will not move all the snake elements every time, instead we will simply remove the last element and put it to the new position.
This is how the trick will look like with our beautiful text snake, this time "x" is the last element:
o
o
o
x
So every time the player decides to move the snake to the right, we will just remove the x from the end and put it to the new position:
ox
o
o
This way it seems like the whole snake moved, even though we just removed the last element and made it the new head.
Note: this also introduces some performance benefits. Instead of performing "n" calculations ("n" is the snake length) we only do 2 calculations in every update call.
Alright so let's talk about some code. We can calculate the new snake position by adding the snake's head with the snake-dir:
(v+ (first @snake) @snake-dir)
We can get rid of the last snake element with pop. This is how our modified update function looks like:
; update
(defn update []
; update snake
(let [new (v+ (first @snake) @snake-dir)] ; new head
(swap! snake #(vec (cons new (pop %))))) ; head+rest
; spawn food
(when (askoracle 0.005)
(swap! food conj [(rand-int 450) (rand-int 200)])))
Note: this works because (cons new (pop %)) replaces the snake with a new snake that consists of the new element and the rest of the old snake, but without the last element (hence pop).
Drawing the Snake
We already know how to draw a bunch of points on the screen, so drawing the snake will be very straight forward:
; draw
(defn draw []
; set background color to dark gray
(q/background-float 0x20)
; draw snake
(q/stroke 0xff)
(doseq [[x y] @snake] (q/point x y))
; draw food
(q/stroke 0x00 0xff 0xff)
(doseq [[x y] @food] (q/point x y)))
If we run everything again then we can see the snake move to the right:
Controlling the Snake
Right now our snake game is still kinda boring, because the player can't control the snake's movement direction. Luckily for us, this is very easy to change. All we have to do is modify our key-pressed function and look for the UP, DOWN, LEFT and RIGHT arrows:
; input
(defn key-pressed []
(cond ; case doesn't work with the KeyEvents
(= (q/key-code) KeyEvent/VK_UP)
(reset! snake-dir [0 -1])
(= (q/key-code) KeyEvent/VK_DOWN)
(reset! snake-dir [0 1])
(= (q/key-code) KeyEvent/VK_LEFT)
(reset! snake-dir [-1 0])
(= (q/key-code) KeyEvent/VK_RIGHT)
(reset! snake-dir [1 0]))))
Whenever one of the arrows was pressed, the snake's movement direction will be changed. And that's all there is to snake movement. If we run the game again then we can now control the snake with the arrow keys.
Feeding the Snake
There is one more feature to add to our game. The snake is supposed to eat the food whenever the snake's head is on top of a meal. And if so then the meal should be removed from the food set and be made the new head of the snake (or in other words, added to the front of it).
So at first we will have to find a way to 'check for eating' whenever the snake moved. We could either do this in our update function, or we could add a watch to our snake atom. Watches are a neat feature because once added to an atom, they will be called whenever that atom changes. This sounds just like what we need.
Here is how we can add a watch to our snake atom:
(add-watch snake :key (fn [k r os ns] (prn k r os ns)))
Note: the four parameters are the key, the reference, the old-state (before the change), and the new-state (after the change).
Now all we have to do is add our 'eat meal' logic in there:
; let the snake eat food
(add-watch snake :key
(fn [k r os ns]
; any meal under snake head?
(let [meal (get @food (first ns))]
(when meal
(swap! food disj meal) ; remove from food
(swap! snake #(vec (cons meal %))))))) ; add to snake
Summary
We just created a very simple snake game in Clojure. We learned how to draw stuff on the screen, how to use atoms and watches and how to decide which of Clojure's data structures to use.
Improvements
Now it's up to you to make the game fun. Add a score or a survival timer. Make the player lose the game when it collides with the snake or with the wall. Maybe add some sound.
About elegance
The above code is not the most elegant solution. The only goal of this tutorial is to explain the quick and dirty way that allows us to make our own game in just a few lines of code.
If this is not enough of a reward for you and you want to make the perfect snake game, feel free to give our Handling Clojure state the Erlang way Tutorial a look to learn how to make the game code more elegant with less global definitions and such.
Download Source Code & Project Files
The Clojure 2D Snake Game source code & project files can be downloaded by Premium members.All Tutorials. All Source Codes & Project Files. One time Payment.
Get Premium today!