Skip to content
Ryan Neufeld edited this page Jul 9, 2013 · 14 revisions

To follow along with this section, start with tag v2.1.0.

Pedestal-app'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.

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

During this process you will be introduced to the following pedestal-app concepts:

  • Rendering without the DOM
  • External JavaScript and externs files

Slicing the new template

In the last section you created a new template named game.html. This template will need to be made available to the application. It will add the basic structure that is needed for the 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})})

All of this code has been explained in the Slicing Templates section of the tutorial.

Emitting a single list of counters

Before making changes to the rendering code, you need to make one change to how the emitters work. There is now a single list of players and it will be much easier to render them in the game if they were emitted as 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 [])]]

The emitters for [:my-counter] and [:other-counters :*] have been removed and a new emitter was added for [:counters :*].

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

Use the Data UI to confirm that the new list of counters is being emitted.

After making this change you may want to create a new recording which incorporates the new deltas. This recording can be used to help implement the new renderer.

New Rendering Code

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

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

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

When the initial delta is received

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

you will add the :tutorial-client-page template as in the previous renderer. In addition to this, the global BubbleGame constructor 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 render/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/BubbleGame. "game-board")]
      (render/set-data! renderer path g)
      (dotimes [_ 5] (.addBubble g)))))

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

(defn destroy-game [renderer [_ path :as delta] input-queue]
  (.destroy (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]])

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 addPlayer method on the game object to draw the player on the screen.

(defn add-player [renderer [_ path] _]
  (.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)]
    (.setScore g n v)
    (when (not= n "Me")
      (.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)]
    (if-let [g (game renderer)]
      (.setStat g (name s) v))))

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

Finally, you 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 it can be ignored. Use the send-transforms function to send the provided messages on the input-queue when this function is called.

(defn add-handler [renderer [_ path transform-name messages] input-queue]
  (.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, you can now play a multi-player game. In fact, all aspects (except for production) 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. Have someone else do this from another machine (or do it from another browser) and play a game.

Using an externs file to fix advanced compilation

If you tried to run the game in Production mode you will have noticed that it didn't work. The JavaScript method calls are not surviving advanced compilation.

Anytime you integrate with external JavaScript, there is a danger that advanced compilation will rename things that refer to code that it is not aware of, leaving your program broken.

To fix this problem, you must tell the compiler which symbols it should not alter. You can do this with an externs file.

Create the externs file below in tutorial-client/app/externs/game.js

var BubbleGame;
var bubbleGame = {};
bubbleGame.addHandler = function(callBack) {};
bubbleGame.addPlayer = function(playerName) {};
bubbleGame.setScore = function(playerName, n) {};
bubbleGame.setOrder = function(playerName, n) {};
bubbleGame.setStat = function(statName, n) {};
bubbleGame.addBubble = function() {};
bubbleGame.removeBubble = function() {};
bubbleGame.destroy = function() {};

With this file in place, add the following to the :production config in tutorial-client/config/config.clj.

:compiler-options {:externs ["app/externs/game.js"]}

After a server restart, production mode should work as expected.

Next steps

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

The tag for this section is v2.1.1.

Home | Game Improvements

Clone this wiki locally