Chapter 6. Building a Simple ClojureScript Game with Reagi

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:

  • Learn about Reagi, a CES framework built on top of core.async
  • Use Reagi to build the rudiments of a ClojureScript game that will teach us how to handle user input in a clean and maintainable way
  • Briefly compare Reagi to other CES frameworks and get a feel for when to use each one

Setting up the project

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:

Setting up the project

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:

Setting up the project

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.

Game entities

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.

Tip

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.

Putting it all together

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:

  • Ship movement: rotation, forward, and backward
  • Firing the ship's gun
  • Pausing the game

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.

Modeling user input as event streams

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:

Modeling user input as event streams

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:

Modeling user input as event streams

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:

Modeling user input as event streams

Working with the active keys stream

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!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.27.72