Game Loop Functions

The game loop is the core code that executes once per frame of gameplay and dispatches all of the per-frame functionality needed to run each of the game’s subsystems. This loop is responsible for controlling the overall pace of gameplay, screen redrawing, input handling, and the movement of the player and all of the elements that exist in the game world. The game loop is also responsible for checking to see if the current level has been “won” and calling for a level change in that case.

The game loop runs continually until the entire episode has been won or the user quits the game.

Supporting Functions

Before the game loop begins, a pair of functions (InitializeEpisode() and InitializeLevel()) run to set up all of the global variables needed for the game to work. InitializeEpisode() is responsible for setting up the initial state of the game (so the player starts on the first level, with three filled health bars, etc.) when a fresh game is started. In the case where a saved game is loaded, LoadGameState() performs these actions using the saved values instead.

InitializeLevel() is responsible for loading the map data into memory and setting up the initial state of the level that is being entered. This function runs each time a level is entered – via regular level progression, loading a saved game, beginning a new game, or by the player dying and restarting the current level. In broad strokes, InitializeLevel() initializes all of the variables that are processed during each iteration of the game loop.

A third function, InitializeMapGlobals() is called by InitializeLevel() to set the initial state of many global variables pertaining specifically to player movement and map object interactivity.


The InitializeEpisode() function sets up the initial values for a few of the episode-related global variables tracked by the game. This sets up the initial score, health, level number, etc. when a new game is started. This places the player on the first level with a zero score, three full bars of health, and so forth.

This function is not used when a saved game is loaded; the save file contains its own values that should be restored. See LoadGameState() for that implementation.

void InitializeEpisode(void)
    gameScore = 0;
    playerHealth = 4;
    playerHealthCells = 3;
    levelNum = 0;
    playerBombs = 0;
    gameStars = 0;
    demoDataPos = 0;
    demoDataLength = 0;
    usedCheatCode = false;
    sawBombHint = false;
    sawHealthHint = false;

This function contains no logic; it simply sets the default initial values for all of the relevant game globals. gameScore, playerBombs, and gameStars are all set to zero, which sets the initial state of those elements on the status bar. playerHealth and playerHealthCells are set to four and three respectively, setting the player’s initial health to 4 (i.e. the player can be damaged four times without dying) and the number of health bars available in the status bar to 3.

By setting levelNum to zero, the first level is selected for play. (See level and map functions for information about the level progression.) demoDataPos and demoDataLength are both zeroed to ensure that any subsequent demo file playback or recording operations start in a sensible state.

Finally, the usedCheatCode, sawBombHint, and sawHealthHint flags are set to false. The usedCheatCode flag prevents the cheat key sequence from being used more than once during an episode, and the remaining flags determine if contextual hint dialogs should be shown as different events happen in the game. In this state, the game presumes that the user might not know how to use bombs or how to regain health.

Note: There is an additional hint variable pounceHintState that is not initialized here; that happened earlier in the “begin new game” branch of TitleLoop().


The InitializeLevel() function initializes all of the global variables and related subsystems for the start (or restart) of the level identified by level_num. This function runs every time a level is entered, whether by starting a new game, playing a demo, loading a game, or restarting due to player death. This function handles the screen/music transitions, loads the level data, initializes the player, and sets up all of the interactive elements on the map.

