There's a certain type of person who, upon seeing a new language, feels compelled to write a game with it. Functional languages represent a unique challenge in this respect, as has been written about at some length elsewhere. Clojure adds an interesting wrinkle to this problem, in the form of its Java interop. If a game consists of a thin layer of Clojure wrapped around a full-featured Java game engine, is it actually "written in Clojure"?
Without getting bogged down in semantics, I suggest that a Clojure application (such as the game discussed in this post) is something that can be easily extended using Clojure's data structures and concurrency primitives. This may or may not be true of the hypothetical Clojure-wrapped game engine; it depends on the design and implementation details.
To explore what goes into making a game in Clojure, we'll look at an implementation of Asteroids that uses Penumbra, an idiomatic wrapper for OpenGL. Penumbra was written to streamline doing interesting things with the graphics card, which includes small-scale game development. The issues I encountered while developing this game informed a number of design decisions I later made.
a description of the game
The game of Asteroids has three major elements: the spaceship, the bullets, and the eponymous asteroids. Optional elements include explosion effects for when an asteroid is split or the spaceship is destroyed, and exhaust effects to indicate thrust from the spaceship.
When implementing the game, we should first consider how each of these elements behave, as well as how they interact. Some of these details will vary between implementations, but for simplicity's sake we will assume the following:
- Bullets travel a straight, predictable path.
- If they come into contact with an asteroid, they blow up the asteroid and disappear.
- If they don't encounter an asteroid, they will eventually disappear.
- Asteroids travel a straight, predictable path.
- Asteroids come in three sizes: large, medium, and small.
- Asteroids explode when they come into contact with a bullet or the spaceship.
- When a large or medium asteroid explodes, it emits four smaller, faster asteroids in random directions. Small asteroids disappear altogether.
- The spaceship travels an unpredictable path dictated by user input.
- When the spaceship comes into contact with an asteroid, it explodes.
- The spaceship has a position and velocity.
- The position is constantly updated w.r.t. the velocity and elapsed time since the last update.
- When the engine is off, the velocity remains constant. When the engine is engaged, the velocity is constantly updated w.r.t. the orientation of the ship and the elapsed time since the last update.
- Exhaust particles travel a straight, predictable path.
- They are emitted whenever the ship's engine is engaged.
- They travel the opposite direction of the ship, adjusted for the ship's velocity (this is important; things will look very strange otherwise).
- They are purely for show, and don't interact with anything.
- After a while, they disappear.
- Explosion particles travel a straight, predictable path.
- They are emitted in random directions from the explosion's center.
- They are purely for show, and don't interact with anything.
- After a while, they disappear.
Looking at this list, we can see that four out of the five elements have paths that are predestined; their position at any given moment is determined the moment they are created. This means that while we could store their position in memory and update that position each frame, we don't have to. Instead, we have the option of defining a function which takes the game time as a parameter, and returns the current position.
This approach has two benefits. First, it's much less taxing to the garbage collector; while a perfect garbage collector would be able to handle constant, predictable churn by just allocating over the previous frame's state, reality is a little less kind. Updating too many things too often will lead to noticeable GC pauses, which any real-time application should strive to avoid. Second, it lets us treat the game as a constantly changing system which is sampled by the renderer, rather than a succession of snapshots whose frequency is arbitrarily dictated by the game's frame rate. Specific reasons for why this is desirable will be discussed later.
It's also obvious that the only difference between the exhaust and explosion particles are their initial conditions. As long as we make our generation method flexible enough, there's no reason to deal with these elements separately.
So to create the game, we need methods for collision testing, generation, physics (for the spaceship), and finally rendering. Below, we'll look at a few of the more interesting aspects. The complete source can be found here.
We need to be able to test collisions between asteroids and ships, and asteroids and bullets. Bullets are perfectly circular, but asteroids and the ship are irregular shapes, so doing this accurately could be pretty complicated. But if we just assume everything's a circle, it's easy:
(defn radius [x] (if (number? (:radius x)) (:radius x) ((:radius x)))) (defn position [x] (if (sequential? (:position x)) (:position x) ((:position x)))) (defn intersects? [a b] (let [min-dist (+ (radius a) (radius b)) dist (map - (position a) (position b))] (> (* min-dist min-dist) (length-squared dist))))
Notice that this code makes a number of assumptions about how the program will be structured: all of our game elements will be hashes, all of them will have
:radius keys, and those keys will either return static values or functions.
Nowhere do we define an interface that explicitly states this, nor can we tell at compile time whether the game elements actually satisfy these requirements. It's easy to see how this approach could cause issues in larger scale development, but it does give us a certain flexibility, and for a program this size, the complexity is manageable.
All of the elements exist on a 2-D plane, but that doesn't mean we can't render them as 3-D objects. We want our asteroids to be spherical, but irregular. We could randomly perturb the vertices, but that would just give us a spiky ball. The position of one vertex can't be independent of its neighbors. In other words, we want to generate a fractal asteroid.
There are a number of different ways to accomplish this, but one of the simplest is the fault line algorithm:
- Split the asteroid into equal halves.
- Expand everything in one half, and shrink everything in the other.
The resulting asteroid isn't too distinctive, so we can just generate a few asteroid templates, and choose one at random whenever we create a new asteroid.
Particles are soft-edged circles which, in large numbers, can be used to represent amorphous shapes. To create a texture, we define a texture which is perfectly white, but with varying transparency. Towards the middle, it is perfectly opaque, but falls off towards the edges per a Gaussian function.
(defn textured-quad  (draw-quads (texture 0 0) (vertex -1 -1) (texture 1 0) (vertex 1 -1) (texture 1 1) (vertex 1 1) (texture 0 1) (vertex -1 1))) (defn init-particles  (def particle-tex (let [tex (create-byte-texture 128 128)] (draw-to-texture! tex (fn [_ pos] (let [opacity (Math/exp (* -16 (length-squared (map - pos [0.5 0.5])))))] [1 1 1 opacity]))) tex)) (def particle-quad (create-display-list (textured-quad))))
Notice that we've wrapped the definitions of
particle-quad inside a function. This is because we need to have both of these executed inside an OpenGL context. This allows us to freely reference these vars in the rest of the code, as long as we make sure
init-particles is executed within an OpenGL context, and before any code that references the internal definitions.
Particles can fill a number of roles. Individual particles can represent bullets, and large quantities can be used to represent both explosions and the flame from the ship.
updating the game
Every frame, we need to determine the position of the various game elements, and draw them. We also need to update these elements - testing for collisions, generating particles when necessary - but this is a completely orthogonal concern to the rendering. Most games are single-threaded, and therefore the sequence of events is something like this:
- Test for user input, and change any related values.
- Update all the elements in the game, taking into consideration the elapsed time since we last updated the elements.
- Render the newly updated elements.
This artificially conflates the act of updating and rendering, but with Clojure, we're not nearly as constrained.
In Penumbra, an application is defined as a series of callbacks, such as
:display. With the exception of
:display, which cannot affect the game state, these are pure functions, which take the current state of the game, and return an altered version of that state. As a result, each function is independent of the others; as long as they don't execute at the same time, it doesn't matter what thread it's executing on.
These callbacks are called in response to user input (
:mouse-up) or a frame being rendered (
:display). However, how often we test for collisions shouldn't have to be coupled to how often we render, nor should how often we emit particles for the spaceship's exhaust. In fact, in the latter case this would only make things look strange: if our frame rate stutters, so would the exhaust. Our rendering should just be a periodic glance into the world of the game, not something that dictates its appearance.
Penumbra supports periodic updates, which are defined as pure functions which alter the state at regular intervals. We can test for collisions 10 times a second, and emit exhaust particles 50 times a second, regardless of the game's frame rate. This is used somewhat trivially in this game, but it's worth noting that this can be an extremely powerful capability: we can reason about the game as a collection of independent, asynchronous processes, without concern for the underlying implementation. In the Tetris example, for instance, the descent of the blocks is controlled by a periodic update which alters its own frequency based on whether the down arrow is pressed.
Brian Carper recently posted about his difficulties using Clojure's concurrency primitives during his implementation of an RPG. While I don't think the approach used by Penumbra is the only way forward, at the very least it demonstrates that it is quite possible to leverage Clojure's unique capabilities towards the develop of games or other real-time graphical applications.
I have recently released version 0.5.0 of Penumbra to clojars, which should greatly simplify its use in a separate project. If anyone has any questions regarding how they could use it for their project, I'd be happy to help.