Clojure 2D Pong Game
Foreword
Let's make a Clojure Pong Game with 63 lines of code!
Drawing Stuff
Alright, let's get right to it. In order to draw things on the screen we will need some kind of drawing library. There are several options like using the Java Canvas or OpenGL. We will use the Quil library for Clojure.
The installation instructions can be found on the Quil site, but basically we just have to add the following 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]))
We also imported KeyEvent because we will need it later on.
Draw and Update
Every game needs some kind of draw and update function. The update function will calculate all the logic (like moving the ball), while the draw function throws everything at the screen.
Here is how we setup a quil sketch with the 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
(background-float 0x20)
(fill 0xff))
; input
(defn key-pressed []
; nothing to check yet...
)
; run
(defsketch pong
:title "noobtuts.com 2d pong game"
:size [450 200]
:setup (fn [] (smooth) (no-stroke) (frame-rate 60))
:draw (fn [] (update) (draw))
:key-pressed key-pressed)
Just the usual quil setup stuff, so far so good. One thing to note here is that quil usually doesn't have a update method. Instead we sneaked 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))
The Rackets
Alright, time to create some rackets. In the end, our rackets are really just rectangles. There are all kinds of ways to define a rectangle, but we will use the following form:
{:x 0 ; x coordinate
:y 0 ; y coordinate
:w 10 ; width
:h 20} ; height
To make our lives easier later on, we will also define a draw-rect function that uses quil's rect function to draw our rectangle:
(defn draw-rect [r]
(rect (:x r) (:y r) (:w r) (:h r)))
So let's define the left and the right racket in that form. We have to keep in mind that the racket's position will change as soon as the user presses a button. So we have to use one of Clojure's constructs that allow 'changes'. For simplicity's sake, we will use atoms:
(def r-left (atom {:x 10 :y 65 :w 10 :h 70}))
(def r-right (atom {:x 430 :y 65 :w 10 :h 70}))
And here is how to work with them:
; we can access atoms like this:
@r-left
; access left racket's x coordinate:
(:x @r-left)
; increase the x coordinate by one:
(swap! r-left update-in [:x] inc)
Now we modify our draw function to draw both rackets:
(defn draw []
; set background color to dark gray, draw color to white
(background-float 0x20)
(fill 0xff)
; draw rackets
(draw-rect @r-left)
(draw-rect @r-right))
Which results in our two rackets being drawn on the screen:
We will use the previously created key-pressed function to move the rackets. The game is supposed to be a two player game where the left player uses the W and S keys and the right player uses the UP and DOWN keys. So let's find out if those keys were pressed, and in that case modify the racket's vertical position (which is y):
(defn key-pressed []
(cond
; left
(= (key-code) KeyEvent/VK_W)
(swap! r-left update-in [:y] dec)
(= (key-code) KeyEvent/VK_S)
(swap! r-left update-in [:y] inc)
; right
(= (key-code) KeyEvent/VK_UP)
(swap! r-right update-in [:y] dec)
(= (key-code) KeyEvent/VK_DOWN)
(swap! r-right update-in [:y] inc)))
Note: a more elegant way to do this would be by using case, which however does not work properly when comparing the KeyEvents.
Now we can move the rackets up and down:
The Ball
No Pong Game works without a ball, so let's define another racket:
(def ball (atom {:x 225 :y 100 :w 10 :h 10}))
We also have to keep track of the ball's current movement direction so we can move it a bit towards that direction in every update call. So let's use a vector to store the direction (again stored in atom because we need to change it sometimes):
; initially it flies to the right, hence why x=1 and y=0
(def ball-dir (atom [1 0]))
Okay so we have the ball and we have the ball direction. We will need a function that takes the two and calculates the ball's new position after moving one step into the direction. Let's create a next-ball function that does just that:
; here is the easy way to do it:
(defn next-ball [ball dir]
(let [dx (first dir)
dy (second dir)]
(assoc ball :x (+ (:x ball) dx)
:y (+ (:y ball) dy))))
; the more elegant way is by using destructuring:
(defn next-ball [b [dx dy]]
(assoc b :x (+ (:x b) dx)
:y (+ (:y b) dy)))
; usage:
(next-ball {:x 0 :y 0 :w 10 :h 10} [1 -1])
; => {:y -1, :x 1, :h 10, :w 10}
Note: destructuring is a weird looking but very powerful concept. It allows us to directly access a structure's components without using first, second and so on.
So what happens is that we pass the ball rectangle and the dir vector to the function and it then creates a new ball by using the assoc function to return the same ball with different x and y coordinates. They are different because the x and y direction were added to them.
Finally we just use our next-ball function in the update function:
; update
(defn update []
; move the ball into its direction
(swap! ball next-ball @ball-dir))
Note: swap applies the next-ball function to the current ball, with the ball-direction as parameter. And we used @ball-dir because the ball-dir is an atom and this is how we access atoms.
And we modify our draw function again in order to draw the ball:
; draw
(defn draw []
; set background color to dark gray, draw color to white
(background-float 0x20)
(fill 0xff)
(draw-rect @r-left)
(draw-rect @r-right)
(draw-rect @ball))
If we run everything again then we can see the ball moving:
Collisions
Alright, the last part of our game requires some math, but nothing that we didn't learn in school already.
We will have to detect if the ball did hit the left or right racket, or the top or bottom wall. Here is what we do in each case:
- Collision with left racket: let it fly to the right and set the vertical (y) direction depending on where it hit the racket (so it also flies up and down and not just to the left and to the right).
- Collision with right racket: same as left racket, just to the opposite direction.
- Collision with top wall: invert the ball's vertical fly direction (y).
- Collision with bottom wall: invert the ball's vertical fly direction (y).
So let's start by implementing a hitfactor function. It takes a racket and the ball as parameters and then returns a certain ratio, depending on where the ball did hit the racket:
; calculate the 'hit factor' between racket and ball
; => it's 0.5 if hit at the top of the racket
; => it's 0 if hit at the middle of the racket
; => it's -0.5 if hit at the bottom of the racket
(defn hitfactor [r b]
(- (/ (- (:y b) (:y r))
(:h r))
0.5))
Like so often, the code looks really weird first. It's really easy to understand though. At first we calculated how far the ball is away to the racket's vertical center with:
(- (:y b) (:y r)) ; ball.y - racket.y
; => gives for example 30px or -20px
Then we calculated the ratio between that distance and the racket's height, so it's something between 0 (hit at the top) and 1 (hit at the bottom):
(/ (- (:y b) (:y r))
(:h r)) ; <- divided by racket.height
; ascii art:
|| 0 <- at the top of the racket
||
|| 0.5 <- at the middle of the racket
||
|| 1 <- at the bottom of the racket
And finally we subtracted 0.5 from it so the value is always between -0.5 and +0.5:
(- (/ (- (:y b) (:y r))
(:h r))
0.5)
; ascii art:
|| -0.5 <- at the top of the racket
||
|| 0 <- at the middle of the racket
||
|| +0.5 <- at the bottom of the racket
Note: we prefer -0.5 and +0.5 so the ball will fly 45° upwards / horizontal / 45° downwards. When using a value between 0 and 1 it would fly horizontal / 45° downwards / 90° downwards.
Finally we modify our update function again. We will check for the previously mentioned collisions and then change the ball's direction accordingly:
; update
(defn update []
; move the ball into its direction
(swap! ball next-ball @ball-dir)
; ball hit top or bottom border?
(when (or (> (:y @ball) 200) (< (:y @ball) 0))
; invert y direction
(swap! ball-dir (fn [[x y]] [x (- y)])))
; ball hit the left racket?
(when (rect-intersects @r-left @ball)
(let [t (hitfactor @r-left @ball)]
; invert x direction, set y direction to hitfactor
(swap! ball-dir (fn [[x _]] [(- x) t]))))
; ball hit the right racket?
(when (rect-intersects @r-right @ball)
(let [t (hitfactor @r-right @ball)]
; invert x direction, set y direction to hitfactor
(swap! ball-dir (fn [[x _]] [(- x) t])))))
Note: rect-intersects is the default rectangle intersection test. It can be found in the project files for this tutorial.
Summary
Improvements
We just created a very simple Pong Game in Clojure. There are tons of optimizations and game play features that can be added, for example a score that gets updated whenever the ball hits the right or left border of the game. Or perhaps a fancy particle trail behind the ball, or some hit sounds and some AI for an enemy.
About State
We used a lot of state (the ball and the racket atoms) in our game, because this is the common way when it comes to quil. This is fine if you think it's fine, however some of us like to keep our code as functional (and stateless) as possible.
There is a pattern that allows us to store our rackets and the ball without defining them globally. The pattern was explained in our Handling Clojure state the Erlang way Tutorial.
So for those who love the functional stuff, feel free to modify the game to work without atoms and state. It can be a great learning experience!
Download Source Code & Project Files
The Clojure 2D Pong Game source code & project files can be downloaded by Premium members.All Tutorials. All Source Codes & Project Files. One time Payment.
Get Premium today!