News Pedal Plane game Files Company Info

Pedal Plane Technical Description & Rationale
Here follow explanations of the design and implementation of Applied Widgetronics' Pedal Plane game.
Design

The game is structured around a basic state machine. Each state has three associated functions: ENTER, UPDATE, and LEAVE.

The ENTER function is called exactly once as the game transitions into the related state. This allows state setup to be performed. Common tasks handled by a state ENTER function include resource acquisition, HUD initialization, and model loading.

The UPDATE function is called once for each game update cycle. It performs the normal tasks associated with a state. For example, the game-play state's UPDATE function reads input from the user, performs the flight physics computation, and updates the scene graph.

The LEAVE function is the counterpart to the ENTER function. It is called exactly once as the game transitions out of the related state. This allows the release of resources acquired by the ENTER function.

State transition is triggered by the return value of the UPDATE function. If the game's main control loop detects that an UPDATE function has returned a state enumerator denoting a state other than the running state, then a state transition occurs. During a state transition, the LEAVE function of the current state is called, and the ENTER function of the next state is called.

As an example, let the game be in the normal game-play state. The user is doing his best to fly the plane. The global variable "curr_state" has the value STATE_LEVEL. The function "level_update" is being called regularly. During normal play, "level_update" returns this same value STATE_LEVEL to indicate that nothing has occurred which would trigger a transition. However, if collision detection indicates contact with the goal entity then "level_update" returns STATE_GOAL. In response, the main control loop invokes "level_leave", followed by "goal_enter" and sets "curr_state" to STATE_GOAL. Thus "goal_update" will be called on the next iteration. Functions are associated with state enumerators via a select statement.

The final game includes 9 states. This is 3 more than were described in the original design document, which did not foresee the pause before play, the fade-out after play, or the need for a null initial state.

STATE_NULL
This is the initial state. Its functions do nothing. It is merely used to guarantee that the ENTER function of the initial state is invoked the first time through.
STATE_TITLE
The title state displays the title screen. The ENTER function acquires the title and help overlays. The UPDATE function cycles through these overlays, moves the character entity along its demo path, and manages the fade-in entity if need be. It checks for a button or key press and transitions to the selection state.
STATE_SELECT
The select state displays the level selector, game-type selector, and high scores. UPDATE looks for input from the user and updates the selections accordingly. Upon activation, it transitions to the start state. ENTER and LEAVE acquire and release resources.
STATE_START
The player views the start state as the pause after a level is selected, but before play begins. The "Get Ready" overlay appears and indicates how to select the input type. The start state's ENTER function has the important task of loading the course file according to selections made during the select state. Upon input, the input mode is specified and the game transitions to the level state.
STATE_LEVEL
This is the normal game-play state. The UPDATE function is the most complex in the game, having to perform input, physics, course animation, collision detection, timer function, and the update of the player, shadow, HUD, camera, and particles. When collision between the player and environment is detected, a transition to the crash state is triggered. When collision between the player and the goal is detected, then either the goal state or the high state is triggered, depending on the value of the timer. Surprisingly, this state's ENTER and LEAVE functions are empty, relying upon adjacent states to manage resources.
STATE_CRASH
This state represents humiliating defeat. It displays the game-over overlay and updates the particle system that scatters the broken pieces of the plane. Upon angry button-press it transitions to fade-out.
STATE_GOAL
This state represents success in completing the course, but failure to set a high score. The physics update is halted and the player is frozen in time at the finish line. A transition to fade-out is triggered upon button-press.
STATE_HIGH
This state represents glorious victory. It is similar to the goal state, but it displays the player's initials. It responds to keyboard input for editing. Upon triumphant button-press it transitions to fade-out.
STATE_FADEOUT
This is a clean-up state that all end-of-game states transition to. The update function blends the fader entity toward opaque and fades the audio volume before finally transitioning back to the title state.
Flight Physics

The flight physics implementation is a colossal hack. It began with a considered approach to flight simulation but, as simplifications were made in an effort to increase accessibility, the code evolved to resemble neither a good-thing nor a fun-thing. In the end, it feels good in-game, and that's the final word.

Three forces act on the aircraft: gravity, thrust, and lift. However, these are not integrated as forces in-game. The three contributing influences are computed as instantaneous velocities. Gravity is a constant (0, -0.33, 0), which is 9.8 m/s downward over 1/30 of a second (one frame interval). Thrust is a vector along the path of the plane, proportional to a function of health. Lift is a vector perpendicular to the wings of the plane, proportional to the magnitude of the current velocity.

These three vectors are summed, with the result representing something like a reasonable instantaneous velocity for the plane. A weighted average between this and the current real velocity is taken. The craft is pointed in that direction, subject to constraints that prevent it from pitching or rolling into an inverted attitude.

Pedal Plane is to flight simulation as Asteroids is to astronomy.

Course Representation

Each course is represented as two lists of records.

A list of "Health" records stores the food balloons including their type, their value, their location, and entities representing the balloon model, food model, and halo. The entities have type COLLISION_HEALTH. When a collision of this type is detected, the player is credited with the stamina, and the entity and record are freed.

