👾Mehrere Sprites – flimmerfrei und steuerbar

Eine kleine Spielszene: zehn Figuren in vier Größen fliegen über den Hintergrund, prallen an den Rändern ab und überlappen sich. Alles läuft als Z80-Programm im Emulator – und die gemessene Rechenzeit zeigt ehrlich, wann ein 50-Hz-Bild nicht mehr reicht.

🧪
Simulation im Browser. Der Z80 wird Befehl für Befehl emuliert; die CPC-Firmware ist dagegen in TypeScript nachgebildet (HLE) – nach den Ein-/Ausgabebedingungen aus dem Firmware-Handbuch SOFT 968. Es wird kein ROM-Inhalt von Amstrad verwendet, der Zeichensatz ist ein gemeinfreier Ersatz. Die Szene ist ein echtes Z80-Programm; Tastenabfrage und Rand-Farbe gehen über die (nachgebildete) Firmware.

🎮Die Szene im Emulator

Im Emulator gemessen: Rechenzeit je Bild (10 Sprites, Mittel über 20 Bilder)
messe …
bereit
ℹ️ Die Szene läuft endlos. Mit „Pause“ und „Schritt“ lässt sie sich wie auf den anderen Seiten anhalten; das Label work_end markiert das Ende der Arbeit eines Bildes. Hinweis: Die Bildschirmanzeige springt zwischen &4000 und &C000 – im Speicher-Fenster lassen sich beide Bilder ansehen.
Quelltext (editierbar)
Assembler-Liste · Klick auf ● = Haltepunkt
Bilder
0
Bilder/s
…
Emulatorzeit
Berührungen
0
Objekt 0 frei
0
1
2
3
Rand
Modus 1 · Basis &C000 · Versatz &0000
Firmware
Firmware jetzt
▶ RESET ENTRY · Details in VisualZ80 ↗
Letzte Falle: –
Letzte Firmware-Aufrufe
noch keine
Register & Flags
AF
&FFFF
BC
&0000
DE
&0000
HL
&0000
IX
&FFFF
IY
&FFFF
SP
&C000
PC
&0000
S
1
Z
1
Y
1
H
1
X
1
P/V
1
N
1
C
1
AF' &0000
BC' &7F8D
DE' &0000
HL' &0000
I &00
R &00
IM 1
IFF1/2 1/1
Z80-Takte: 0 TCPC-Zeit: 0 µsROMs: unten aus, oben ausRAM-Konfig: &C0
Disassembler ab PC
0000HLE ▸ RESET ENTRY
0002RET
0003NOP
0004NOP
0005NOP
0006NOP
0007NOP
0008HLE ▸ LOW JUMP
000ARET
000BHLE ▸ KL LOW PCHL
Stapel (SP)
C0000000← SP
C0020000
C0040000
C0060000
C0080000
C00A0000
C00C0000
C00E0000
Speicher (Hex)
800000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ················
801000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ················
802000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ················
803000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ················
804000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ················
805000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ················
806000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ················
807000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ················
PCSP(HL)(DE)Leseansicht mit aktueller ROM-/Bank-Einblendung

🔁Die Hauptschleife

Ein Durchlauf erzeugt ein Bild. Im flimmerfreien Modus wird immer in das Bild gezeichnet, das der Monitor gerade NICHT zeigt.
          ┌──────────────────────────────────────────────┐
          │ frame:                                       │
          ▼                                              │
  1. Hintergrund zurück  (Liste des unsichtbaren Bildes, │
     letzter Eintrag zuerst)                             │
          ▼                                              │
  2. Tasten lesen (KM TEST KEY) · bewegen · abprallen    │
          ▼                                              │
  3. Kollision Objekt 0 ↔ andere → Rand rot, Zähler      │
          ▼                                              │
  4. nach y sortieren (Maleralgorithmus)                 │
          ▼                                              │
  5. je Sprite: Hintergrund sichern, maskiert zeichnen   │
          ▼   ── work_end ──                             │
  6. warten: VSYNC-Ende, dann VSYNC-Beginn (PPI Port B)  │
          ▼                                              │
  7. CRTC R12 umschalten (&10 ↔ &30), Listen tauschen ───┘
