Prev: 4. Designing an Event-Driven Application
Next: 6. Defining Event Signals and Event Parameters
I hope that the analysis of the sequence diagram in Figure 4-1 makes it clear that actions performed by an active object depend as much on the events it receives, as on the internal mode of the object. For example, the Missile active object handles the TIME_TICK event very differently when the Missile is in flight (Figure 4-1(12)) compared to the time when it is not (Figure 4-1(3)). The best known mechanism of handling such modal behavior is through state machines because a state machine makes the behavior explicitly dependent on both the event and the state of an object. In Chapter 2 of Practical UML Statecharts in C/C++, Second Edition I introduce UML state machine concepts more thoroughly. In this section, I give a cursory explanation of the state machines associated with each object in the "Fly 'n' Shoot" game.
TIME_TICK in Figure 5-1(3)). State transitions are represented as arrows originating at the boundary of the source state and pointing to the boundary of the target state. At a minimum, a transition must be labeled with the triggering event. Optionally, the trigger can be followed by event parameters, a guard, and a list of actions.
Figure 5-1 Missile state machine diagram.
TIME_TICK. Subscribing to an event means that the framework will deliver the specified event to the Missile active object every time the event is published to the framework. Chapter 7 of Practical UML Statecharts in C/C++, Second Edition describes the implementation of the publish-subscribe event delivery in QF.MISSILE_FIRE(x, y) event denotes a state transition, that is, change of state from "armed" to "flying". The MISSILE_FIRE(x, y) event is generated by the Ship object when the Player triggers the Missile (see the sequence diagram in Figure 4-1). In the MISSILE_FIRE event, Ship provides Missile with the initial coordinates in the event parameters (x, y).me-> prefix and to the event parameters through the e-> prefix. For example, the action me->x = e->x; means that the internal data member x of the Missile active object is assigned the value of the event parameter x.TIME_TICK enlisted in the compartment below the state name denotes an internal transition. Internal transitions are simple reactions to events performed without a change of state. An internal transition, as well as a regular transition, can have a guard condition, enclosed in square brackets. Guard condition is a Boolean expression evaluated at runtime. If the guard evaluates to TRUE, the transition is taken. Otherwise, the transition is not taken and no actions enlisted after the forward slash "/" are executed. In this particular case, the guard condition checks whether the x-coordinate propagated by the Missile speed is still visible on the screen. If so, the actions are executed. These actions include propagation of the Missile position by one step and posting the MISSILE_IMG event with the current Missile position and the MISSILE_BMP bitmap number to the Tunnel active object. Direct event posting to an active object is accomplished by the QF function QActive::postFIFO(), which I discuss in Chapter 7 of Practical UML Statecharts in C/C++, Second Edition.TIME_TICK with the [else] guard denotes a regular state transition with the guard condition complementary to the other occurrence of the TIME_TICK event in the same state. In this case, the TIME_TICK transition to "armed" is taken if the Missile object flies out of the screen.HIT_MINE(score) triggers another transition to the "armed" state. The action associated with this transition posts the DESTROYED_MINE event with the parameter e->score to the Ship object, to report destroying the mine.HIT_WALL triggers a transition to the "exploding" state, with the purpose of animating the explosion bitmaps on the display.me->exp_ctr) member of the Missile object.TIME_TICK internal transition is guarded by the condition that the explosion does not scroll off the screen, and that the explosion counter is lower than 16. The actions executed include propagation of the explosion position and posting the EXPLOSION_IMG event to the Tunnel active object. Please note that the bitmap of the explosion changes as the explosion counter gets bigger.TIME_TICK regular transition with the complementary guard changes the state back to the "armed" state. This transition is taken after the animation of the explosion completes.
One of the main responsibilities of the Ship active object is to maintain the current position of the Ship. On the original LM3S811 board, this position is determined by the potentiometer wheel (see Figure 2-2). The PLAYER_SHIP_MOVE(x, y) event is generated whenever the wheel position changes, as shown in the sequence diagram (Figure 4-1). The Ship object must always keep track of the wheel position, which means that all states of the Ship state machine must handle the PLAYER_SHIP_MOVE(x, y) event.
In the traditional finite state machine (FSM) formalism, you would need to repeat the Ship position update from the PLAYER_SHIP_MOVE(x, y) event in every state. But such repetitions would bloat the state machine and, more importantly, would represent multiple points of maintenance both in the diagram and the code. Such repetitions go against the DRY principle (Don't Repeat Yourself), which is vital for flexible and maintainable code.
Hierarchical state nesting remedies the problem. Consider the state "active" that surrounds all other states in Figure 5-2. The high-level "active" state is called the superstate and is abstract in that the state machine cannot be in this state directly, but only in one of the states nested within, which are called the substates of "active". The UML semantics associated with state nesting prescribes that any event is first handled in the context of the currently active substate. If the substate cannot handle the event, the state machine attempts to handle the event in the context of the next-level superstate. Of course, state nesting in UML is not limited to just one level and the simple rule of processing events applies recursively to any level of nesting.
Specifically to the Ship state machine diagram shown in Figure 5-2, suppose that the event PLAYER_SHIP_MOVE(x, y) arrives when the state machine is in the "parked" state. The "parked" state does not handle the PLAYER_SHIP_MOVE(x, y) event. In the traditional finite state machine this would be the end of story—the PLAYER_SHIP_MOVE(x, y) event would be silently discarded. However, the state machine in Figure 5-2 has another layer of the "active" superstate. Per the semantics of state nesting, this higher-level superstate handles the PLAYER_SHIP_MOVE(x, y) event, which is exactly what's needed. The same exact argumentation applies for any other substate of the "active" superstate, such as "flying" or "exploding", because none of these substates handle the PLAYER_SHIP_MOVE(x, y) event. Instead, the "active" superstate handles the event in one single place, without repetitions.
Figure 5-2 Ship state machine diagram.
TIME_TICK and PLAYER_TRIGGER.PLAYER_SHIP_MOVE(x, y) event as an internal transition in which it updates the internal data members me->x and me->y from the event parameters e->x and e->y, respectively.SCORE with the event parameter me->score to the Tunnel active object.TIME_TICK internal transition causes posting the event SHIP_IMG with current Ship position and the SHIP_BMP bitmap number to the Tunnel active object. Additionally, the score is incremented for surviving another time tick. Finally, when the score is "round" (divisible by 10) it is also posted to the Tunnel active object. This decimation of the SCORE event is performed just to reduce the bandwidth of the communication, because the Tunnel active object only needs to give an approximation of the running score tally to the user.PLAYER_TRIIGGER internal transition causes posting the event MISSILE_FIRE with current Ship position to the Missile active object. The parameters (me->x, me->y) provide the Missile with the initial position from the Ship.DESTROYED_MINE(score) internal transition causes update of the score kept by the Ship. The score is not posted to the Table at this point, because the next TIME_TICK will send the "rounded" score, which is good enough for giving the Player the score approximation.HIT_WALL event triggers transition to "exploding"HIT_MINE(type) event also triggers transition to "exploding"TIME_TICK[else] transition is taken when the Ship finishes exploding. Upon this transition, the Ship object posts the event GAME_OVER(me->score) to the Tunnel active object to terminate the game and display the final score to the Player.The Tunnel state machine uses state hierarchy more extensively than the Ship state machine in Figure 5-2. The explanation section immediately following Figure 5-3 illuminates the new uses of state nesting as well as the new elements not explained yet in the other state diagrams.
Figure 5-3 Tunnel state machine diagram.
PLAYER_QUIT event as a transition to the final state (see explanation of element (3)). Please note that the PLAYER_QUIT transition applies to all substates directly or transitively nested in the "active" superstate. Because a state transition always involves execution of all exit actions from the states, the high-level PLAYER_QUIT transition guarantees the proper cleanup that is specific to the current state context, whichever substate happens to be active at the time when the PLAYER_QUIT event arrives.PLAYER_QUIT event indicates termination of the game.MINE_DISABLED(mine_id) event is handled at the high level of the "active" state, which means that this internal transition applies to the whole sub-machine nested inside the "active" superstate. (See also the discussion of Mine object in the next section.)me->screenTimeEvt to expire in 20 seconds. Time events are allocated by the application, but they are managed by the QF framework. QF provides functions to arm a time event, such as QTimeEvt_postIn() for one-shot timeout, and QTimeEvt_postEvery() for periodic time events. Arming a time event is in effect telling the QF framework, for instance, "Give me a nudge in 20 seconds". QF then posts the time event (the event me->screenTimeEvt in this case) to the active object after the requested number of clock ticks. Chapters 6 and 7 of Practical UML Statecharts in C/C++, Second Edition talk about time events in detail.PLAYER_TRIGGER transition.SCREEN_TIMEOUT transition to "screen_saver" is triggered by the expiration of the me->screenTimeEvt time event. The signal SCREEN_TIMEOUT is assigned to this time event upon initialization and cannot be changed later.PLAYER_TRIGGER applies equally to the two substates of the "screen_saver" superstate.
Figure 5-4 The Table active object manages two types of Mines.
Figure 5-5 shows a hierarchical state machine of Mine2 state machine. Mine1 is very similar, except that it uses the same bitmap for testing collisions with the Missile and the Ship.
Figure 5-5 Mine2 state machine diagram.
MINE_PLANT(x, y) event to the Mine. The Tunnel provides the (x, y) coordinates as the original position of the Mine.mines[] array (see also Figure 5-4(4)). The mine_id parameter of the event becomes the index into the mines[] array. Please note that generating the MINE_DISABLDED(mine_id) event in the exit action from "used" is much safer and more maintainable than repeating this action in each individual transition (3), (4), (5), and (6).
1.5.4