-
Notifications
You must be signed in to change notification settings - Fork 0
Game Improvements
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 we 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, we will learn a bit more about designing a Pedestal application and see how to create and render transforms which take parameters.
- Messages with parameters
- Custom propagator functions
- Timed events
The API for the drawing code provides a function which allows us to set the order in which a player should appear in the leaderboard. Since we currently order by score, it would be each to put code in the drawing layer which automatically sorts the leaderboard based on score.
It will often come up that we would like to sort something in the UI based on data in the application. Often the data that we use for sorting is not displayed or available to the renderer or to drawing code. Since this is a common problem, we will here show how to do this in the dataflow.
As with all changes that we make in Pedestal, there are two distinct things that we are changing independently: application logic and rendering. While making changes to application logic, we can use tests or the Data UI to confirm that our changes work, and then go on to rendering when we know that application logic is sound.
in behavior, add the derive function which will be used to the sort the scores
First, add a derive function to sort the counters. It will receive the
value at [:my-counter] as me and the other counters as
others. It returns a map of players names to sort index.
(defn sort-counters [_ {:keys [me other]}]
(into {} (map-indexed (fn [i [k v]]
[k i])
(reverse
(sort-by second (cons ["Me" me] (map (fn [[k v]] [k v]) other)))))))To use this function, add its configuration to the derive section. We
output the order map to [:counter-order].
;; add this derive config
[{[:my-counter] :me [:other-counters] :other} [:counter-order] sort-counters :map]Add a separate emitter for [:counter-order :*] after the emitter for
[:other-counters :*].
;; add this emit config after [:other-counters :*]
[#{[:counter-order :*]} (app/default-emitter :tutorial)]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 we increment our counter the data will change.
Why would we create a separate sort order data structure rather than than just have a sorted list of scores? We could just sort the list of players and hand this sorted list to the renderer. But then the renderer would either have to redraw the whole list in the correct order or have to figure out what parts of the list need to change.
With this approach, only the changes in sort order are reported. This works well when we 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.
With the above modifications, changes in sort order will now be
reported. In tutorial-client.rendering, we need to respond to
these changes by calling setOrder.
The set-player-order function is another simple rendering function
(defn set-player-order [renderer [_ path _ v] _]
(let [n (last path)]
(js/setOrder (game renderer) n v)))which should called when we receive deltas for [:tutorial :counter-order :*]. Add the following line to the render
configuration.
[:value [:tutorial :counter-order :*] set-player-order]With these changes in place, we should now see the scores change position as new players achieve higher scores.
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 we 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, let's remove the code that does this from game.js and add
the following code to game-driver.js. We can test this code by going
to the Design page and clicking on Game.
var makeCircles = function() {
var p = players.length;
for(var i=0;i<p;i++) {
game.addBubble();
}
}
setInterval(makeCircles, 2000);Trying to player the game in the UI page will no longer work.
The goal in the section is to allow the application logic to control the creation of bubbles. The game should create one bubble for each player every two seconds.
This will require that we have some way to do something every two seconds. To do this we will create a timeout which increments a clock value every two seconds.
A good place to do this is the tutorial-client.start
namespace. First, require the io.pedestal.app.util.platform
namespace.
[io.pedestal.app.util.platform :as platform]Create a function which will send an :inc message to the clock every
two seconds.
(defn increment-game-clock [app]
(p/put-message (:input app) {msg/type :inc msg/topic [:clock]})
(platform/create-timeout 2000 (fn [] (increment-game-clock app))))In the create-app function, add the initial call to
increment-game-clock.
(defn create-app [render-config]
(let [app (app/build (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)]
(increment-game-clock 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 :tutorial)]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]For each clock update, we want to create one bubble for each
player. The add-bubbles function will do this.
(defn add-bubbles [_ {:keys [clock other-players]}]
{:clock clock :count (inc (count other-players))})This function takes the current clock value and the other 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, changes are only propagated when something changes, we 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. Store the value above
under [:add-bubbles].
[{[:clock] :clock [:other-counters :*] :other-players} [:add-bubbles] add-bubbles :map]Finally, emit changes to [:add-bubbles]
[#{[:add-bubbles]} (app/default-emitter :tutorial)]and stop emitting changes to [:clock] by removing the following emit
configuration.
[#{[:clock]} (app/default-emitter :tutorial)]Once again we 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)]
(js/addBubble (game renderer))))and then the following line to the render configuration.
[:value [:tutorial :add-bubbles] add-bubbles]TODO: test that this works and maybe use this in the implementation above.
Adding the clock value to force the value under [:add-bubbles] to
always propagate is a bit of a trick. We will stick with this
implementation but there is another way to do this.
In Pedestal propagation occurs when something changes. But we can
customize this by adding our own propagator functions. We could have
written the add-bubbles function like this:
(defn add-bubbles [_ {:keys [other-players]}]
(inc (count other-players)))and then, when configuring the emitter, we could have added a custom propagator as shown below.
[#{^{:propagator (... clock tick propagator ...)} [:add-bubbles]} (app/default-emitter :tutorial)]TODO: write the above function when this is tested.
This will use the provided function to determine if the emitter should be called. This function will allow propagation any time the clock value has changed.
The final change that we would like 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 we have 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 is sent. The
:transform-enable which we created to increment the counter contains
a single :inc message. The same message will always be sent.
(defn init-emitter [_]
[[:transform-enable [:tutorial :my-counter] :inc [{msg/topic [:my-counter]}]]])Pedestal allows us to create messages which specify parameters which
are to be filled in when the message is sent. Suppose that we wanted
to send a message and we 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-emitter to send a new :add-points messages which
collects a :points parameter.
(defn init-emitter [_]
[[:transform-enable [:tutorial :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 transform
[: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 dialog. It is not yet smart enough to know what kind of data is required and how to validate inputs.
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, we 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]
(js/addHandler (game renderer)
(fn [p]
(events/send-transforms input-queue transform-name messages {:points p}))))It's time to clean some things up. In the next section will do some refactoring to make the code clearer and to ease the way toward making a multi-page application. In the section after that we will learn how to make multi-page applications while adding a login screen, allowing players to enter their real name.
The tag for this step is step13.