Game loop and timing

Run the simulation at a fixed rate from the audio device clock. Draw only when an update is due. Keep the music serviced during long work. Pause when the clock is not valid.

The shape of a loop

The game owns its loop. libtini supplies a clock that has the same rate at 33 MHz and in TURBO. libtini also converts that clock into a number of simulation steps. The simulation advances in fixed steps, and the game draws as often as the machine can.

#include "libtini.h"

/* The game's own parts. */
ti_error start_screen(void);
void pause_game(void);
uint8_t read_controls(void);
void update_game(uint8_t controls);
void draw_changes(void);

/* Mockingboard, no speech, nominal clock, one AY chip of three voices. */
static ti_audio_config audio = {TI_AUDIO_MOCKINGBOARD, 0, 0, 0, 3};

int main(void)
{
    uint8_t steps, key, controls;
    if (ti_init(1, 3)) return 1;
    if (start_screen()) return 1;          /* surface, ti_gfx_begin, first frame */
    if (ti_audio_init(&audio)) return 1;  /* the clock comes from the card */
    ti_steps_begin(ti_audio_ticks, TI_AUDIO_TICKS_PER_SECOND, 30, 30);
    for (;;) {
        ti_audio_update();                 /* music, effects and speech */
        ti_input_poll();
        if (ti_input_key(&key) == TI_OK && (key == 'Q' || key == 'q')) break;
        if (!ti_audio_clock_valid()) { pause_game(); continue; }
        steps = ti_steps_due();
        if (steps == TI_STEPS_GAP) { pause_game(); continue; }
        if (!steps) continue;              /* no update due: no draw */
        controls = read_controls();        /* ti_input_held, ti_stick_read */
        while (steps--) update_game(controls);
        draw_changes();                    /* scene and HUD; see Drawing */
        if (ti_error_first()) break;
    }
    return 0;                              /* cleanup runs on return */
}

Pauses and gaps

Two conditions tell the game that the clock is not valid for the moment:

Then pause the game, as STARFALL does. Do not simulate a second that the game did not see.

void pause_game(void)
{
    uint8_t key;
    ti_music_stop();
    show_pause_panel();
    do ti_input_poll(); while (ti_input_key(&key) != TI_OK);
    ti_audio_resume_clock();   /* arm the clock again after a gap; no effect otherwise */
    ti_steps_reset();          /* the pause is not game time */
    redraw_screen();
    start_music();
}

ti_steps_due keeps only the low 16 bits of the clock. A pause of 65,536 ticks or more is approximately eleven minutes. End such a pause with ti_steps_reset, as the example shows.

Keep the music serviced

ti_audio_update plays the score events and song rows that are due since its last call. It writes the AY registers one time for each tick. It can catch up a maximum of 32 events or rows. If more are due, or if more than 100 ticks pass between calls, the music stops and the call returns TI_E_TIMING.

Call ti_audio_update at least one time for each tick, which is every 10 ms. Long work needs more calls:

static void service(void) { ti_audio_update(); }
static const ti_scene_config scene = {0, 16, 240, 184, images, IMAGE_COUNT, 0, service};

Long operations

File calls and AMEM requests pause the audio driver automatically. The clock stops, effects and speech stop, and the music becomes silent. After the operation, the driver starts again, and the pause does not count as game time.

Other work can block, or keep interrupts masked for more than one tick. Put the same calls around this work:

ti_audio_mark_gap();        /* silence, stop the clock */
long_blocking_work();
ti_audio_resume_clock();    /* set a new base; the gap is not elapsed time */
ti_steps_reset();

Animation by ticks

Use elapsed device ticks or simulation steps for animation. Do not use the number of drawn frames. Then animations keep their speed when the drawing slows down.

ti_gfx_animation_frame calculates the frame of a frame sequence. Palette animation needs only a new palette from the step counter and one ti_gfx_palette call for each frame. STARFALL uses palette animation for its bullets and shields.

Errors one time for each frame

Put ti_error_keep around drawing calls and other calls that must stop the game when they fail. Test ti_error_first one time for each frame.

Do not keep the result of ti_audio_update. It reports gaps and speech timeouts. The game pauses for a gap. For a speech timeout, the game continues without speech.

Display edges are not a clock

ti_time_poll counts only the vertical blank edges that it sees. The count is a lower limit, and it stops when the game is busy. Thus it cannot time a game.

libtini has no call that waits for vertical blank. A publication is complete when its CPU writes are complete. The Appletini shows the screen at its own frame boundary.

Measure the time

tools/profiler.py measures named functions of a built game on the emulator. It gives the time for each call path, at both CPU models. Use it to find where the time of a frame goes, and then make the code faster. See Tools.