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

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

The current game works but there are several things which are not ideal. The player scores are not sorted so it is not easy to tell who is winning. Bubble creation happens automatically, outside of the control of application logic which puts too much logic in the rendering code.

To make the game a little more interesting it should allow skilled players to get more points by being fast. The rendering code already handles this but be the application logic does not.

While making these changes, you will be introduced to the following pedestal-app concepts:

  • Messages with parameters
  • Timed events

Sorting player scores

The BubbleGame object provides a function which allows you to set the order in which a player should appear in the leaderboard. Since order is determined by score, it would be easy to put code in the drawing layer which automatically sorts the leaderboard based on score.

It will often come up that you would like to sort something in the UI based on data in the application. Often the data that you use for sorting is not displayed or available to the renderer or to drawing code.

As with all changes that you will make in pedestal-app, there are two distinct things that will be changing independently: application logic and rendering. While making changes to application logic, you can use tests or the Data UI to confirm that the changes work, and then go on to rendering when you know that application logic is sound.

First, in the namespace tutorial-client.behavior, add a derive function to sort the counters. It will receive a map of player names to scores and will return a map of player names to sort index.

(defn sort-players [_ players]
  (into {} (map-indexed (fn [i [k v]] [k i])
                        (reverse
                         (sort-by second (map (fn [[k v]] [k v]) players))))))

To use this function, add its configuration to the derive section. The order map will be output to [:player-order].

