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 */
}
ti_steps_begin(ti_audio_ticks, TI_AUDIO_TICKS_PER_SECOND, 30, 30)gives 30 updates a second from the nominal 100 Hz clock. The remainder of a step carries over. Thus an update is due every 3 or 4 ticks, and the average is exactly 30 a second.- The last argument sets the maximum number of steps for one call, here 30. libtini discards time above this limit. After a very slow frame, the game slows down and does not jump ahead.
- Use a smaller limit, for example 4, if one large catch-up is unfair to the player.
- The loop reads the controls once for each drawn frame. All the steps of that frame use the same controls.
- The loop draws only after at least one step. Thus a fast frame does not draw an unchanged screen again.
Pauses and gaps
Two conditions tell the game that the clock is not valid for the moment:
ti_steps_duereturnsTI_STEPS_GAPwhen more than one second passes between two calls.ti_audio_clock_validreturns 0 when the audio driver finds a gap. A gap is more than one second between updates, or more than 32 score events or song rows due at the same time.
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:
- A sprite scene calls the
servicefunction of its configuration after every fourth band of 8 lines. Setserviceto a function that callsti_audio_update. - Between drawing calls, call
ti_audio_updatein your code. Thepresentfunction of STARFALL calls it after each publication. Its large title logo calls it after each letter. - A call in the same tick as the last call does almost no work. Thus more calls cost little time.
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.