In the previous chapter, we learned how a framework for Compositional Event Systems (CES) works by building our own framework, which we called respondent. It gave us a great insight into the main abstractions involved in such a piece of software as well as a good overview of core.async
, Clojure's library for asynchronous programming and the foundation of our framework.
Respondent is but a toy framework, however. We paid little attention to cross-cutting concerns such as memory efficiency and exception handling. That is okay as we used it as a vehicle for learning more about handling and composing event systems with core.async
. Additionally, its design is intentionally similar to Reagi's design.
In this chapter, we will:
core.async
Have you ever played Asteroids? If you haven't, Asteroids is an arcade space shooter first released by Atari in 1979. In Asteroids, you are the pilot of a ship flying through space. As you do so, you get surrounded by asteroids and flying saucers you have to shoot and destroy.
Developing the whole game in one chapter is too ambitious and would distract us from the subject of this book. We will limit ourselves to making sure we have a ship on the screen we can fly around as well as shoot space bullets into the void. By the end of this chapter, we will have something that looks like what is shown in the following screenshot:
To get started, we will create a newClojureScript
project using the same leiningen template we used in the previous chapter, cljs-start
(see https://github.com/magomimmo/cljs-start):
lein new cljs-start reagi-game
Next, add the following dependencies to your project file:
[org.clojure/clojurescript "0.0-2138"] [reagi "0.10.0"] [rm-hull/monet "0.1.12"]
The last dependency, monet (see https://github.com/rm-hull/monet), is a ClojureScript library you can use to work with HTML 5 Canvas. It is a high-level wrapper on top of the Canvas API and makes interacting with it a lot simpler.
Before we continue, it's probably a good idea to make sure our setup is working properly. Change into the project directory, start a Clojure REPL, and then start the embedded web server:
cd reagi-game/ lein repl Compiling ClojureScript. Compiling "dev-resources/public/js/reagi_game.js" from ("src/cljs" "test/cljs" "dev-resources/tools/repl")... user=> (run) 2014-06-14 19:21:40.381:INFO:oejs.Server:jetty-7.6.8.v20121106 2014-06-14 19:21:40.403:INFO:oejs.AbstractConnector:Started [email protected]:3000 #<Server org.eclipse.jetty.server.Server@51f6292b>
This will compile the ClojureScript source files to JavaScript and start the sample web server. In your browser, navigate to http://localhost:3000/
. If you see something like the following, we are good to go:
As we will be working with HTML 5 Canvas, we need an actual canvas to render to. Let's update our HTML document to include that. It's located under dev-resources/public/index.html
:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>bREPL Connection</title>
<!--[if lt IE 9]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<canvas id="canvas" width="800" height="600"></canvas>
<script src="js/reagi_game.js"></script>
</body>
</html>
We have added a canvas
DOM element to our document. All rendering will happen in this context.
Our game will have only two entities: one representing the spaceship and the other representing bullets. To better organize the code, we will put all entity-related code in its own file, src/cljs/reagi_game/entities.cljs
. This file will also contain some of the rendering logic, so we'll need to require monet
:
(ns reagi-game.entities (:require [monet.canvas :as canvas] [monet.geometry :as geom]))
Next, we'll add a few helper functions to avoid repeating ourselves too much:
(defn shape-x [shape] (-> shape :pos deref :x)) (defn shape-y [shape] (-> shape :pos deref :y)) (defn shape-angle [shape] @(:angle shape)) (defn shape-data [x y angle] {:pos (atom {:x x :y y}) :angle (atom angle)})
The first three functions are simply a shorter way of getting data out of our shape data structure. The shape-data
function creates a structure. Note that we are using atoms
, one of Clojure's reference types, to represent a shape's position and angle.
This way, we can safely pass our shape data into monet's rendering functions and still be able to update it in a consistent way.
Next up is our ship constructor function. This is where the bulk of the interaction with monet happens:
(defn ship-entity [ship] (canvas/entity {:x (shape-x ship) :y (shape-y ship) :angle (shape-angle ship)} (fn [value] (-> value (assoc :x (shape-x ship)) (assoc :y (shape-y ship)) (assoc :angle (shape-angle ship)))) (fn [ctx val] (-> ctx canvas/save (canvas/translate (:x val) (:y val)) (canvas/rotate (:angle val)) (canvas/begin-path) (canvas/move-to 50 0) (canvas/line-to 0 -15) (canvas/line-to 0 15) (canvas/fill) canvas/restore))))
There is quite a bit going on, so let's break it down.
canvas/entity
is a monet constructor and expects you to provide three arguments that describe our ship: its initial x, y coordinates and angle, an update function that gets called in the draw loop, and a draw function that is responsible for actually drawing the shape onto the screen after each update.
The update function is fairly straightforward:
(fn [value] (-> value (assoc :x (shape-x ship)) (assoc :y (shape-y ship)) (assoc :angle (shape-angle ship))))
We simply update its attributes to the current values from the ship's atoms.
The next function, responsible for drawing, interacts with monet's API more heavily:
(fn [ctx val] (-> ctx canvas/save (canvas/translate (:x val) (:y val)) (canvas/rotate (:angle val)) (canvas/begin-path) (canvas/move-to 50 0) (canvas/line-to 0 -15) (canvas/line-to 0 15) (canvas/fill) canvas/restore))
We start by saving the current context so that we can restore things such as drawing style and canvas positioning later. Next, we translate the canvas to the ship's x,y coordinates and rotate it according to its angle. We then start drawing our shape, a triangle, and finish by restoring our saved context.
The next function also creates an entity, our bullet:
(declare move-forward!) (defn make-bullet-entity [monet-canvas key shape] (canvas/entity {:x (shape-x shape) :y (shape-y shape) :angle (shape-angle shape)} (fn [value] (when (not (geom/contained? {:x 0 :y 0 :w (.-width (:canvas monet-canvas)) :h (.-height (:canvas monet-canvas))} {:x (shape-x shape) :y (shape-y shape) :r 5})) (canvas/remove-entity monet-canvas key)) (move-forward! shape) (-> value (assoc :x (shape-x shape)) (assoc :y (shape-y shape)) (assoc :angle (shape-angle shape)))) (fn [ctx val] (-> ctx canvas/save (canvas/translate (:x val) (:y val)) (canvas/rotate (:angle val)) (canvas/fill-style "red") (canvas/circle {:x 10 :y 0 :r 5}) canvas/restore))))
As before, let's inspect the update
and drawing
functions. We'll start with update
:
(fn [value] (when (not (geom/contained? {:x 0 :y 0 :w (.-width (:canvas monet-canvas)) :h (.-height (:canvas monet-canvas))} {:x (shape-x shape) :y (shape-y shape) :r 5})) (canvas/remove-entity monet-canvas key)) (move-forward! shape) (-> value (assoc :x (shape-x shape)) (assoc :y (shape-y shape)) (assoc :angle (shape-angle shape))))
Bullets have a little more logic in their update function. As you fire them from the ship, we might create hundreds of these entities, so it's a good practice to get rid of them as soon as they go off the visible canvas area. That's the first thing the function does: it uses geom/contained?
to check whether the entity is within the dimensions of the canvas, removing it when it isn't.
Different from the ship, however, bullets don't need user input in order to move. Once fired, they move on their own. That's why the next thing we do is call move-forward!
We haven't implemented this function yet, so we had to declare it beforehand. We'll get to it.
Once the bullet's coordinates and angle have been updated, we simply return the new entity.
The draw function is a bit simpler than the ship's version mostly due to its shape being simpler; it's just a red circle:
(fn [ctx val] (-> ctx canvas/save (canvas/translate (:x val) (:y val)) (canvas/rotate (:angle val)) (canvas/fill-style "red") (canvas/circle {:x 10 :y 0 :r 5}) canvas/restore))
Now, we'll move on to the functions responsible for updating our shape's coordinates and angle, starting with move!
:
(def speed 200) (defn calculate-x [angle] (* speed (/ (* (Math/cos angle) Math/PI) 180))) (defn calculate-y [angle] (* speed (/ (* (Math/sin angle) Math/PI) 180))) (defn move! [shape f] (let [pos (:pos shape)] (swap! pos (fn [xy] (-> xy (update-in [:x] #(f % (calculate-x (shape-angle shape)))) (update-in [:y] #(f % (calculate-y (shape-angle shape)))))))))
To keep things simple, both the ship and bullets use the same speed value to calculate their positioning, here defined as 200
.
move!
takes two arguments: the shape map and a function f
. This function will either be the +
(plus) or the -
(minus) function, depending on whether we're moving forward or backward, respectively. Next, it updates the shape's x,y coordinates using some basic trigonometry.
If you're wondering why we are passing the plus and minus functions as arguments, it's all about not repeating ourselves, as the next two functions show:
(defn move-forward! [shape] (move! shape +)) (defn move-backward! [shape] (move! shape -))
With movement taken care of, the next step is to write the rotation functions:
(defn rotate! [shape f] (swap! (:angle shape) #(f % (/ (/ Math/PI 3) 20)))) (defn rotate-right! [shape] (rotate! shape +)) (defn rotate-left! [shape] (rotate! shape -))
So far, we've got ship movement covered! But what good is our ship if we can't fire bullets? Let's make sure we have that covered as well:
(defn fire! [monet-canvas ship] (let [entity-key (keyword (gensym "bullet")) data (shape-data (shape-x ship) (shape-y ship) (shape-angle ship)) bullet (make-bullet-entity monet-canvas entity-key data)] (canvas/add-entity monet-canvas entity-key bullet)))
The fire!
function takes two arguments: a reference to the game canvas and the ship. It then creates a new bullet by calling make-bullet-entity
and adds it to the canvas.
Note how we use Clojure's gensym
function to create a unique key for the new entity. We use this key to remove an entity from the game.
This concludes the code for the entities
namespace.
gensym
is quite heavily used in writing hygienic macros as you can be sure that the generated symbols will not clash with any local bindings belonging to the code using the macro. Macros are beyond the scope of this book, but you might find this series of macro exercises useful in the learning process, at https://github.com/leonardoborges/clojure-macros-workshop.
We're now ready to assemble our game. Go ahead and open the core namespace file, src/cljs/reagi_game/core.cljs
, and add the following:
(ns reagi-game.core (:require [monet.canvas :as canvas] [reagi.core :as r] [clojure.set :as set] [reagi-game.entities :as entities :refer [move-forward! move-backward! rotate-left! rotate-right! fire!]]))
The following snippet sets up various data structures and references we'll need in order to develop the game:
(def canvas-dom (.getElementById js/document "canvas")) (def monet-canvas (canvas/init canvas-dom "2d")) (def ship (entities/shape-data (/ (.-width (:canvas monet-canvas)) 2) (/ (.-height (:canvas monet-canvas)) 2) 0)) (def ship-entity (entities/ship-entity ship)) (canvas/add-entity monet-canvas :ship-entity ship-entity) (canvas/draw-loop monet-canvas)
We start by creating monet-canvas
from a reference to our canvas
DOM element. We then create our ship data, placing it at the center of the canvas, and add the entity to monet-canvas
. Finally, we start a draw-loop, which will handle our animations using the browser's native capabilities—internally it calls window.requestAnimationFrame()
, if available, but it falls back to window.setTimemout()
otherwise.
If you were to try the application now, this would be enough to draw the ship on the middle of the screen, but nothing else would happen as we haven't started handling user input yet.
As far as user input goes, we're concerned with a few actions:
To account for these actions, we'll define some constants that represent the ASCII codes of the keys involved:
(def UP 38) (def RIGHT 39) (def DOWN 40) (def LEFT 37) (def FIRE 32) ;; space (def PAUSE 80) ;; lower-case P
This should look sensible as we are using the keys traditionally used for these types of actions.
One of the things discussed in the earlier chapters is that if you can think of events as a list of things that haven't happened yet; you can probably model it as an event stream. In our case, this list is composed by the keys the player presses during the game and can be visualized like so:
There is a catch though. Most games need to handle simultaneously pressed keys.
Say you're flying the spaceship forwards. You don't want to have to stop it in order to rotate it to the left and then continue moving forwards. What you want is to press left at the same time you're pressing up and have the ship respond accordingly.
This hints at the fact that we need to be able to tell whether the player is currently pressing multiple keys. Traditionally this is done in JavaScript by keeping track of which keys are being held down in a map-like object, using flags. Something similar to the following snippet:
var keysPressed = {}; document.addEventListener('keydown', function(e) { keysPressed[e.keyCode] = true; }, false); document.addEventListener('keyup', function(e) { keysPressed[e.keyCode] = false; }, false);
Then, later in the game loop, you would check whether there are multiple keys being pressed:
function gameLoop() { if (keyPressed[UP] && keyPressed[LEFT]) { // update ship position } // ... }
While this code works, it relies on mutating the keysPressed
object which isn't ideal.
Additionally, with a setup similar to the preceding one, the keysPressed
object is global to the application as it is needed both in the keyup
/keydown
event handlers as well as in the game loop itself.
In functional programming, we strive to eliminate or reduce the amount of global mutable state in order to write readable, maintainable code that is less error-prone. We will apply these principles here.
As seen in the preceding JavaScript example, we can register callbacks to be notified whenever a keyup
or keydown
event happens. This is useful as we can easily turn them into event streams:
(defn keydown-stream [] (let [out (r/events)] (set! (.-onkeydown js/document) #(r/deliver out [::down (.-keyCode %)])) out)) (defn keyup-stream [] (let [out (r/events)] (set! (.-onkeyup js/document) #(r/deliver out [::up (.-keyCode %)])) out))
Both keydown-stream
and keyup-stream
return a new stream to which they deliver events whenever they happen. Each event is tagged with a keyword, so we can easily identify its type.
We would like to handle both types of events simultaneously and as such we need a way to combine these two streams into a single one.
There are many ways in which we can combine streams, for example, using operators such as zip
and flatmap
. For this instance, however, we are interested in the merge
operator. merge
creates a new stream that emits values from both streams as they arrive:
This gives us enough to start creating our stream of active keys. Based on what we have discussed so far, our stream looks something like the following at the moment:
(def active-keys-stream (->> (r/merge (keydown-stream) (keyup-stream)) ... ))
To keep track of which keys are currently pressed, we will use a ClojureScript set. This way we don't have to worry about setting flags to true or false—we can simply perform standard set operations and add/remove keys from the data structure.
The next thing we need is a way to accumulate the pressed keys into this set as new events are emitted from the merged stream.
In functional programming, whenever we wish to accumulate or aggregate some type of data over a sequence of values, we use reduce
.
Most—if not all—CES frameworks have this function built-in. RxJava calls it scan
. Reagi, on the other hand, calls it reduce
, making it intuitive to functional programmers in general.
That is the function we will use to finish the implementation of active-keys-stream
:
(def active-keys-stream (->> (r/merge (keydown-stream) (keyup-stream)) (r/reduce (fn [acc [event-type key-code]] (condp = event-type ::down (conj acc key-code) ::up (disj acc key-code) acc)) #{}) (r/sample 25)))
r/reduce
takes three arguments: a reducing function, an optional initial/seed value, and the stream to reduce over.
Our seed value is an empty set as initially the user hasn't yet pressed any keys. Then, our reducing function checks the event type, removing or adding the key from/to the set as appropriate.
As a result, what we have is a stream like the one represented as follows:
The ground work we've done so far will make sure we can easily handle game events in a clean and maintainable way. The main idea behind having a stream representing the game keys is that now we can partition it much like we would a normal list.
For instance, if we're interested in all events where the key pressed is UP
, we would run the following code:
(->> active-keys-stream (r/filter (partial some #{UP})) (r/map (fn [_] (.log js/console "Pressed up..."))))
Similarly, for events involving the FIRE
key, we could do the following:
(->> active-keys-stream (r/filter (partial some #{FIRE})) (r/map (fn [_] (.log js/console "Pressed fire..."))))
This works because in Clojure, sets can be used as predicates. We can quickly verify this at the REPL:
user> (def numbers #{12 13 14}) #'user/numbers user> (some #{12} numbers) 12 user> (some #{15} numbers) nil
By representing the events as a stream, we can easily operate on them using familiar sequence functions such as map
and filter
.
Writing code like this, however, is a little repetitive. The two previous examples are pretty much saying something along these lines: filter all events matching a given predicate pred
and then map the f
function over them. We can abstract this pattern in a function we'll call filter-map
:
(defn filter-map [pred f & args] (->> active-keys-stream (r/filter (partial some pred)) (r/map (fn [_] (apply f args)))))
With this helper function in place, it becomes easy to handle our game actions:
(filter-map #{FIRE} fire! monet-canvas ship) (filter-map #{UP} move-forward! ship) (filter-map #{DOWN} move-backward! ship) (filter-map #{RIGHT} rotate-right! ship) (filter-map #{LEFT} rotate-left! ship)
The only thing missing now is taking care of pausing the animations when the player presses the PAUSE
key. We follow the same logic as above, but with a slight change:
(defn pause! [_]
(if @(:updating? monet-canvas)
(canvas/stop-updating monet-canvas)
(canvas/start-updating monet-canvas)))
(->> active-keys-stream
(r/filter (partial some #{PAUSE}))
(r/throttle 100)
(r/map pause!))
Monet makes a flag available that tells us whether it is currently updating the animation state. We use that as a cheap mechanism to "pause" the game.
Note that active-keys-stream
pushes events as they happen so, if a user is holding a button down for any amount of time, we will get multiple events for that key. As such, we would probably get multiple occurrences of the PAUSE
key in a very short amount of time. This would cause the game to frantically stop/start. In order to prevent this from happening, we throttle the filtered stream and ignore all PAUSE
events that happen in a window shorter than 100 milliseconds.
To make sure we didn't miss anything, this is what our src/cljs/reagi_game/core.cljs
file should look like, in full:
(ns reagi-game.core (:require [monet.canvas :as canvas] [reagi.core :as r] [clojure.set :as set] [reagi-game.entities :as entities :refer [move-forward! move-backward! rotate-left! rotate-right! fire!]])) (def canvas-dom (.getElementById js/document "canvas")) (def monet-canvas (canvas/init canvas-dom "2d")) (def ship (entities/shape-data (/ (.-width (:canvas monet-canvas)) 2) (/ (.-height (:canvas monet-canvas)) 2) 0)) (def ship-entity (entities/ship-entity ship)) (canvas/add-entity monet-canvas :ship-entity ship-entity) (canvas/draw-loop monet-canvas) (def UP 38) (def RIGHT 39) (def DOWN 40) (def LEFT 37) (def FIRE 32) ;; space (def PAUSE 80) ;; lower-case P (defn keydown-stream [] (let [out (r/events)] (set! (.-onkeydown js/document) #(r/deliver out [::down (.-keyCode %)])) out)) (defn keyup-stream [] (let [out (r/events)] (set! (.-onkeyup js/document) #(r/deliver out [::up (.-keyCode %)])) out)) (def active-keys-stream (->> (r/merge (keydown-stream) (keyup-stream)) (r/reduce (fn [acc [event-type key-code]] (condp = event-type ::down (conj acc key-code) ::up (disj acc key-code) acc)) #{}) (r/sample 25))) (defn filter-map [pred f & args] (->> active-keys-stream (r/filter (partial some pred)) (r/map (fn [_] (apply f args))))) (filter-map #{FIRE} fire! monet-canvas ship) (filter-map #{UP} move-forward! ship) (filter-map #{DOWN} move-backward! ship) (filter-map #{RIGHT} rotate-right! ship) (filter-map #{LEFT} rotate-left! ship) (defn pause! [_] (if @(:updating? monet-canvas) (canvas/stop-updating monet-canvas) (canvas/start-updating monet-canvas))) (->> active-keys-stream (r/filter (partial some #{PAUSE})) (r/throttle 100) (r/map pause!))
This completes the code and we're now ready to have a look at the results.
If you still have the server running from earlier in this chapter, simply exit the REPL, start it again, and start the embedded web server:
lein repl Compiling ClojureScript. Compiling "dev-resources/public/js/reagi_game.js" from ("src/cljs" "test/cljs" "dev-resources/tools/repl")... user=> (run) 2014-06-14 19:21:40.381:INFO:oejs.Server:jetty-7.6.8.v20121106 2014-06-14 19:21:40.403:INFO:oejs.AbstractConnector:Started [email protected]:3000 #<Server org.eclipse.jetty.server.Server@51f6292b>
This will compile the latest version of our ClojureScript source to JavaScript.
Alternatively, you can leave the REPL running and simply ask cljsbuild
to auto-compile the source code from another terminal window:
lein cljsbuild auto Compiling "dev-resources/public/js/reagi_game.js" from ("src/cljs" "test/cljs" "dev-resources/tools/repl")... Successfully compiled "dev-resources/public/js/reagi_game.js" in 13.23869 seconds.
Now you can point your browser to http://localhost:3000/
and fly around your spaceship! Don't forget to shoot some bullets as well!
3.149.27.72