[#{[:counters]} [:player-order] sort-players :single-val]

Add a separate emitter for [:player-order :*] after the emitter for [:other-counters :*].

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

This order of emitters will ensure that deltas which describe the order of elements always appear after deltas which create elements.

After making these changes the sort order data should now appear in the Data UI. As you increment the counter, the order data should change.

Why?

Why create a separate sort order data structure rather than than just have a sorted list of scores? You could just sort the list of players and hand this sorted list to the renderer. This would just push off the job of figuring out what has changed to the renderer. It could punt and redraw the whole list in the correct order or it could figure out what is different and make precise changes.

With this approach, only the changes in sort order are reported. This works well when you are rendering by drawing things on the screen at calculated positions. The rendering code does not have to figure anything out, it is simply told the index of something which causes it to calculate the new position and move the item there.

Rendering the order

With the above modifications, changes in sort order will now be reported. In tutorial-client.rendering, respond to these changes by calling setOrder.

The set-player-order function is another simple rendering function which should be called when the renderer receives deltas for [:main :player-order :*].

(defn set-player-order [renderer [_ path _ v] _]
  (let [n (last path)]
    (.setOrder (game renderer) n v)))

Add the following line to the render configuration.

[:value [:main :player-order :*] set-player-order]

With these changes in place, you should now see the scores change position as new players achieve higher scores.

Creating bubbles

The current game drawing code contains a loop which creates bubbles. The makeCircles function is called every 2 seconds creating one new bubble for each player in the game.

This is way too much application logic in the drawing code. What if you wanted to have different rules for how bubbles are created based on the current state of the game? The control for creating bubbles should be moved into application logic.

As a start, remove the code that does this from game.js and add the following code to game-driver.js.

var makeCircles = function() {
  if(gameActive) {
    var p = players.length;
    for(var i=0;i<p;i++) {
      game.addBubble();
    }
    setTimeout(makeCircles, 2000);
  }
}

Add a call to makeCircles at the end of the startGame function.

You can test this code by going to the Design page and clicking on Game.

Trying to play the game from the UI page will no longer work.

Creating a clock

The goal is to allow application logic to control the creation of bubbles. The game should create one bubble for each player every two seconds. This will require that there be some way to do something every two seconds. This can be achieved by creating a function external to the application dataflow which will send a new message every two seconds, incrementing a clock value.

The clock code will go in a new namespace named tutorial-client.clock. This will be a ClojureScript file.

(ns tutorial-client.clock
  (:require [io.pedestal.app.protocols :as p]
            [io.pedestal.app.messages :as msg]
            [io.pedestal.app.util.platform :as platform]))

(defn increment-game-clock [queue]
  (p/put-message queue {msg/type :inc msg/topic [:clock]})
  (platform/create-timeout 2000 (fn [] (increment-game-clock queue))))

The increment-game-clock function will send an :inc message to [:clock] every two seconds.

In the create-app function within the tutorial-client.start namespace, require the clock namespace

[tutorial-client.clock :as clock]

and add the initial call to increment-game-clock.

(defn create-app [render-config]
  (let [app (app/build (post/add-post-processors behavior/example-app))
        render-fn (push-render/renderer "content" render-config render/log-fn)
        app-model (render/consume-app-model app render-fn)]
    (clock/increment-game-clock (:input app))
    (app/begin app)
    {:app app :app-model app-model}))

Because create-app is used by both the production and simulated main functions, this change will work for both versions of the app.

Update tutorial-client.behavior to receive the new :inc message

[:inc [:clock] inc-transform]

and emit the :clock value.

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

A clock which is updated every two seconds should now be visible in the Data UI.

The :transform section of the dataflow definition now has two transform functions configured with the same shape.

[:inc [:my-counter] inc-transform]
[:inc [:clock] inc-transform]

These can be collapsed to one line using a wildcard.

[:inc [:*] inc-transform]

Making bubbles

For each clock update, one bubble should be created for each player. The add-bubbles function will do this.

(defn add-bubbles [_ {:keys [clock players]}]
  {:clock clock :count (count players)})

This function takes the current clock value and the list of players and returns a map with :clock and :count keys. The :count value is the total number of players or the total number of bubbles to create.

Why create a map containing both clock and count values? If there are three players then the value of count will always be three. Since, by default, dataflow propagation only occurs when something changes, the renderer would get the first update but none of the subsequent ones. Adding the click as part of the value will make it unique each time.

Add the derive configuration for this function, storing the value under [:add-bubbles].

[{[:clock] :clock [:counters] :players} [:add-bubbles] add-bubbles :map]

To test how well you understand pedestal-app, what would have happened if you had used [:counters :*] instead of [:counters] in the above config? The answer is that the number of bubbles created when there are three players would now always have been 3.

Finally, emit changes to [:add-bubbles]

[#{[:add-bubbles]} (app/default-emitter [:main])]

and stop emitting changes to [:clock] by removing the following emit configuration.

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

Once again you can confirm that this change works in the Data UI. To render this change, add the following function to the tutorial-client.rendering namespace.

(defn add-bubbles [renderer [_ path _ v] _]
  (dotimes [x (:count v)]
    (.addBubble (game renderer))))

and then the following line to the render configuration.

[:value [:main :add-bubbles] add-bubbles]

Variable points

The final change to make to the game is to allow for a variable number of points to be set. The drawing code already sends the points to the handler function but it currently ignores them.

Note: This is another place where there is game logic in the rendering code. Consider having the rendering code simply report what happened and then have the application logic decide how many points that is worth.

To implement this, there must be a way to create a transform which will collect some of its data when the messages are sent. The :transform-enable which was created to increment the counter contains a single :inc message. The same message will always be sent.

(defn init-main [_]
  [[:transform-enable [:main :my-counter] :inc [{msg/topic [:my-counter]}]]])

Messages with parameters

Pedestal-app allows you to create messages which specify parameters which are to be filled in when the message is sent. Suppose that you wanted to send a message and you will not know the value of the :foo key until the message is sent. Instead of putting :foo in the message map, put (msg/param :foo). This marks this key as a parameter to be provided later. For now the value for this key can just be {}. In the future, this value map will be used to specify constraints on the type or properties of the data which can be entered there.

Update init-main to send a new :add-points message which collects a :points parameter.

(defn init-main [_]
  [[:transform-enable [:main :my-counter]
    :add-points [{msg/topic [:my-counter] (msg/param :points) {}}]]])

Add a new transform function to process the add-points message. Instead of just incrementing the counter, this function will add the new points to the existing points.

;; transform function
(defn add-points [old-value message]
  (if-let [points (int (:points message))]
    (+ old-value points)
    old-value))

Configure this transform function.

[:add-points [:my-counter] add-points]

With these changes in place, test the new behavior with the Data UI. When you click on the :add-points button a dialog will appear asking you to input a value for :points. Enter a number and click continue.

The Data UI is smart enough to notice that a message has missing values and will ask for the values with a dialog. It is not yet smart enough to know what kind of data is required and how to validate inputs, therefore you must enter a number or bad things will happen.

Providing parameter values

In tutorial-client.rendering, the add-handler function provided a function to call to report points. This function already received the points from the drawing layer, you just need to set the points value in the message.

To do this, pass an input map to events/send-transforms.

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

You may now use one of the UI, Development or Production aspects to confirm that if you pop a bubble fast enough, you will get more than one point.

Next steps

When playing a game with multiple players, it is hard to tell who is winning. In the next section, you will add a login screen which allows you to use actual player names instead of "Me" or a UUID. In the process you will learn how to make multi-screen applications in pedestal-app.

The tag for this section is v2.1.2.

Home | Multi-screen Applications