-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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})})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]])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.
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.