Chapter 9 - Shapes, Sound and Displays on the Board
Every picture in the book so far has been built from single calls to
FbPlot: a dot, a drop, a bar of pixels in a loop. A game’s character
deserves a body - two pixels by two, or a whole 8x8 figure - and
plotting it pixel by pixel inside every render buries the picture in
code. The picture belongs in data, drawn where you can see its shape.
A game also speaks through more than the matrix. When the character hits a wall, the player should hear it. When the score changes, the player should be able to read it between moves. The TEC-1G board carries an instrument for each job: a speaker, the six-digit seven-segment display, and a four-row LCD, all sitting beside the matrix and all reachable from a block.
This chapter teaches one pattern four times over. You declare a
resource in the .glim file - a shape, a sound cue, a text string -
and Glimmer generates the data plus something callable to go with it.
Your blocks call what was generated, and the declaration reads like
the resource it describes.
Fanfare
The chapter’s program is a small celebration. A cyan spark, two pixels square, bounces around the matrix on its own timer. Every wall hit reverses its direction, beeps the speaker, and adds one to a score on the seven-segment display. The LCD announces the program from the first frame.
program Fanfare
platform tec1g-mon3
display matrix8x8
shape Spark color cyan
"XX"
"XX"
end
sound Bounce len 8 div 3
text MsgHello "FANFARE"
state SparkX : byte = 3 changed
state SparkY : byte = 2
state VelX : byte = 1
state VelY : byte = 1
state Score : word = 0 changed
state Banner : byte = 0 changed
pulse Tick
timer Step : byte = 6 -> Tick
effect Move
on Tick
updates SparkX, VelX, SparkY, VelY, Score
begin
; step x, bounce off columns 0 and 6
ld a,(VelX)
ld b,a
ld a,(SparkX)
add a,b
ld (SparkX),a
or a
jr z,_hitx ; left wall
cp 6
jr nz,_xdone ; open board: no bounce
_hitx:
ld a,(VelX)
neg
ld (VelX),a
ld hl,(Score)
inc hl
ld (Score),hl
call Snd_Bounce
_xdone:
; step y, bounce off rows 0 and 6
ld a,(VelY)
ld b,a
ld a,(SparkY)
add a,b
ld (SparkY),a
or a
jr z,_hity ; top wall
cp 6
jr nz,_ydone
_hity:
ld a,(VelY)
neg
ld (VelY),a
ld hl,(Score)
inc hl
ld (Score),hl
call Snd_Bounce
_ydone:
end
render DrawSpark
on SparkX, SparkY
begin
call FbClear
ld a,(SparkX)
ld b,a ; B = x
ld a,(SparkY)
ld c,a ; C = y
ld hl,Shape_Spark
call ShapeDraw
end
render ShowScore
on Score
begin
ld hl,(Score)
call HudWriteU16
end
render Greet
on Banner
begin
lcd_row MsgHello, LcdRow1
end
Three declarations at the top are new: shape, sound, and text.
The rest is chapter 7’s machinery - a timer fires Tick every 6
frames, Move runs on Tick - carrying a new idea in its state:
velocity as a fact. VelX holds 1 travelling right or $FF
travelling left, and adding $FF to a byte steps it down by one, so
one add moves the spark whichever way it is going. After the step,
a spark at column 0 or column 6 has an edge against a wall (the shape
is 2 wide, so 6 is as far right as it fits), and the rule answers
with three moves: negate the velocity, bump the score word, start the
sound cue. Then the same again for y.
Build it and let it run. The spark ricochets, each hit chirps, and the digits climb - twice in quick succession when it rounds a corner.
A shape is pixel art with a name
shape Spark color cyan
"XX"
"XX"
end
A shape declares a bitmap you can read at a glance: quoted rows of
X for a lit pixel and . for an empty one. Rows are rectangular,
from 1x1 up to 8x8, and the colour is one of the matrix’s seven:
red, green, blue, yellow, cyan, magenta, white. A
tall cross, three wide and four high, would read:
shape Cross color red
".X."
"XXX"
".X."
".X."
end
From each shape Glimmer emits a data table named Shape_<Name>, and
because at least one shape exists, the profile library gains
ShapeDraw, the routine that paints any of them. Its interface is
three registers:
ld hl,Shape_Spark
ld b,3 ; x
ld c,2 ; y
call ShapeDraw
HL picks the shape, B and C place its top-left corner. ShapeDraw
ORs each lit pixel’s colour bits into the framebuffer, so lit pixels
land on top of whatever is there and empty pixels leave it alone -
two overlapping shapes combine. DrawSpark starts with FbClear for
the same reason as every moving picture since chapter 1: a moving
shape redraws from a clean board.
Placement is the caller’s whole responsibility: ShapeDraw plots
every lit pixel at x plus column, y plus row, straight into the
framebuffer, and a row that hangs off the board writes into whatever
memory follows it. Keep the whole shape inside the 8x8 matrix -
for the 2x2 spark that means x and y each stay in 0..6, which is
exactly the range Move enforces with its bounce tests. Register
hygiene matters here too: the generated contract line declares that
ShapeDraw clobbers A, BC, DE, and HL, so load its arguments last,
as DrawSpark does.
Sound that keeps out of the way
sound Bounce len 8 div 3
A sound declares a cue: a short, non-blocking beep served by the
matrix scan itself. The scan loop visits the speaker once per row, 8
ticks per frame, and len counts those ticks - len 8 sounds for
about one frame. div sets the pitch as a divider: smaller values
are higher. Cues of this kind are the vocabulary of a board game’s
feedback - clicks, chirps, buzzes - with a long low len 200 div 9
about as mournful as the speaker gets.
Each cue compiles to a routine named Snd_<Name>, and calling it is
the entire interface:
call Snd_Bounce
The call starts the cue and returns at once; the scan plays it out over the following frames while your blocks keep running. One cue is active at a time, and starting a new cue replaces the current one - a fresh wall hit restarts the chirp from the top, which is exactly the feedback a fresh hit deserves.
Where the call sits is the lesson of Move. Sound accompanies an
event, and the event lives inside a rule, behind a conditional - so
the call Snd_Bounce sits inside the effect, on the branch where the
wall hit happened. The quiet path steps past it. Feedback is one line
in the rule that knows.
The score, on the seven-segment display
The six-digit seven-segment display is the board’s number instrument, and the same scan that serves the speaker serves it: one digit per row tick, refreshed forever. The profile library drives it with two routines:
HudWriteU16- HL = value, shown as five decimal digits, 0 to 65535.HudBlankDig- clear all six digits.
Score is a word, so ShowScore loads all sixteen bits and hands
them over:
render ShowScore
on Score
begin
ld hl,(Score)
call HudWriteU16
end
The startup code Glimmer generates calls FbClear and HudBlankDig
before the first frame, so both displays begin dark; Score is
declared changed, so ShowScore runs on frame one and the score
opens at zero rather than blank.
Move’s header has one consequence to trace. updates lists every
fact the block may change, and each listed fact is marked changed
whenever the block runs - so ShowScore repaints its digits every
step, quiet ticks included. The repaint writes the same six
glyph bytes and costs a few dozen cycles in the blank window. When a
score changes rarely and its redraw is heavy, split the rule; when
the redraw is HudWriteU16, the broad updates reads better.
Words on the LCD
text MsgHello "FANFARE"
A text declares a zero-terminated string for the TEC-1G’s four-row
LCD. The LCD is board hardware, alongside the keypad rather than part
of any display profile, so text resources work the same on the matrix
and, later in the book, on the TMS9918. Writing a string to a row is
one line in a block:
lcd_row MsgHello, LcdRow1
lcd_row is an AZM op: a named instruction sequence that the
assembler owns, defined once in the generated file and expanded
inline wherever it is invoked. You write it like an instruction -
name, then arguments - and the assembler replaces it with its body,
with the arguments substituted in. Glimmer emits the lcd_row op
whenever a program declares text, and you can define ops of your own
in hand-written AZM modules, which chapter 12 reaches. Here it
packages the two MON-3 calls that position the LCD cursor and
stream a string, taking the message label and a row constant:
LcdRow1 through LcdRow4 come with it.
Greet shows the run-once pattern that puts a title on screen.
Banner starts changed and appears in no block’s updates, so it
changes exactly once, before the first frame - Greet runs on frame
one, writes FANFARE to the top row, and rests for the rest of the
program’s life. A fact that starts changed and never changes again is
a startup hook, declared in one line of state.
The file, resource by resource
Each resource declaration left its mark on the generated file. Open
fanfare.main.asm and find every declaration’s other half.
The text resource is its bytes, terminator included:
; --- text resources (zero-terminated LCD strings) ---
MsgHello:
.db "FANFARE", 0
The shape is a five-byte table: a header of width, height, and colour, then one mask byte per row, lit pixels packed from bit 7:
; --- shape resources ---
; Table format: width, height, colour, then left-aligned row masks.
Shape_Spark:
.db 2, 2, COLOR_CYAN
.db %11000000
.db %11000000
ShapeDraw walks exactly this: read the header, then for each row
shift the mask left and FbPlot every set bit at its offset from B
and C. The declaration you drew in Xs and the table the routine
consumes are the same picture at two zoom levels.
The sound cue is a three-instruction wrapper:
; --- sound cues ---
; Non-blocking matrix-profile cues. len is row ticks; div is the
; speaker divider. Starting a cue replaces the currently active cue.
.routine
Snd_Bounce:
ld a,8
ld c,3
jp SndStart
Your len and div became the two loads, and SndStart is the
library routine that arms the scan’s speaker service - A carrying
the duration in ticks, C the divider. Declaring a second cue would
add a second wrapper over the same SndStart.
And the op, defined once near the end of the file:
; Position the LCD cursor at a row command, then write a string.
op lcd_row(msg imm16, row imm8)
ld b,row
ld c,ApiCommandToLcd
rst $10
ld hl,msg
ld c,ApiStringToLcd
rst $10
end
Two parameters, typed by size: msg is a 16-bit immediate, row an
8-bit one. The body is two MON-3 calls through rst $10, with the
parameters standing where their values will go. At every invocation
the assembler drops these six instructions in place, so Greet’s
block body lands in the file with the invocation still readable:
; --- render block Greet ---
.routine
Glim_Greet:
lcd_row MsgHello, LcdRow1
ret
Your one line, verbatim, and the assembler finishes the job from the definition above.
Summary
- A
shapeis a named bitmap: quoted rows ofXand., 1x1 up to 8x8, in one of seven colours. Glimmer emits aShape_<Name>table and theShapeDrawroutine: HL = shape, B = x, C = y. ShapeDrawORs lit pixels into the framebuffer, overlaps combine, and placement that keeps the whole shape on the board is the caller’s job. It clobbers A, BC, DE, and HL.- A
soundcue compiles toSnd_<Name>; one call starts it and the matrix scan plays it out.lencounts row ticks (8 per frame),divsets pitch (smaller is higher), and a new cue replaces the active one. Call cues inside rules, on the branch where the event happened. - The seven-segment HUD shows a value with
HudWriteU16(HL = value, five decimal digits) and clears withHudBlankDig. - A
textis a zero-terminated LCD string;lcd_row Msg, LcdRowNwrites it to a row. An AZM op is an assembler-owned macro: Glimmer emits the definition, you invoke it like an instruction, and the assembler expands it inline. - A state cell that starts
changedand appears in noupdatesis a run-once startup hook.
Fanfare’s whole world is one spark and one number. Next, the board itself becomes data: arrays and layout types, for games whose state is many related bytes.