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