; ================= Hauptschleife: ein Durchlauf = ein Bild =================
frame:
        call restore_all      ; 1. Hintergrund zurück (umgekehrte Zeichenreihenfolge)
        ; (kein Spieler – alle Sprites bewegen sich selbst)
        call move_all         ; 2. Positionen: x += dx, y += dy, an den Rändern abprallen
        call collide          ; 3. Kollision Objekt 0 ↔ andere (Rechtecke)
        call sort_y           ; 4. nach y sortieren (Maleralgorithmus)
        call draw_all         ; 5. Hintergrund sichern + maskiert zeichnen
work_end:
        call wait_frame       ; 6. auf den Beginn des nächsten Strahlrücklaufs warten
        call flip             ; 7. unsichtbares Bild zeigen (CRTC R12), Puffer tauschen
        ld hl,(frames)
        inc hl
        ld (frames),hl        ; Bildzähler
        jp frame

🖌️Zeichenreihenfolge: der Maleralgorithmus

Wo sich Sprites überlappen, gewinnt das zuletzt gezeichnete – wie beim Malen auf Leinwand.
💡 Nach y sortieren
Die Figuren werden vor dem Zeichnen nach ihrer y-Position sortiert: weiter oben zuerst, weiter unten zuletzt. Weiter unten wirkt „näher“ – so entsteht ohne Z-Puffer eine glaubwürdige Überdeckung. Die Liste ist vom letzten Bild fast sortiert, deshalb reicht ein Bubble Sort, der meist nach einem Durchgang fertig ist.
⚠️ Rückwärts wiederherstellen
Jede Figur sichert beim Zeichnen den Hintergrund – der kann schon eine andere Figur enthalten. Deshalb wird in umgekehrter Reihenfolge wiederhergestellt: das zuletzt gezeichnete Sprite zuerst. Sonst bleiben Reste stehen.
✅ Zwei Listen bei zwei Bildern
Mit Double Buffering hat jedes Bild seine eigene Liste (Adressen, Typen) und eigene Hintergrundpuffer – ein Bild wird ja erst zwei Durchläufe später wieder bearbeitet.

⏱️Warum es ohne Double Buffering flackert

Ohne zweites Bild fehlt jedes Sprite eine Weile: vom Wiederherstellen des Hintergrunds bis zum Neuzeichnen. Trifft der Strahl genau diese Lücke, sieht man es.
VSYNC 1VSYNC 2VSYNC 3Pixelgeist y=8Münze y=26Käfer y=44Rakete y=55⚠ Käfer y=73⚠ Pixelgeist y=91⚠ Münze y=102⚠ Rakete y=120⚠ Käfer y=149⚠ Münze y=167
Strahl gibt die Zeilen dieses Sprites ausSprite fehlt oder ist halb gezeichnet (wiederhergestellt, noch nicht neu gezeichnet)… und der Strahl liest genau dann → sichtbares Flackern/Zerreißen
Ohne Double Buffering: 6 von 10 Sprites werden in diesem Durchlauf gelesen, während sie fehlen oder halb fertig sind.
Mit Double Buffering: 0 – gezeichnet wird ausschließlich im unsichtbaren Bild, umgeschaltet wird im Strahlrücklauf.
Berechnet aus den im Emulator gemessenen Einzelkosten je Typ (wiederherstellen / sichern / zeichnen) und den Startpositionen; die Verwaltung (Bewegen, Sortieren, Kollision) ist hier weggelassen. Der Emulator selbst zeichnet sein Bild einmal je Browser-Bild vollständig – das Zerreißen ist dort nicht zu sehen, deshalb diese Zeitachse.
vollständig
zerrissen
Simulation: oben schon 2 Pixel weiter, unten noch an der alten Stelle – so sieht ein Sprite aus, das der Strahl mitten im Umzeichnen erwischt.

📊Das Zeitbudget: wie viele Sprites passen in 19 968 µs?

SpritesRechenzeit je Bild (µs)AnteilBilder/sBudget

messe … (0/16) Messung im Emulator: Mittel über 10 Bilder, Balken bis 3 Bildzeiten. Die Sprite-Mischung ist fest (Geist, Rakete, Münze, Käfer …).

