-
Notifications
You must be signed in to change notification settings - Fork 0
Start a Game
To follow along with this section, start with tag v2.1.5.
The game is starting to take shape. The next improvement to make is to prevent the game from starting after login. It would be nice if you could wait for more players to join before starting the game.
In this section you will create a wait screen which will live between the login and game screens. After login a player will go to the wait page. As players join, each player will be displayed on the wait page. This page will also have a Start button. When this button is clicked the game will start with the current players. If a player joins the game after the game has started they will immediately enter the game.
While making these changes you will be introduced to the following pedestal-app concepts:
- Changing focus based on state changes
- Continue Functions
- Generating new messages for the input queue
To add the wait screen, you will follow the same procedure that was followed when adding the login screen. You first add the new data to the data model, including transforms. You then move this to a new screen and then, finally, render it.
While making the following changes, have the Data UI running so that you can see the effect of each change.
The first thing we do is create the structure for the wait
screen. This screen will have a Start button. When this button is
clicked it should start the game locally and for each connected
player. This will require a transform which sends two messages: one to
change the local state and one to change the state of remote
players. The new init-wait function is shown below.
(defn init-wait [_]
(let [start-game {msg/type :swap msg/topic [:active-game] :value true}]
[{:wait
{:start
{:transforms
{:start-game [{msg/topic msg/effect :payload start-game}
start-game]}}}}]))This function creates the initial structure for the wait page and adds
the :start-game transform to the [:wait :start] node. this
transform contains two messages, one of which has not been seen
before.
{msg/topic msg/effect :payload start-game}This message will place the :payload message directly on the effect
queue.
The wait page will not only have a Start button but it will also
show a list of the players which are available to play a game. This
list already exists at [:counters :*] and can used to populate the
waiting players list.
First, configure the init-wait function and add the emitter which
will emit [:counters :*] under the :wait root node.
{:init init-wait}
{:in #{[:counters :*]} :fn (app/default-emitter [:wait]) :mode :always}This emitter is configured as a map and contains a :mode key which
has not yet been used in this tutorial. The :mode is set to
:always. The list of emitters is processed sequentially. Normally,
if an emitter emits something at a path, any subsequent emitter will
not see this change. This allows early emitters to override the
behavior of emitters that appear later.
To allow two emitters to emit changes for the same path, you must set
their mode to :always.
{:in #{[:counters :*]} :fn (app/default-emitter [:main]) :mode :always}To be clear, notice that the :mode has been set to :always for two
emitters. The one which emits the counter list under :wait and the one
which emits the counter list under :main.
Finally, update the focus to show the :wait section of the tree on
the game screen.
:focus {:login [[:login]]
:game [[:main] [:pedestal] [:wait]]
:default :login}With these changes in place, the Data UI should show the wait data
and print the [:active-game] messages to the console when the
Start button is clicked.
As things are now, the game still starts when the app starts instead
of when the Start button is clicked. The service simulator, in the
namespace tutorial-client.simulated.service, must be updated to
simulate the new behavior.
(defn start-game-simulation [input-queue]
(receive-messages input-queue))
(defrecord MockServices [app]
p/Activity
(start [this]
;; this will now simulate adding players
)
(stop [this]))
(defn services-fn [message input-queue]
(if (and (= (msg/topic message) [:active-game]) (:value message))
(start-game-simulation input-queue)
(.log js/console (str "Sending message to server: " message))))Instead of starting the game when calling start, the start function
will now do nothing. A game will be started when a message is sent
which sets [:active-game] to true.
Test this in the Data UI to ensure that a game starts properly.
Instead of starting the game when the start function is called, slowly
add two players. This is done by sending a [:other-counters name]
message for a player with a score of 0.
(defn add-player [name input-queue]
(p/put-message input-queue {msg/type :swap
msg/topic [:other-counters name]
:value 0}))
(defrecord MockServices [app]
p/Activity
(start [this]
(platform/create-timeout 10000 #(add-player "abc" (:input app)))
(platform/create-timeout 15000 #(add-player "xyz" (:input app))))
(stop [this]))Once again, check this behavior in the Data UI. Players should
slowly join the page and that game should not start until the
:start-game button is pressed.
As you did in the previous section, move the wait page to its own
screen. Make the following changes in the tutorial-client.behavior
namespace.
Change the :set-focus message in the init-login emitter to change
focus to the :wait page.
(defn init-login [_]
[{:login
{:name
{:transforms
{:login [{msg/type :swap msg/topic [:login :name] (msg/param :value) {}}
{msg/type :set-focus msg/topic msg/app-model :name :wait}]}}}}])Update the :focus section of the dataflow definition, giving :wait
its own name..
:focus {:login [[:login]]
:wait [[:wait]]
:game [[:main] [:pedestal]]
:default :login}Under what circumstances should a player transition from the wait
screen to the game screen? The easy thing to do would be to add a
:set-focus to the :start-game transform. When the Start button
is clicked, the game would start.
This only works for starting the game locally. A game should also start
when any other player clicks the Start button. Currently the
:start-game transform will do two things: it changes [:active-game]
to true in the data model and then sends a message to everyone else to
set their [:active-game] value to true.
There are two conditions in the data model which, if met, should cause
focus to change to the :game screen:
- the game is active and login name was just set
- the login name is set and the game has just become active
The start-game function below will send a :set-focus message when
either of these conditions are met. First require the
io.pedestal.app.dataflow namespace
[io.pedestal.app.dataflow :as dataflow]and then add the start-game function.
(defn start-game [inputs]
(let [active (dataflow/old-and-new inputs [:active-game])
login (dataflow/old-and-new inputs [:login :name])]
(when (or (and (:new login) (not (:old active)) (:new active))
(and (:new active) (not (:old login)) (:new login)))
[^:input {msg/topic msg/app-model msg/type :set-focus :name :game}])))This is a Continue function. Continue functions are used to generate new messages to be processed by the dataflow engine. These functions return a sequence of messages. The default behavior of continue is to take the messages that it produces and run each of them through the dataflow engine until no messages are returned. This is done before effects and emitter deltas are generated.
In this example, :input metadata is used to tag a message as
something which should escape the dataflow and be put on the input
queue. :set-focus messages are handled by the app engine before they
get to the dataflow engine, so they must be put on the input queue.
To configure a continue function, add a continue keyword to the dataflow definition.
:continue #{[#{[:login :name] [:active-game]} start-game]}This should now work correctly in the Data UI. You should be able to transition from the login screen to the wait screen and then to the game screen.
You have now been introduced to the three types of dataflow functions which produce output. Each of these functions have the same signature. They are passed inputs and they return a vector of messages. The difference between the three functions is only in how their return values are handled.
In the next two sections, the template for the wait page is created and rendered.
The tag for this section is v2.1.6.