Breakout, the game the course builds in HiSoft C, running on the Tatung Einstein: coloured brick rows, the bat and the ball on a black court
← Back to Courses
module
26

How Fast, How Big

Introduction

Before the game is written, two numbers have to be known: how much can happen in one tick, and how much program there is room for. Both are measurable, and both have been measured in the sections before this one; this section puts them together, because they decide how Breakout is built - what it draws each tick, how it waits, and why it does not bother to save bytes.

What A Tick Costs

The listing below times three things by printing a marker before and after each. Watched with a stopwatch, or with the emulator's clock:

Fifty ofTookEach
a thousand passes of an empty loop5.0 s100 ms per thousand
a wait for the next frame1.0 s20 ms
ten kbd() calls0.4 s0.8 ms per call

And from S24: a character printed, positioned or not, 4 ms; a printf of a few characters, 18 ms.

So a tick that rubs out the ball, draws it, redraws the bat if it moved, polls the keyboard and prints a score is about 20 to 30 milliseconds of work. That is the budget, and it is comfortable: the game could tick thirty times a second doing all of that, and it will tick more slowly than that because the ball would be unplayable otherwise.

Where the time goes is worth knowing. Almost none of it is C: ten thousand loop passes a second means a hundred microseconds a pass, and a pass is a compare, an increment and a jump. All of it is the screen. MOS draws each character pixel by pixel into the video chip's memory, through a port, and four milliseconds is what that costs - the same four whether the character follows the last one or was placed with curat, which is two pokes and free. A game on this machine is a program that spends its time waiting for the screen, and the design rule is S24's: draw what changed.

Three Ways To Wait

A game that runs flat out is too fast, and it has to wait for the next tick somehow. There are three ways:

An empty loop. for (i = 0; i < 1000; i++) ; takes a tenth of a second. Simple, and the game can tune its speed by changing the number. Its fault is that the number means something different on every machine - and in the emulator, which runs the Einstein as fast as your computer allows unless you tell it not to, it means almost nothing.

The frame flag. while ((inp(9) & 0x80) == 0) ; waits for the video chip to finish drawing a frame, which it does fifty times a second whatever else is happening. Fifty waits took a second, exactly. A game that waits for a frame at the end of each tick runs at fifty ticks a second, or twenty-five, or whatever fraction it chooses, on a real Einstein and in an emulator alike. This is the way, and it is the way the assembly course's game does it too.

Waiting for a key. rawin() stops everything until the player does something - right between levels and at the end, wrong in the middle of a game.

Breakout waits for frames: three frames a tick to begin with, which is sixteen or seventeen ticks a second, and fewer frames as the game speeds up (S32).

How Big

S6 said what a program costs on the disc: 4,352 bytes of runtime, your code, and XtalDOS's 2K rounding - 6K for anything small. What has changed since is that you have seen the big ones: S22's KEYS.C, with the Einstein library's kbd and a gets, is Code 1200 1465, 613 bytes of code. The whole of Breakout will be a few kilobytes. The runtime will be most of the .COM to the end.

Memory at run time is S21's: everything between the end of the code and the globals at the top, fifty thousand bytes or so, for the stack and the heap. Breakout's bricks are a char bricks[4][18] - seventy-two bytes - and its ball and bat are eight and four. Nothing about this game is going to run out, and nothing in this course has had to be made smaller to fit. That is what 64K is for a game of this size, and it is why the course has never asked you to save a byte: on this machine, with this compiler, a game's problem is time, not space.

Two habits are worth keeping anyway, because the day comes. A local array is allotted every call and freed on return, and a global one is allotted once; a big table wants to be global. And the manual says register is accepted and ignored, so nothing you write can make the compiler use the Z80's registers better than it does - the only speed that is yours to find is in doing less.

The Code

#include STDIO.H

main()
{
    int i, j;
    cls40();
    printf("delay-start\n");
    for (j = 0; j < 50; j++)
        for (i = 0; i < 1000; i++)
            ;
    printf("delay-end\n");
    printf("frame-start\n");
    for (j = 0; j < 50; j++)
        while ((inp(9) & 0x80) == 0)
            ;
    printf("frame-end\n");
    printf("kbd-start\n");
    for (j = 0; j < 500; j++)
        kbd();
    printf("kbd-end\n");
}

#include ?EIN.LIB?
#include ?STDIO.LIB?

Starting from

Typed as TICK.C; compiled and run, with a watch.

What you should see

delay-start
delay-end
frame-start
frame-end
kbd-start
kbd-end

with about five seconds between the first pair, one second between the second, and under half a second between the third. In MAME the gaps are what MAME's throttle makes them; at the default speed they are the Einstein's.

Change One Thing

  • Change the inner loop's 1000 to 10000. Predict the gap before you run it.
  • Change 50 frames to 250. How long, and what does that say about the frame rate?
  • Put curat(0, 10); putchar('*'); inside the frame loop, after the while. Does the gap change?

Exercises

26.1 Time 1,000 putchar('*') calls that do not scroll - print them on one row with putchar(13) after every forty - and compare with S24's four milliseconds.

26.2 Write wait_frames(n) that waits for n frames, and use it to print the numbers 1 to 10 one second apart.

26.3 Look at the Code line of the map for TICK.C, KEYS.C and SCREEN.C, and put the three programs in order of size. Which line of which program do you think cost most?

Worked solutions are in Appendix II.

When It Goes Wrong

SymptomCause
The game runs at a different speed in MAME and on the EinsteinIt waits with an empty loop. Wait for frames.
A frame wait never returnsThe & 0x80 is missing, or it tests == 1 instead of != 0. The flag is the top bit.
The game is slow however small the waitIt redraws too much each tick. Count the curats.
The map's Fixed Data starts lowA big global array. S21.

Summary

Ten thousand loop passes a second; a positioned character in 4 ms; a kbd() in under a millisecond; a frame every 20 ms. Wait for frames, not loops. A small program is 6K on the disc and leaves 50K free; this game will not run out of anything but time.

Next

S27, The Court - the game begins: the walls, the bat, and a BREAKOUT.H to keep the numbers in.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.