🚀Wie man mehr Sprites schafft

Pixelgeist (4 × 16 Bytes)Szene (µs)schneller (µs)wie
Hintergrund wiederherstellen1.178695Schleife mit DJNZ → LDI entrollt (Routine a)
Hintergrund sichern1.178695ebenso mit LDI
maskiert zeichnen1.562565Breite aus Variable → kompiliertes Sprite (Routine f)
Summe je Bild3.9181.9555 bzw. 10 Geister passen rechnerisch in 19 968 µs (ohne Verwaltung)

Alle Werte sind im Emulator gemessene Routinen (inkl. CALL/RET). Die Szene selbst verwendet bewusst die einfachen, allgemeinen Schleifen (Breite und Höhe aus Variablen), damit ein Programm alle Größen zeichnen kann – das kostet. Zum Vergleich die einfache Maskenroutine mit festen Maßen (Routine c): 1.511 µs.

✅ Schnellere Routinen
LDI statt Schleifen, kompilierte Sprites statt Masken-Schleife – die Tabelle oben zeigt die gemessene Ersparnis für einen Geist.
✅ Kleinere Sprites
Die Kosten wachsen mit Breite × Höhe: eine Münze (2 × 8 Bytes) kostet in der Szene nur rund ein Drittel eines Geists (4 × 16).
✅ Nur Geändertes restaurieren
Wer nur die Streifen wiederherstellt, die ein Sprite beim Bewegen wirklich freigibt, spart den Großteil der Restaurierung – bei 1 Byte Bewegung je Bild ist das nur eine schmale Spalte.
✅ Auf zwei Bilder verteilen
Die Hälfte der Figuren in geraden, die andere in ungeraden Bildern bewegen: die Arbeit je Bild halbiert sich, jede Figur bewegt sich dann mit 25 statt 50 Schritten je Sekunde.

⌨️Tastatur: vom Browser zur CPC-Tastennummer

Die Firmware-Routine KM TEST KEY (&BB1E) erwartet in A eine Tastennummer 0–79 und meldet mit dem Zero-Flag, ob die Taste gedrückt ist (SOFT 968). Achtung: in C liefert sie den Zustand von Shift und Control – C ist danach verändert.
TasteNummerMatrix
Cursor hoch0Zeile 0, Bit 0
Cursor rechts1Zeile 0, Bit 1
Cursor runter2Zeile 0, Bit 2
Cursor links8Zeile 1, Bit 0
P27Zeile 3, Bit 3
O34Zeile 4, Bit 2
Leertaste47Zeile 5, Bit 7
W59Zeile 7, Bit 3
S60Zeile 7, Bit 4
D61Zeile 7, Bit 5
Q67Zeile 8, Bit 3
A69Zeile 8, Bit 5
Joystick 0 hoch / runter / links / rechts72Zeile 9, Bits 0–3 → 72–75

Tastennummer = 8 × Matrixzeile + Bit. Matrix nach K. Thacker, „Reading the keyboard and Joysticks“ (cpctech); SOFT 968: gültige Tastennummern 0–79.

💡 Was die Firmware dafür tut
Die Tastatur ist eine Matrix aus 10 Zeilen × 8 Bits. Über Bits 3–0 von PPI-Port C wird eine Zeile gewählt; ihr Zustand liegt am Ein-/Ausgabeport A des Soundchips AY-3-8912 (PSG-Register 14) und wird über PPI-Port A gelesen – ein gedrücktes Bit ist 0. Die Firmware tastet die Matrix regelmäßig ab (normalerweise 50-mal je Sekunde) und legt das Ergebnis in einer Tabelle ab, die KM TEST KEY liest.
⚠️ Im Browser
Nur wenn der CPC-Bildschirm den Fokus hat, werden Tasten an den Emulator gegeben – dann (und nur dann) scrollen Pfeiltasten die Seite nicht. Jede Browser-Taste wird die gleichnamige CPC-Taste; bei QAOP ist A „runter“, bei WASD „links“ – deshalb die Auswahl der Belegung. Echte Tastatur-Geisterbilder („keyboard clash“) bei mehreren gedrückten Tasten bildet der Simulator nicht nach.