void InitializeLevel(word level_num)
    FILE *fp;
    word bdnum;

    if (level_num == 0 && isNewGame) {
    } else {

In the case when the level_num being entered is the zeroth level and the isNewGame flag is set, this function knows that the user chose to begin a new game (as opposed to loading a game or starting demo playback that might happen to be on the zeroth level). In this case, the DrawFullscreenImage() function displays IMAGE_ONE_MOMENT (an image of an alien, who we learn much later is Zonk, saying “One Moment”) and WaitSoft() inserts an artificial (but partially skippable) delay of 300 game ticks. If the user chooses to skip the delay, the subsequent code will take a perceptible amount of time to load the level data and construct the backdrop image tables, so the image will not immediately disappear on many computers.

I guess that’s growing up.

All my childhood, I wondered what the game was doing while it was showing this “One Moment” screen. I always figured it was doing some important calculation or data load operation, but it turns out it’s just sitting idle. In retrospect, I probably should’ve realized that it wasn’t doing anything critical since demos and saved games loaded without such a delay.

In all other cases, the screen immediately fades out due to a call to FadeOut() and the function continues with the screen blank.

    fp = GroupEntryFp(mapNames[level_num]);
    mapVariables = getw(fp);

The passed level_num is looked up in the mapNames[] array to translate it into a group entry name. This name is passed to GroupEntryFp() which looks up the correct data in the group files and returns a file stream pointer to this data in fp.

The first two bytes in the map file format are map variables that control a few details about the global environment of the map. These are read with a call to getw() and stored in the 16-bit word variable mapVariables. Since this is the only part of the map data that needs to be processed here, fp is closed by the call to fclose().

Note: The map file will be reopened as part of the subsequent LoadMapData() call, which is a bit redundant and wasteful.

    hasRain = (bool)(mapVariables & 0x0020);
    bdnum = mapVariables & 0x001f;
    hasHScrollBackdrop = (bool)(mapVariables & 0x0040);
    hasVScrollBackdrop = (bool)(mapVariables & 0x0080);
    paletteAnimationNum = (byte)(mapVariables >> 8) & 0x07;
    musicNum = (mapVariables >> 11) & 0x001f;

StopMusic() stops any menu or in-game music that might be playing. It has to happen somewhere, may as well be here.

Next, the mapVariables are decoded. The 16-bit value is packed according to the map variables table, and its bit fields are extracted into the boolean hasRain, hasHScrollBackdrop, and hasVScrollBackdrop variables, while the numeric fields are stored in bdnum, paletteAnimationNum, and musicNum. The bdnum variable contains the map’s backdrop number, which is handled locally and does not get stored in any global variables here.


InitializeMapGlobals() is responsible for initializing almost four dozen global variables pertaining to player movement, actor interaction, and map state. There is no logic performed in there; all of these variables are unconditionally set to the same initial value every time this function is called.

    if (IsNewBackdrop(bdnum)) {
        LoadBackdropData(backdropNames[bdnum], mapData.b);

Here the backdrop data is loaded and the scrolling preprocessing is done if needed. This is governed by calling IsNewBackdrop with the map’s bdnum as the argument. If the new backdrop is the same as the backdrop that has previously been loaded, there is no reason to load it again and the if body does not execute. Otherwise, LoadBackdropData is called to prepare the new backdrop for use. The backdrop’s group file name is looked up in the backdropNames[] array by using bdnum as the index.

LoadBackdropData requires a block of memory to use as scratch space, and the byte-addressable view into mapData is large enough to serve that purpose. After the load is complete, mapData and its contents are no longer needed and it can be refilled with the actual, correct map data.


    if (level_num == 0 && isNewGame) {
        isNewGame = false;

The actual loading of the map data and the construction of the actors it contains is handled in the LoadMapData() function. It takes the level_num as an argument to specify which level to load.

Similar to the beginning of the function, a special condition checks for the case where a new game has been started. In this case, the “One Moment” image is still visible on the screen, so FadeOut() takes it down. To prevent it from showing again if this level restarts, the isNewGame flag is unset here.

    if (demoState == DEMOSTATE_NONE) {
        switch (level_num) {
        case 0:
        case 1:
        case 4:
        case 5:
        case 8:
        case 9:
        case 12:
        case 13:
        case 16:
        case 17:

This block of code handles UI feedback in the form of the “Now entering level…” text before each level begins. If demoState is DEMOSTATE_NONE, a demo is neither being recorded nor played back and such feedback is appropriate. The switch cases mirror the level progression of the game. This ensures that this dialog only appears before maps 1–10, without appearing before bonus levels or the eleventh level of the first episode.

For these regular levels, the dialog is shown by calling both SelectDrawPage() and SelectActivePage() with the zeroth video page as the argument. This makes it so that the effect of all draw functions becomes immediately visible without the page-flipping machinery getting in the way. ClearScreen() erases this draw page by replacing it with solid black tiles, and FadeIn() restores the palette to its normal state, making this solid black screen visible.

ShowLevelIntro() displays the “Now entering level…” dialog with level_num used to influence the number shown in the message. WaitSoft() pauses on the message for an interruptible 150 game ticks, then FadeOut() fades the message away. From there, execution breaks back to the main flow of the function.


These functions initialize the actor-like elements that all maps can have, as well as a bit of player movement.

InitializeShards, InitializeExplosions, InitializeDecorations, and InitializeSpawners reset the fixed-size arrays for the map’s shards, explosions, decorations, and spawners, respectively. Each level should start with none of these in an active state, so this clears any such elements that might have been left running from the previous map.

ClearPlayerPush resets the global player control variables that come into play when the player is involuntarily pushed around the map. Each map should begin with the player in a “not pushed” state, and this function achieves that.

    activePage = !activePage;

Next the in-game drawing environment is set up. ClearGameScreen() erases the screen and draws the status bar background and current score/bombs/health/stars values on top of it. This sets up the static areas of the game screen on both video pages.

The initial state of the page-flipping is set up next. In steady-state, the value in activePage represents the page number that is currently being displayed on the screen, leaving the opposite page number (the “draw” page) to be the one being redrawn in the background. This sequence reverses the pages’ roles: SelectDrawPage() tells drawing to occur on the (old) active page, then activePage is negated to produce the (new) active page, and SelectActivePage() displays this (new) active page.

Visually this does not do anything at this point in the execution, since both pages contain identical content, but it does ensure that the pages are in a reasonable state for the game loop to begin drawing and flipping pages itself later. The GameLoop() function performs these same steps at the end of each frame it draws.


SaveGameState() saves a snapshot of the current level’s global variables into the temporary save slot (identified by the character 'T'), which defines the restore point that will be subsequently used if the player dies and the level needs to restart with the score/health/etc. that the player had when they initially entered.

StartGameMusic() loads and begins playing the map’s chosen music (identified by musicNum) from the beginning. If there is no AdLib hardware installed or the music is disabled, this function skips doing some of that work.

    if (!isAdLibPresent) {
        tileAttributeData = miscData + 5000;
        miscDataContents = IMAGE_TILEATTR;

This code is the inverse implementation of a similar fragment found near the end of Startup(). In this case, the if body is executed when the system does not have an AdLib card installed (isAdLibPresent is false). The reason for this separation is for reasons of memory efficiency: If the system does not have an AdLib card, there is no need to load any music data into the memory backed by miscData and that space can be repurposed to hold tile attribute data instead. (Otherwise, when miscData is holding music, there is a separate allocation of memory that tileAttributeData permanently points to.)

Here miscData points to a 35,000 byte arena of memory, where the first 5,000 bytes are reserved for demo data that may be playing or being recorded. The tileAttributeData pointer is set to the first byte past the end of this data. miscDataContents is set to IMAGE_TILEATTR to mark that this memory has been claimed for this purpose. Finally, LoadTileAttributeData() loads the data from the named group entry into this memory.


At this point in the execution, the screen is faded to black via palette manipulation, but the video memory contains a black game window with an initialized status bar at the bottom of the screen. The call to FadeIn() fades this into view. FadeIn() blocks until completion, so the initialization does not proceed (and the game loop does not start) until after it returns.

    if (paletteAnimationNum == PALANIM_EXPLOSIONS) {
        SetPaletteRegister(PALETTE_KEY_INDEX, MODE1_BLACK);

There is one last bit of palette manipulation that occurs in episode three, which is the only episode with the EXPLOSION_PALETTE macro defined. In this episode, the level’s paletteAnimationNum is checked to see if it matches PALANIM_EXPLOSIONS, in which case the special “flash during explosions” palette animation mode is activated. In this mode, all magenta areas of the screen show as black by default, but flash bright white and yellow while explosions occur. To facilitate the initial state for this mode, SetPaletteRegister() is called to set the palette register named by PALETTE_KEY_INDEX (which represents magenta areas in the game’s graphics) to the EGA’s MODE1_BLACK color.


The InitializeMapGlobals() function resets many of the global variables pertaining to player movement and map/actor interactivity. This function is called whenever a level begins and ensures it has a consistent and clean state, with nothing from the previous level carried over.

void InitializeMapGlobals(void)
    winGame = false;
    playerClingDir = DIR4_NONE;
    isPlayerFalling = true;
    cmdJumpLatch = true;
    playerJumpTime = 0;
    playerFallTime = 1;
    isPlayerRecoiling = false;
    playerMomentumNorth = 0;
    playerFaceDir = DIR4_EAST;
    playerFrame = PLAYER_WALK_1;
    playerBaseFrame = PLAYER_BASE_EAST;
    playerDeadTime = 0;
    winLevel = false;
    playerHurtCooldown = 40;
    transporterTimeLeft = 0;
    activeTransporter = 0;
    isPlayerInPipe = false;
    scooterMounted = 0;
    isPlayerNearTransporter = false;
    isPlayerNearHintGlobe = false;
    areForceFieldsActive = true;
    blockMovementCmds = false;


    blockActionCmds = false;
    arePlatformsActive = true;
    isPlayerInvincible = false;
    paletteStepCount = 0;
    randStepCount = 0;
    playerFallDeadTime  = 0;
    sawHurtBubble  = false;
    sawAutoHintGlobe = false;
    numBarrels = 0;
    numEyePlants = 0;
    pounceStreak = 0;

    sawJumpPadBubble =
        sawMonumentBubble =
        sawScooterBubble =
        sawTransporterBubble =
        sawPipeBubble =
        sawBossBubble =
        sawPusherRobotBubble =
        sawBearTrapBubble =
        sawMysteryWallBubble =
        sawTulipLauncherBubble =
        sawHamburgerBubble = false;

This function contains no logic and behaves identically in every context where it is run. It sets the following variables:

  • winGame is set to false, ensuring that the player must reach an episode-specific goal to complete the game and see the end story.
  • playerClingDir is set to DIR4_NONE, indicating that the player is not currently clinging to any walls. In this state, the player is standing on solid ground (or possibly free-falling toward it).
  • isPlayerFalling is set to true, which simplifies the interaction between the player’s starting position and the various movement variables that control the player. The game assumes that the player starts every map in empty space, ready to free-fall. Whenever the player is free-falling, the MovePlayer function continually tries to pull the player down until they land on a solid map tile. If the player’s start position should happen to be on such a map tile already, the free-fall will be canceled and the player will be switched to a standing state immediately.
  • cmdJumpLatch = true
  • playerJumpTime = 0
  • playerFallTime = 1
  • isPlayerRecoiling = false
  • playerMomentumNorth = 0
  • playerFaceDir is set to DIR4_EAST, which makes the player start each level facing east. By convention, the player usually starts toward the left side of the map, looking in the direction they need to travel to progress.
  • playerFrame is set to PLAYER_WALK_1, but this assignment is not that important since this value will be immediately overwritten with the correct standing/falling frame during the next call to MovePlayer.
  • playerBaseFrame is set to PLAYER_BASE_EAST, following the same motivations as playerFaceDir above.
  • playerDeadTime is set to zero, indicating that the player has not died yet.
  • winLevel is set to false, ensuring that the player must reach a map-specific goal to complete the level and progress to the next one.
  • playerHurtCooldown is initialized to 40. This makes the player invincible for roughly the first four seconds of gameplay to allow them to get situated in their surroundings without taking damage. This invincibility is accompanied by the player sprite flashing.
  • transporterTimeLeft is set to zero, since the player is not using any transporters at the start of the level.
  • activeTransporter is set to zero, indicating that there is currently no transporter actor being interacted with.
  • isPlayerInPipe is set to false, conceptually placing the player “outside” the pipe system where the pipe corner actors will have no effect.
  • scooterMounted is set to zero, ensuring the player enters every map without a scooter.
  • isPlayerNearTransporter and isPlayerNearHintGlobe are both set to false, since the player should not be touching either of these actor types when entering a level.
  • areForceFieldsActive is set to true. The game’s design is such that force fields are always active when a level is started, and can only be deactivated by using the Foot Switch (deactivates force fields).
  • blockMovementCmds is set to false, allowing the player to walk and jump freely.
  • ClearPlayerDizzy is called to reset the state variables pertaining to the “dizzy” immobilization the player can sometimes experience.
  • blockActionCmds is set to false, allowing the player to be moved freely.
  • arePlatformsActive is set to true, specifying that all platforms on the map are active and moving by default. As the map is loaded, the presence of a Foot Switch (activates platforms) may turn this off, requiring the player to find and interact with that switch to make the platforms run again.
  • isPlayerInvincible is set to false, since the player will not have an Invincibility Sphere to start with.
  • paletteStepCount is set to zero, ensuring that any palette animations used on the current map start at the beginning of their color sequence.
  • randStepCount is set to zero, which resets the pseudorandom number generator in GameRand to a predictable and stable state. This is critically important to ensure that actors that use randomness behave identically during demo recording and playback.
  • playerFallDeadTime is set to zero, indicating that the player has not fallen off the bottom of the map yet.
  • sawHurtBubble is set to false, allowing the “OUCH!” bubble to show once the player is hurt for the first time.
  • sawAutoHintGlobe is set false, which will auto-activate the first hint globe the player happens to touch.
  • numBarrels and numEyePlants are both set to zero. These variables track the number of barrels/baskets and the number of Eye Plants on the map, respectively. Since each map initially loads with zero actors of any type, zero is appropriate here.
  • pounceStreak is set to zero, eliminating any chance of a carryover of the previous map’s pounce streak.
  • All of the speech bubble flags – sawJumpPadBubble, sawMonumentBubble, sawScooterBubble, sawTransporterBubble, sawPipeBubble, sawBossBubble, sawPusherRobotBubble, sawBearTrapBubble, sawMysteryWallBubble, sawTulipLauncherBubble, and sawHamburgerBubble – are set to false. This re-enables the speech bubbles (generally “WHOA!” but occasionally “UMPH!”) that appear when the player first interacts with each of these actor types.


The GameLoop() function runs once for each frame of gameplay and is responsible for running the appropriate sub-functions for timing, input, player/actor movement, world drawing, and level exit conditions. This function takes a demostate argument which should be one of the DEMOSTATE_* constants – this controls the presence of the “DEMO” overlay sprite and is passed through to the input handling functions.

The game loop is structured as a true infinite loop that can only terminate under the following conditions:

  1. The user presses one of the “quit game” keys and confirms their choice at the prompt. In this case the loop exits immediately with a return statement.
  2. The episode is “won.” In this case, the loop exits due to a break statement and the ending story is shown before this function returns.

In either case, this function returns back to the title loop in the InnerMain() function.

Overall Structure

void GameLoop(byte demostate)
    for (;;) {
        while (gameTickCount < 13)
            ;  /* VOID */

        gameTickCount = 0;

        /* ... Sub-function calls are discussed below ... */

        if (winLevel) {
            winLevel = false;
        } else if (winGame) {


Only the outermost structure of the game loop is shown here for clarity. Almost the entire function is contained inside the body of an infinite for loop. On each iteration, the value of gameTickCount is checked, and execution only proceeds once the count reaches 13. (This counter is incremented externally in the PCSpeakerService() function, which is called by a timer interrupt 140 times per second.) This busy loop ensures that the game waits at least 13 ∕ 140 seconds between successive frames, effectively creating a frame-rate limiter a bit shy of 11 frames per second. Once the requisite amount of time has passed, gameTickCount is reset to zero to set up for the delay on the subsequent iteration and the rest of the game loop body runs.

For a fast computer that can draw a frame quickly, this loop eats up the remaining unused time before another loop iteration is allowed to start. On a very slow computer, it’s possible for gameTickCount to be at or above 13 by the time the next game loop iteration is entered, resulting in no busy wait at all. In these cases, the gameplay will appear noticeably sluggish because there are no mechanisms in place to skip frames or adjust the movement speeds of objects.

After the timing loop, the sub-functions are called. They are discussed below.

Near the end of the loop body, a check is made against the winLevel flag. If it holds a true value, the player interacted with a level-winning object on the map during the current iteration. When this happens, the winLevel is set back to false, the SND_WIN_LEVEL fanfare is queued via a call to StartSound(), and the next level number is selected by NextLevel(). This has the side-effect of changing the value in levelNum. That new level number is passed to InitializeLevel(), which loads the new map data and reinitializes all of the relevant global state to make it playable from the beginning. On the next iteration of the game loop, the player will be in the starting position of that new map with fresh actors to face.

A different exit condition can be reached when winGame is true, in which case the player has qualified to win the entire episode of the game during the current iteration of the game loop. In this case, execution breaks out of the infinite for loop and falls to the ShowEnding() call. This shows the relevant end story for the episode being played, and the GameLoop() function returns once the story has been shown to completion.


During each iteration of the outer infinite for loop, after the timing loop and before the end-level checks, the bulk of the function calls occur. There is very little logic done in the game loop itself; almost everything is handled in a function specific to each of the game’s subsystems.


If the current map has enabled one of the palette animation modes, each call to AnimatePalette() will “step” the animation to the next color defined in the sequence and change the visual representation of the magenta areas on the screen.

        {  /* for scope */
            word result = ProcessGameInputHelper(activePage, demostate);
            if (result == GAME_INPUT_QUIT) return;
            if (result == GAME_INPUT_RESTART) continue;


        if (scooterMounted != 0) {

        if (queuePlayerDizzy || playerDizzyLeft != 0) {

This section handles the input devices (keyboard, joystick, or the demo system) and moves the player through the map in response.

ProcessGameInputHelper() performs input handling based on the value passed in demostate. In the case where a demo is being played back, input is ignored except as a signal to quit. Otherwise input is accepted and used for player movement. ProcessGameInputHelper() can also display menus and dialogs. To do this it must bypass the usual page-flipping mechanism and draw directly to the video page in activePage – that is why it is passed here.

ProcessGameInputHelper() returns a result code that can exit or restart the game loop. In the case of GAME_INPUT_QUIT, the user has requested to quit the game with Esc or Q, finished/dismissed the playback of a demo, or ran out of space while recording a demo. For these cases, return from the game loop immediately. For GAME_INPUT_RESTART, either a saved game was restored or the level warp cheat was used. In either case, the global state of the game world has changed and the game loop must be restarted from the top; continue accomplishes this.

The call to MovePlayer interprets the raw movement commands read from the input device and uses them to adjust the position of the player in the game world. This is a heinously complicated function with one simple to understand precondition – if the player happens to be riding a scooter, it returns without doing anything.

The game loop checks the player’s scooter state by reading scooterMounted. If it has a nonzero value, the scooter is active and MovePlayerScooter should be called to invoke the alternate, slightly simpler player movement logic.

The player can sometimes become temporarily “dizzy” by exiting a pipe system or falling from a great distance. This condition can be detected by either the queuePlayerDizzy flag being true or the playerDizzyLeft counter having a nonzero value. In either case, the ProcessPlayerDizzy function is called to do the per-frame bookkeeping for this state.


The combination of MovePlatforms and MoveFountains calls take care of moving map elements that the player can stand and walk on. These movements need to happen relatively early because the player’s position can be adjusted if they happen to be standing on top of one of these during this frame.

        if (DrawPlayerHelper()) continue;

This is the first point during a regular iteration of the game loop where something is drawn to the screen. DrawMapRegion draws the entire map visible map area – solid tiles, masked tiles, and the backdrop behind empty areas – over the game window based on the current X and Y scroll position. This fully erases everything that was left over in the video memory on the draw page. Platforms are regular solid tiles in the map memory that move around, so they are drawn as part of this function. Fountains contain no visible map tiles, so they are hidden at this point. (The player can still stand on these invisible tiles, however.)

The DrawPlayerHelper ultimately draws the player sprite onto the screen, but also does some per-frame checks to handle the player’s response to being hurt. Most notably, if the player dies or falls off the map, DrawPlayerHelper reloads the current level and returns true to request a game loop restart (continue). Anything drawn after this point can cover the player sprite, including all actors and visual effects.

DrawFountains draws the necessary sprite tiles onto the screen to show any fountains within the scrolling game window, even if the player is not currently interacting with any.


This is the “everything else” section of object movement and drawing. Some objects do not move (e.g. explosions and lights) but all are drawn at this time, in this order:

  • MoveAndDrawActors updates the position of every “awake” actor on the map, including ones that may be off a screen edge. Sprites are drawn for those visible within the scrolling game window.
  • MoveAndDrawShards updates the positions and draws all the sprites belonging to any shards that are visible. Those that are fully off the screen are stopped and removed.
  • MoveAndDrawSpawners updates and draws all spawning objects, and creates new actor objects once each spawner’s lifecycle is complete.
  • DrawRandomEffects draws random sparkles on slippery areas of the map, and spawns raindrops if the map requires them.
  • DrawExplosions animates a successive frame of each active explosion visible on the screen and draws the corresponding sprite.
  • MoveAndDrawDecorations handles the movement, animation, and looping properties of every decoration visible inside the game window. The sprite for each is drawn in the process. Decorations are stopped and removed once their loop count runs out or they leave the visible screen area.
  • DrawLights handles the special map actors that represent lighted areas of the map. Such lights shine “down” the screen and continue until they hit a solid map tile (or they reach a hard-coded maximum distance). Anything currently drawn on the screen at this point can be “lightened” if one of these light beams touches it.
        if (demoState != DEMOSTATE_NONE) {
            DrawSprite(SPR_DEMO_OVERLAY, 0, 18, 4, DRAWMODE_ABSOLUTE);

If a demo is being recorded or played back, demoState will have a value other than DEMOSTATE_NONE. In this case, DrawSprite() draws frame zero of the SPR_DEMO_OVERLAY “DEMO” sprite on the screen. This call uses the screen-relative DRAWMODE_ABSOLUTE, so the coordinates (18, 4) represent a fixed position near the center-top of the game window. This is the last thing drawn during a typical iteration of the game loop, and nothing should subsequently cover it.

        activePage = !activePage;

This is the page-flipping. Up until this point, all of the drawing had been directed at the “draw” page, which is the opposite of the “active” page that is currently being shown on the screen. SelectDrawPage() is called with activePage as the argument, informing the drawing functions that they should subsequently draw to the area of video memory that is currently on the screen.

Immediately after that, activePage is inverted to select the page that is not targeted by the drawing functions. This value is then passed to SelectActivePage(), which switches the content on the screen to the opposite page. This immediately makes visible everything that had been drawn in this iteration of the game loop, while also giving the next iteration of the game loop an area of video memory where it can perform its drawing work without it being visible on the screen.

        if (pounceHintState == POUNCE_HINT_QUEUED) {
            pounceHintState = POUNCE_HINT_SEEN;

A conceivably late addition to the game loop is this pounce hint check. If the value in pounceHintState is equal to POUNCE_HINT_QUEUED, something occurred during the current iteration of the game loop to warrant displaying the pounce hint dialog. Update pounceHintState to POUNCE_HINT_SEEN so this cannot recur, then call ShowPounceHint() to show the dialog.

Queue you!

The queuing implementation used here is probably not necessary; the behavior of the health hints in TouchPlayer demonstrates a more direct way to do this. The placement here suggests that maybe an older version of this function interacted badly with the page-flipping, perhaps appearing and pausing gameplay with some of the actors not yet drawn for the current frame.


The ProcessGameInputHelper() function is a small wrapper around the ProcessGameInput function that prepares the video hardware for the possibility of showing a menu or dialog during the game loop. page should contain the “active” (i.e. currently displayed) video page number, and demo should hold one of the DEMOSTATE_* values.

byte ProcessGameInputHelper(word page, byte demo)
    byte result;



    result = ProcessGameInput(demo);


    return result;

This function is effectively a wrapper around ProcessGameInput, passing the demo value directly through and returning its result without modification. This function exists to prepare the video hardware for certain drawing operations that ProcessGameInput may need to do.

EGA_MODE_LATCHED_WRITE() puts the EGA hardware into “latched” write mode. This is the mode used when drawing solid tile images, since these are stored in the EGA’s onboard memory and need to use the latches to copy data onto a video page.

Work harder, not smarter.

The call to EGA_MODE_LATCHED_WRITE() is arguably not needed, since all of the dialog/menu functions that can be invoked during the game ultimately call DrawTextFrame() which sets the EGA mode anyway. It could also be argued that, since most of the frames drawn by the game loop never show a menu or dialog in the first place, performing this call on every loop iteration is simply a waste of clock cycles.

The call to SelectDrawPage() sets the draw page – that is, the area of video memory that is visible on the screen right now – to page. This sets it up so that any subsequent drawing operation goes directly and immediately to the screen, visible to the user without page flipping coming into play.

In this state, anything drawn inside ProcessGameInput and the functions it calls are immediately visible.

Once the input handling is done, SelectDrawPage() is called again with the inverse of page as the argument. This resets the draw page back to the way it was when the function was first entered, and leaves the hardware in the “draw everything in a hidden buffer” mode it expects.

Finally, the original return value of ProcessGameInput, stashed in result, is returned to the caller.