Skip to content
Brenton Ashworth edited this page Jun 30, 2013 · 14 revisions

Rendering the Game

Pedestal's approach to rendering works just as well when using JavaScript libraries like d3 or Raphael for rendering as it does when using the DOM.

We will not learn anything new in this section. We will only see a different way of using the knowledge we already have.

In this section we will using the JavaScript drawing code which we added in the last section to render the game as the new user interface for our existing application. When we are finished, we will have a simple game which can be played by multiple players from anywhere in the world.

Slicing the new template

In the last section we created a new template named game.html. We need to make that template available to our application. This template will add the basic structure that is needed for our JavaScript drawing code.

In the namespace tutorial-client.html-templates, update the tutorial-client-templates to grab the new tutorial template from game.html.

(defmacro tutorial-client-templates
  []
  {:tutorial-client-page (dtfn (tnodes "game.html" "tutorial") #{:id})})

New rendering code

Before we make changes to the rendering code, we need to make one change to how our emitters work. We now have a single list of players and it will be easier to render them if we emit a single list.

In tutorial-client.behavior, update the emitters to look like this:

:emit [{:init init-main}
       [#{[:total-count]
          [:max-count]
          [:average-count]} (app/default-emitter [:main])]
       [#{[:counters :*]} (app/default-emitter [:main])]
       [#{[:pedestal :debug :dataflow-time]
          [:pedestal :debug :dataflow-time-max]
          [:pedestal :debug :dataflow-time-avg]} (app/default-emitter [])]]

We have removed the emitters for [:my-counter] and [:other-counters :*] and added a new emitter for [:counters :*].

[#{[:counters :*]} (app/default-emitter [:main])]

Check the Data UI to make sure this works before moving on.

TODO: Write something about using the recording to test these rendering changes.

Even though the new method of rendering is completely different, the new rendering code is just as simple as the previous version.

Before we make the change, add one new required namespace in tutorial-client.rendering.

[io.pedestal.app.render.events :as events]

When the initial tutorial delta is received

[:node-create [:main] render-template]

we will add the :tutorial-client-page template as we did before. In addition to this, the global createGame function is called to create the game. the set-data! function is used to associate arbitrary data with a path in the renderer. When the [:main] node is removed from the tree, the reference to this data will be deleted so that it can be garbage collected. If any additional cleanup is required, the on-destroy! function can be used to provide a function to call when a path is removed from the tree.

A function is also created to get the game from the renderer.

(defn add-template [renderer [_ path :as delta] input-queue]
  (let [parent (render/get-parent-id renderer path)
        id (render/new-id! renderer path)
        html (templates/add-template renderer path (:tutorial-client-page templates))]
    (dom/append! (dom/by-id parent) (html {:id id}))
    (let [g (js/createGame "game-board")]
      (render/set-data! renderer path g)
      (dotimes [_ 5] (js/addBubble g)))))

(defn game [renderer]
  (render/get-data renderer [:main]))

(defn destroy-game [renderer [_ path :as delta] input-queue]
  (js/destroyGame (game renderer))
  (render/drop-data! renderer path)
  (h/default-destroy renderer delta input-queue))

(defn render-config []
  [[:node-create [:main] add-template]
   [:node-destroy [:main] destroy-game]])

-- TODO: Use Externs -- Note: we are using global functions as our API instead of just calling the methods directly on the game object to allow our program to work when it is advanced compiled. If we called the methods directly on game, the function calls would get destroyed during advanced compilation. We could use an externs file to prevent this but that is just as much work as what we are doing here. We will add externs in a future section.

The add-player handler is called when a new player is added to the game. It will get the player name from the path and then call the global function addPlayer to draw the player on the screen.

(defn add-player [renderer [_ path] _]
  (js/addPlayer (game renderer) (last path)))

(defn render-config []
  [...
   [:node-create [:main :counters :*] add-player]])

set-score will be called when any score is updated. It will change the players score and if the player is not "Me", it will remove a bubble from the screen. This will be called each time the score changes and scores can only change by one, so one bubble will be removed per score.

(defn set-score [renderer [_ path _ v] _]
  (let [n (last path)
        g (game renderer)]
    (js/setScore g n v)
    (when (not= n "Me")
      (js/removeBubble g))))

(defn render-config []
  [...
   [:value [:main :counters :*] set-score]])

The set-stat function will set one of the game statistics.

(defn set-stat [renderer [_ path _ v] _]
  (let [s (last path)]
    (js/setStat (game renderer) (name s) v)))

(defn render-config []
  [...
   [:value [:pedestal :debug :*] set-stat]
   [:value [:main :*] set-stat]])

Finally, we must arrange for scores to be reported when a bubble is popped. The addHandler function will register a handler. The registered function takes the number of points received, for now this will always be 1 so we can ignore it. We use the function send-transforms to send the provided messages on the input-queue when this function is called.

(defn add-handler [renderer [_ path transform-name messages] input-queue]
  (js/addHandler (game renderer)
                 (fn [p]
                   (events/send-transforms input-queue transform-name messages))))

(defn render-config []
  [...
   [:transform-enable [:main :my-counter] add-handler]])

It is again important to note that this function does not have to know the details of the messages that it sends.

The final render configuration should look like the one shown below.

(defn render-config []
  [[:node-create [:main] add-template]
   [:node-destroy [:main] destroy-game]
   [:node-create [:main :counters :*] add-player]
   [:value [:main :counters :*] set-score]
   [:value [:pedestal :debug :*] set-stat]
   [:value [:main :*] set-stat]
   [:transform-enable [:main :my-counter] add-handler]])

Playing a multi-player game

With these changes to the renderer, we can now play a multi-player game. In fact, all aspects should now work with the new renderer.

To play a multi-player game, start the service and the client projects and open

http://localhost:3000

and then use the Tools menu to navigate to Development or Production. Have someone else do this from another machine and play a game.

Next steps

The game works but there are many improvements to be made. The scores are not sorted, we can only get one point at a time, etc. In the next section we will make some improvements to the game to make it a bit more interesting.

The tag for this step is v0.1.2b.

Home | Game Improvements

Clone this wiki locally