A list of "Checkpoint" records stores the checkpoint rings including their position, orientation, and entities representing the ring and radar point. When the course file is loaded, the record list is sorted in path order. So, "First Checkpoint" may be taken as the starting point and "Last Checkpoint" may be taken as the goal. The entities have type COLLISION_COURSE. When a collision of this type is detected, the record and entities are freed and the highlight halo is moved to the new "First". When the record list is empty, a goal is triggered.

The course file storing these entities is saved directly from GtkRadiant in its native source map format. Technically, it is a valid unprocessed Quake3 level file. This format is syntactically regular and very easy to parse. A parser was written in Blitz to read and process the Quake3 entities for health power-ups (as Health records) and motion paths (as Checkpoint records).

The Radiant file format was convenient because Radiant was also used to define the geometry of the environment. Using a single tool to define both the environment and objects within that environment was ideal.

Effects
Shadow

Early testers complained that they could not tell how high they were, especially when flying over water. To rectify this problem, a shadow was implemented.

While there are a number of possible shadow implementations, most were not suitable in this case. The old flat gray plane trick would not work, as it would not adapt properly to the shape of the terrain. Blitz does not expose enough 3D functionality to support the implementation of a high-quality general-purpose shadow algorithm, so a compromise had to be found.

The final implementation took advantage of multi-texture. A small plane shadow image was created in gray on white. This image was loaded with U and V clamp enabled. It was applied to the terrain as a secondary texture in modulation mode. The clamped white border of the texture spread across the entire terrain, but modulation against white would show no change. The gray pixels in the center of the image would darken only a small plane-sized portion of the terrain. A texture coordinate transformation was applied to this texture every frame to ensure that these darkened pixels remained positioned below the 3D plane, and oriented to match its bearing.

There are problems with this implementation. First, as the plane pitches or rolls, one would expect the shadow to shorten or narrow. While this would have been possible, it was deemed unnecessary. Second, Blitz applies texture translation before rotation. The shadow required that rotation be applied before translation. So trigonometric gymnastics were required to compute a pre-anti-rotated translation that would fall into the correct position after the true rotation was applied. In this context, the additional scale operation would have been uncomfortably complex.

Camera

A sloppy follow camera was found to increase the perceived interactivity and tangibility of the aircraft. A rigid tail-cam reduced the character entity to little more than a visual obstacle. So, an interpolated camera was implemented.

This led to a number of nice visual effects. When the player pedaled, the plane would lurch ahead, leaving the camera sightly behind, as if it were struggling to keep up. This provided a sense of motion that wasn't otherwise apparent. In addition, the lazy camera allowed views of the side of the plane during tight maneuvers.

The implementation of this was simple: two view positions were maintained. The first was the computed ideal camera position relative to the plane. The second was the actual position of the camera. At each update a weighted average of the two was taken. actual = (actual * 7 + ideal) / 8. Thus, the actual view would quickly, but asymptotically approach the ideal.

Crash

A number of ideas for visual representations of the plane crash were tossed about. In the end, simplicity won out. The plane model was carved into 6 chunks. In the event of a crash, the one animated plane model was replaced with these 6 chunks. Each chunk was given a velocity computed from the original velocity of the plane, the normal of the collision, and a random vector. The motion of these "particles" was then integrated under gravity until colliding a second time with the terrain.

HUD

The heads-up-display and all 2D text overlays were implemented using Blitz sprites. Sprites were found to be preferable to image overlays because they allowed for subtle blending effects, and they were automatically visible in both eyes without special consideration.

No text rendering was done in-game. In all cases, text was pre-rendered to PNG images. Digits 0 through 9 were rendered to separate images and combined into sprite hierarchies for timer displays. Letters A through Z were rendered to separate images and combined for player initial displays.

Halo

In an attempt to improve the visibility of significant in-game entities, several sprites were used as highlight halos. The current destination ring uses a pair of counter-rotating bands of light. Food uses a pulsing rainbow. In each case, these are simple Blitz billboards.

Dynamic Audio

There are three dynamic sounds in the game: water, wind, and wheel. Each of these are loaded, looped, and played once at the beginning of the game, with their channel volumes set at zero.

The water sound is heard when the player is near water. It keys off the altitude of the player entity. Channel volume is linearly interpolated from 0 to 1 as the plane moves from 0 to 64 meters on the Y axis.

The wind sound is heard when the player is moving fast. Channel volume is the magnitude of plane velocity, which is already roughly 0 to 1.

The wheel sound is heard while the player is pedaling. The squeak sample plays at 22050Hz, and channel pitch is varied plus-or-minus 5000Hz as stamina ranges from full to empty.

Fade-out

The game fades in at the title screen, and out again when a game ends. The effect is produced using a single black sprite parented to the camera and placed slightly farther out than the near clipping plane. The sprite's opacity is slowly increased during a fade-out, and decreased during a fade-in.


Copyright 2004 Applied Widgetronics