Prev: 3. The main() Function and the qpn_port.h Header File
Next: 5. Elaborating State Machines of Active Objects
To proceed further with the explanation of the "Fly 'n' Shoot" application, I need to step up to the design level. At this point I need to explain how the application has been decomposed into the active objects, and how these objects exchange events to collectively deliver the functionality of the "Fly 'n' Shoot" game.
In general, the decomposition of a problem into active objects is not trivial. As usual in any decomposition, your goal is to achieve possibly loose coupling among the active object components (ideally no sharing of any resources), and you also strive for minimizing the communication in terms of the frequency and size of exchanged events.
In the case of the "Fly 'n' Shoot" game, I need first to identify all objects with reactive behavior (i.e. with a state machine). I applied the simplest object-oriented technique of identifying objects, which is to pick the frequently used nouns in the problem specification. From Section 2. Let's Play, I identified Ship, Missile, Mines, and Tunnel. However, not every state machine in the system needs to be an active object (with a separate task context, an event queue, and a unique priority level), and merging them is a valid option when performance or space is needed. As an example of this idea, I ended up merging the Mines into the Tunnel active object, whereas I preserved the Mines as independent state machine components of the Tunnel active object. By doing so I applied the "Orthogonal Component" design pattern described in Chapter 5 of Practical UML Statecharts in C/C++, Second Edition.
The next step in the event-driven application design is assigning responsibilities and resources to the identified active objects. The general design strategy for avoiding sharing of resources is to encapsulate each resource inside a dedicated active object and to let that object manage the resource for the rest of the application. That way, instead of sharing the resource directly, the rest of the application shares the dedicated active object via events.
So, for example, I decided to put the Tunnel active object in charge of the display. Other active objects and state machine components, such as Ship, Missile and Mines, don't draw on the display directly, but rather send events to the Tunnel object with the request to render the Ship, Missile, or Mine bitmaps at the provided (x, y) coordinates of the display.
With some understanding of the responsibilities and resource allocations to active object I can move on to devising the various scenarios of event exchanges among the objects. Perhaps the best instrument to aid the thinking process at this stage is the UML sequence diagram, such as the diagram depicted in Figure 4-1. This particular sequence diagram shows the most common event exchange scenarios in the "Fly 'n' Shoot" game (the primary use cases, if you will). The explanation section immediately following the diagram illuminates the interesting points.
Figure 4-1 The sequence diagram of the Fly 'n' Shoot game.
TIME_TICK is the most important event in the game. This event is generated by the QF framework from the system time tick interrupt at a rate of 30 times per second, which is needed to drive a smooth animation of the display. Because the TIME_TICK event is of interest to virtually all objects in the application, it is published by the framework to all active objects. (The publish-subscribe event delivery in QF is described in Chapter 6 of Practical UML Statecharts in C/C++, Second Edition.)TIME_TICK event, the Ship object advances its position by one step and posts the event SHIP_IMG(x, y, bmp) to the Tunnel object. The SHIP_IMG event has parameters x and y, which are the coordinates of the Ship on the display, as well as the bitmap number bmp to draw at these coordinates.TIME_TICK event this time.TIME_TICK event. First, Tunnel redraws the entire display from the current frame buffer. This action performed 30 times per second provides the illusion of animation of the display. Next, the Tunnel clears the frame buffer and starts filling it up again for the next time frame. The Tunnel advances the tunnel walls by one step and copies the walls to the frame buffer. The Tunnel also dispatches the TIME_TICK event to all its Mine state machine components.MINE_IMG(x, y, bmp) event to the Tunnel to render the appropriate Mine bitmap at the position (x, y) in the current frame buffer. Mines of type 1 send the bitmap number MINE1_BMP, while mines of type 2 send MINE2_BMP.SHIP_IMG(x, y, bmp) event from the Ship, the Tunnel object renders the specified bitmap in the frame buffer and checks for any collision between the ship bitmap and the tunnel walls. Tunnel also dispatches the original SHIP_IMG(x, y, bmp) event to all active Mines.PLAYER_TRIGGER event is generated when the Player reliably presses the button (button press is debounced). This event is published by the QF framework and is delivered to the Ship and Tunnel objects, which both subscribe to the PLAYER_TRIGGER event.MISSILE_FIRE(x, y) event to the Missile object. The parameters of this event are the current (x, y) coordinates of the Ship, which are the starting point for the Missile.PLAYER_TRIGGER event as well because Tunnel occasionally needs to start the game or terminate the screen saver mode based upon this stimulus.MISSILE_FIRE(x, y) event by starting to fly, whereas it sets its initial position from the (x, y) event parameters delivered from the Ship.TIME_TICK event arrives while Missile is in flight. Missile posts the MISSILE_IMG(x, y, bmp) event to the Table.MISSILE_IMG(x, y, bmp) event to all the Mines to let the Mines test for the collision with the Missile. This determination depends on the type of the Mine. In this scenario a particular Mine[n] object detects a hit and posts the HIT_MINE(score) event to the Missile. The Mine provides the score earned for destroying this particular mine as the parameter of this event.HIT_MINE(score) event by becoming immediately ready to launch again and lets the Mine do the exploding. As I decided to make the Ship responsible for the scorekeeping, the Missile also generates the DESTROYED_MINE(score) event to the Ship, to report the score for destroying the Mine.DESTROYED_MINE(score) event, the Ship updates the score reported by the Missile.PLAYER_SHIP_MOVE(x, y) event by updating its position from the event parameters.SHIP_IMG(x, y, bmp_id) event next time around, it detects a collision between the Ship and the tunnel wall. In that case it posts the event HIT_WALL to the Ship.HIT_WALL event by transitioning to the "exploding" state.
Prev: 3. The main() Function and the qpn_port.h Header File
Next: 5. Elaborating State Machines of Active Objects
1.5.4