Qt SCXML Pinball Example¶
Encapsulates the internal logic of an application in an SCXML file.
Pinball demonstrates a clear separation between the user interface, which may be easily replaced, and the internal logic encapsulated in an SCXML file, which could also be used with another user interface.
Running the Example¶
To run the example from Qt Creator , open the Welcome mode and select the example from Examples. For more information, visit Building and Running an Example.
Pinball Features¶
The Pinball example mimics a pinball game. The targets on the pinball table are substituted by GUI controls, mainly by push buttons. Display elements, including current score, highscore, and targets’ lights, are substituted by labels. Usually, the state of the targets’ lights changes very often during a game: the lights get turned on or off permanently or they blink at varying speed indicating a game (or a certain target) entered a temporary state. The state of each target light is presented as an enabled or a disabled label. There is no real ball, but clicking a target’s button represents hitting a real pinball target with a ball.
Our pinball contains the following features:
When not in
hurryState
, the letters already hit should blink at intermediate speed (500ms). Letters not hit yet should stay off.When in
hurryState
, the letters already hit should stay on. Letters not hit yet should blink fast (200ms). In addition, the HURRY light should blink at the same speed.When the jackpot gets collected, the JACKPOT light should stay on.
SCXML Part: Internal Logic Description¶
The pinball.scxml file describes the internal logic implemented for the pinball game. In this example, we have chosen the ECMAScript data model:
The ECMAScript data model enables declaring variables with initial values that can be modified later. We declare the "highscore"
and "score"
variables with the initial values of 0:
We define a root parallel state "global"
, with two child states, guiControl
and internalState
, which are also parallel. Because the top global
state is parallel, all of its direct children are active when it is active. In this example, the role of global
is to collect the child states and make them both active at a time.
Maintaining Light State¶
The guiControl
element is responsible for maintaining the current state of each light control that is visible on the pinball table. Each light has a corresponding state.
For example, the light of the letter C corresponds to the cLight
state. Each light state has two child states indicating whether the light is on or off:
As mentioned before, the guiControl
state is always active, and since it is of parallel type, all its direct children are always active too. Therefore, the cLight
state is always active. However, only one of its children, cLightOn
or cLightOff
, is active at a time. The same applies to the other children of the guiControl
state. In addition, we define transitions between on and off substates. For example, whenever the active state is cLightOn
and a turnOffC
event is received, we change the active substate of cLight
to cLightOff
. Whenever the active state is cLightOff
and we receive a turnOnC
event, we change the active substate of cLight
to cLightOn
.
In our application, we use instances of QLabel
class in C++ to represent real lights on the table. When the light transitions into the on or off state, we enable or disable the particular label accordingly. The connection between the state machine and the GUI part of the application will be shown in the C++ code later on. For now, it is enough to realize that changes to active states inside the state machine will serve as the external interface of the state machine that the other parts of the application (such as the GUI part) can listen to.
All of the mentioned events that switch the state of a light will be generated by this state machine inside the internalState
in reaction to running timers or external triggers.
Maintaining Game State¶
The internalState
state consists of two main parts: logicalState
and workflow
.
The logicalState
state holds the definitions for the modes that the game is able to go into and for the logical states of collected targets. The workflow
state implements a generator for light blinking and calculates most of the new states the machine should go into depending on incoming events and on currently active states. As mentioned already, internalState
is always active, and since it is of a parallel type, logicalState
and workflow
are always active too.
Maintaining Game Modes¶
The modeState
state consists of two substates, offState
and onState
.
The offState
state describes what should happen before the pinball game is started and when it is over, while onState
represents the logic appropriate for the active game.
When the pinball application starts or a game ends, the machine goes into offState
. Entering that state invokes some actions, which are enclosed inside an <onentry>
element. First, we update the highScore
variable in case the current highScore
value is less than current score
value. This is being checked inside the "cond"
attribute of the <if>
element (note that we need to escape the “<” character with “<”). Even in the off
state, we want to show the last reached score, so we do not clear it here; we will do that when we enter the on
state. Next, we raise two events: resetLetters
to logically reset all letters that might have been hit during the last game and update
to immediately activate the blinking and updating of all lights. When the machine is in offState
, it is ready to transition into the onState
if only the startTriggered
event occurs, which is described by the <transition> element. This event is expected to be generated externally after clicking the START button on the pinball table.
Game On¶
When the state machine enters onState
, it first clears the current score variable. The onState
state is of the parallel type and has two direct child states: hurryState
and jackpotState
. They are active as long as their parent, onState
, is active. Both hurryState
and jackpotState
contain two substates that reflect their off and on states. Only one substate of hurryState
and one substate of jackpotState
can be active at a time. Initially, the off substates are active.
Whenever we enter hurryStateOff
or hurryStateOn
, we generate the same two events we generate when entering the onState
state: resetLetters
and update
. In addition, when we enter the hurryStateOn
state, we send a delayed event, goToHurryOff
, with a delay of five seconds, marked with hurryId
. This means that after five seconds we just switch the state back to hurryStateOff
without granting the bonus points. In this way, we implement the five-second hurry feature of the pinball table. We also define transitions from hurryStateOff
to hurryStateOn
when the goToHurryOn
event occurs and from hurryStateOn
to hurryStateOff
when the goToHurryOff
event occurs. When we exit the hurryStateOn
state, we cancel the possibly pending delayed event that was marked with hurryId
. This is important in case the five seconds have not elapsed yet, but players have collected all the five letters in the hurry state. We then collect the jackpot and want the pending timer to finish.
The substates of jackpotState
generate the request to update the state of lights. The jackpotStateOff
state defines the transition to jackpotStateOn
when the goForJackpot
event occurs. The opposite transition is not needed, because when the jackpot gets collected, the corresponding light remains lit until the end of game. When a new game starts, the jackpotState
is entered again which causes its initial active substate to be jackpotStateOff
.
In addition, the onState
state defines one transition in reaction to the ballOutTriggered
event which instructs the machine to go into the offState
. The ballOutTriggered
event is expected to be an event posted into the state machine from outside of the state machine. This event should be generated when the ball gets out of playing area of the table. In our example we mimic it by the clicking BALL OUT button. Posting the event from outside of state machine will be shown in the C++ code later on.
Generating Blinking Lights¶
The workflow
state is responsible for generating the blinking lights. The generator is defined in its lightImpulseGenerator
substate. In addition, it is responsible for reacting to events that have been posted so far from the other parts of the state machine.
...
...
...
...
...
...
The lightImpulseGenerator
contains two child states: lightImpulseOn
and lightImpulseOff
, with only one active at a time.
Whenever the delayed lightImpulse
event is being delivered, it immediately causes the transition from lightImpluseOn
into lightImpulseOff
or vice versa, depending on the state the machine was in. In effect, the lightImpulseGenerator
toggles between its on and off state. These transitions are defined inside lightImpulseGenerator
, so it means that during this toggling the machine also exits lightImpulseGenerator
and reenters it immediately afterwards. Entering lightImpulseGenerator
causes the generation of the update
event. The update
event triggers a targetless transition and posts two other events: scheduleNewImpulse
and updateLights
. The first one, scheduleNewImpulse
, returns back to the lightImpulseGenerator
, which posts a delayed lightImpulse
event. After the delay, the lightImpulse
event gets delivered back to lightImpulseGenerator
, which causes it to toggle its substate again. In this way, the machine enters into a cycle. The current delay of the lightImpulse
event depends on the state in which the machine was in the time of posting the delayed event. If a scheduleNewImpulse
event occurs on demand, before the next delayed lightImpulse
event gets delivered, we cancel any possible pending events.
Whenever we receive the event the name of which matches the done.state.letter.*
, we update the current score. When the machine enters the final substate of the letter.C
, it emits the done.state.letter.C
event. The same happens for all other letters we have previously defined. We capture the events for all letters, that is why we have used an asterisk after a dot in the event name. The transition above is targetless, since we just listen for matching events and update the internal data accordingly without changing any active state. The new score is being increased by 1.000 or 10.000 points, depending on whether we currently are in hurryStateOff
or hurryStateOn
. After the score is updated, we generate the updateLights
event in order to immediately update the letters’ lights accordingly. We do not generate the update
event here, since we do not want to toggle the light impulse now, but just update the lights according to the current impulse state.
We also intercept the done.state.lettersState
event, which is being generated when all the letters have been hit. Depending on which state we are currently in, we grant the players either a small bonus of 100.000 or a big one of 1.000.000 (jackpot). In addition, we toggle the hurryState
substate by sending the goToHurryOn
or goToHurryOff
event. When all letters have been collected while in hurryStateOn
, we also raise the goForJackpot
event which instructs the machine to activate the jackpotStateOn
.
When we receive the updateLights
event, we first want to send a updateScore
event outside of the state machine. We pass the current values of the highScore
and score
variables to the event. This event is received by the C++ part.
Next, depending on whether we are in jackpotStateOn
or jackpotStateOff
, we send the turnOnJackpot
or the turnOffJackpot
event, which instructs the guiControl
state to transition to jackpotLightOn
or jackpotLightOff
, respectively.
When the machine is in idle state, (that is, in the off state) or when the game is on, but no interaction occurs, the updateLights
event is delivered periodically during the game, each time with the lightImpulseOn
or lightImpulseOff
state toggled. Depending on the current state of the light impulse and on the active state (offState
, hurryStateOff
or hurryStateOn
), we turn on or off all the lights according to the description of the pinball table.
GUI Part: User Interface Description¶
The GUI part of the application consists of a mainwindow.ui file which describes the static user interface of the game.
C++ Part: Glue GUI with SCXML¶
The C++ part of the application consists of a MainWindow
class which glues the GUI part with the SCXML part. The class is declared in mainwindow.h.
The MainWindow
class holds the pointer to the QScxmlStateMachine *m_machine
which is the state machine class automatically generated by Qt out of SCMXL file and the pointer to the Ui::MainWindow *m_ui
which describes the GUI part. It also declares two helper methods.
The constructor of the MainWindow
class instantiates the GUI part of the application and stores the pointer to the passed QScxmlStateMachine
. It also initializes the GUI part and glues the GUI part to the state machine by connecting their communication interfaces together.
The initAndConnect()
method connects the state with the corresponding GUI widget by binding its activity with the enabling of the widget, so that whenever the state is active, its corresponding widget is enabled and whenever the state is inactive, the widget is disabled. We do that for all lights, targets, and description labels.
We also intercept the updateScore
event sent by the state machine, in order to update the score displays with the values passed with the event.
The info about hitting any GUI target needs to be passed to the state machine and we do that by connecting all target buttons’ clicked
signals to the lambda expressions which submit the corresponding event into the state machine.
In the main()
function in the main.cpp file, we instantiate the app
application object, Pinball
state machine, and MainWindow
GUI class. We initialize and start the state machine, show the main window, and execute the application.
© 2022 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.