Im neuen Video meiner Assembler-Reihe geht es um einen Klassiker aus unzähligen C64-Spielen: Raumschiffe, die in einer Wellenformation über den Bildschirm fliegen. Der eigentliche Knackpunkt dabei ist nicht das Setzen der Sprites, sondern die Mathematik dahinter – und die Frage, wie man sie so umsetzt, dass ein Rechner mit knapp 1 MHz noch hinterherkommt.
In diesem Beitrag findet ihr die Erklärungen aus dem Video zum Nachlesen, ein paar Ergänzungen, für die im Video keine Zeit mehr war, und am Ende den vollständigen Quellcode aller Programme zum Herauskopieren.
Das Ziel
Ein Sprite bewegt sich in jedem Schritt um ein Pixel nach rechts. Seine Y-Position folgt dabei einer Sinuskurve:
Y = 140 + 50 * SIN(phase * PI / 128) mit phase = 2 * X
Mehrere Schiffe fliegen auf derselben Bahn, jeweils 40 Pixel versetzt. Wie diese Formel zustande kommt und was die einzelnen Zahlen bedeuten, zeigen die nächsten beiden Abschnitte.
Die Sinusfunktion – erklärt am Riesenrad
Stellt euch ein Riesenrad mit einer einzigen Gondel vor. Die Sinusfunktion beantwortet nur eine Frage: Wie hoch ist die Gondel gerade – gemessen von der Radachse aus? Bei einem Rad mit dem Radius 1 liegt die Antwort immer zwischen -1 (ganz unten) und +1 (ganz oben). Trägt man diese Höhe auf, während sich das Rad dreht, entsteht die Wellenlinie.
Wie weit sich das Rad gedreht hat, nennen wir die Phase. Normalerweise misst man eine Umdrehung in Grad (360°). Der C64 rechnet aber am liebsten mit Bytes – deshalb teilen wir eine volle Umdrehung in 256 Schritte auf:
- Phase 0: Start, Gondel auf Achsenhöhe (SIN = 0)
- Phase 64: Viertelumdrehung, Gondel ganz oben (SIN = +1)
- Phase 128: halbe Umdrehung, wieder auf Achsenhöhe (SIN = 0)
- Phase 192: Dreiviertelumdrehung, Gondel ganz unten (SIN = -1)
- Phase 256: volle Umdrehung – das Byte läuft über und steht wieder auf 0
Der Teil phase * PI / 128 in der Formel ist nur eine Umrechnung: Die SIN-Funktion möchte den Winkel im Bogenmaß haben, und dort entspricht eine volle Umdrehung 2 * PI. 256 Schritte mal PI/128 ergeben genau 2 * PI.
Hier die Animation dazu – die Gondel dreht sich pro Bild um einen Phasenschritt, genau wie das Schiff im Programm:
Von SIN zur Bildschirmposition
Im Video bin ich darauf nicht näher eingegangen, deshalb hier der Vollständigkeit halber: Die SIN-Funktion liefert nur Werte zwischen -1 und +1. Auf dem Bildschirm wäre das ein Zittern von zwei Pixeln. Deshalb wird das Ergebnis noch umgerechnet:
- mal 50 – aus -1 bis +1 wird -50 bis +50. Das Schiff schwingt 50 Pixel nach oben und nach unten (die Amplitude).
- plus 140 – aus -50 bis +50 wird 90 bis 190. Negative Sprite-Koordinaten gibt es nicht, und so liegt die Mitte der Welle bei Rasterzeile 140, ungefähr in der Bildschirmmitte.
- abrunden – Sprite-Positionen sind ganze Zahlen, die Nachkommastellen fallen weg.
Ein Beispiel mit Phase 100:
100 * PI / 128 = 2.454 (Winkel) SIN(2.454) = 0.634 (Hoehe der Gondel) * 50 = 31.7 + 140 = 171.7 -> Y = 171
Eine Kleinigkeit am Rande: Beim C64 zeigt die Y-Achse nach unten. Wo der Sinus positiv ist, fliegt das Schiff deshalb tiefer – die Kurve erscheint auf dem Bildschirm gespiegelt.
Ansatz 1: Live-Berechnung
Der naheliegende Weg: Der C64 bringt im BASIC-ROM eine komplette Fließkomma-Bibliothek mit, inklusive SIN. Warum also nicht einfach die benutzen? Die Routine calc_y macht genau das – Phase rein, Y-Position raus:
calc_y: tay ; lo = Phase lda #0 ; hi jsr GIVAYF ; FAC = Phase lda #<c_step ldy #>c_step jsr FMULT ; FAC = Phase * PI/128 jsr SIN ; FAC = SIN(FAC) lda #<c_amp ldy #>c_amp jsr FMULT ; FAC = FAC * 50 lda #<c_center ldy #>c_center jsr FADD ; FAC = FAC + 140 jsr GETADR ; Y = lo, A = hi (hier immer 0) tya ; Ergebnis nach A rts
Die ROM-Routinen arbeiten dabei wie die Tasten eines Taschenrechners: Alle rechnen auf demselben “Display”, dem Fließkomma-Akkumulator (FAC). GIVAYF tippt die Phase ein, FMULT, SIN und FADD rechnen weiter, GETADR liest das Ergebnis als Ganzzahl ab.
Warum überhaupt Fließkomma, wenn am Ende eine Ganzzahl herauskommt? Weil die Zwischenschritte keine Ganzzahlen sind – SIN liefert Werte wie 0.634. Und der 6502 kann weder multiplizieren noch mit Kommazahlen rechnen, er kennt nur Addition und Subtraktion.
Das Problem: Diese Rechnung ist sehr langsam. Schon für ein einziges Schiff braucht sie in etwa so lange wie der Aufbau eines ganzen Bildes, bei mehreren Schiffen deutlich länger. Die Animation wird entsprechend langsam und ruckelig.
Rasterzeit sichtbar machen
Um das zu messen, färbt das Programm vor der Berechnung den Rahmen rot und danach wieder schwarz. Der VIC baut das Bild Zeile für Zeile auf, 50-mal pro Sekunde, und übernimmt eine neue Rahmenfarbe sofort. Deshalb zeigt der rote Bereich direkt, wie lange die CPU gerechnet hat: Jede rote Rasterzeile entspricht 63 Taktzyklen. Ein ganzes Bild hat 312 Rasterzeilen – ist fast alles rot, braucht die Rechnung ein ganzes Bild oder mehr, und die Animation wird langsamer.
Ansatz 2: Lookup-Table, beim Start berechnet
Die Flugbahn ist immer dieselbe. Also rechnen wir sie nicht in jedem Schritt neu, sondern einmal beim Programmstart – mit genau derselben Routine calc_y – und legen die 256 Ergebnisse in einer Tabelle ab (blauer Rahmen beim Start). In der Hauptschleife wird dann nur noch nachgeschlagen:
ldx #0 ; fuer jedes Schiff Y nachschlagen lut_loop: ldy phase,x ; Phase als Tabellenindex lda SINTAB,y sta ypos,x inx cpx #NUM_SHIPS bne lut_loop
Ein Tabellenzugriff kostet nur wenige Taktzyklen. Selbst mit acht Schiffen braucht das Nachschlagen nur etwa 2 bis 3 Rasterzeilen – die Animation läuft mit vollen 50 Bildern pro Sekunde.
Die Tabelle im Detail
So sieht die Tabelle im Speicher aus. Jeder der 256 Einträge ist die fertige Sprite-Y-Position für einen Phasenschritt – also das Riesenrad, einmal komplett durchgedreht und bei jedem Schritt “fotografiert”, bereits umgerechnet in Bildschirmkoordinaten.
Korrektur zum Video: Dort habe ich gesagt, die 256 Werte seien zweimal die Sinuskurve. Das stimmt nicht – die 256 Werte sind genau eine vollständige Welle, also eine volle Umdrehung des Riesenrads. Dass unser Programm nur 128 davon benutzt, liegt daran, dass die Phase pro Schritt um 2 erhöht wird und deshalb nur jeder zweite Eintrag gelesen wird. Und dass die Tabelle am Ende automatisch wieder von vorn beginnt, liegt am Byte-Überlauf: Nach 255 kommt wieder 0. Genau deshalb ist eine 256er-Tabelle so praktisch – mit einem Byte als Index kann man sie gar nicht verlassen.
Ansatz 3: Die Tabelle rechnet der Assembler
Noch einen Schritt weiter: Warum soll der C64 die Tabelle überhaupt selbst berechnen? Der Assembler kann das beim Assemblieren erledigen und die 256 Werte direkt als Bytes ins Programm schreiben. In C64 Studio sieht das so aus:
* = $2100
SINTAB:
!for i, 0, 255 {
!byte math.floor( Y_CENTER + Y_AMP * math.sin( i * 1.40625 ) )
}
Wichtig: math.sin erwartet in C64 Studio den Winkel in Grad. Eine volle Umdrehung verteilt auf 256 Schritte sind 360 / 256 = 1.40625 Grad pro Schritt. Wer mit ACME arbeitet, schreibt die Zeile stattdessen so:
!byte int(Y_CENTER + Y_AMP * sin(float(i) * 3.14159265358979 / 128))
Damit entfallen calc_y, die ROM-Aufrufe und der Tabellenaufbau beim Start komplett. Der C64 rechnet keinen einzigen Sinuswert mehr. In der Praxis ist das der übliche Weg.
Fazit
Die Lookup-Table tauscht Rechenzeit gegen Speicher: 256 Byte für die Tabelle, dafür praktisch keine Rechenzeit mehr in der Hauptschleife. Das lohnt sich, wenn die Eingabewerte einen kleinen, festen Bereich haben, dieselben Werte immer wieder gebraucht werden und die Rechnung teuer ist – bei einer Sinusbewegung trifft alles drei zu. Die Live-Berechnung hat dafür den Vorteil, dass sie flexibel ist und keinen zusätzlichen Speicher braucht.
Ganz starr ist diese Regel übrigens nicht: In größenbeschränkten Demos (etwa 4K-Intros) werden Tabellen oft gerade zur Laufzeit erzeugt, weil ein paar Bytes Code kleiner sind als 256 Byte Daten – dann allerdings mit schneller Ganzzahl-Arithmetik statt mit den ROM-Routinen.
Der Quellcode
Alle Programme habe ich mit C64 Studio assembliert und im Emulator getestet. Gestartet werden sie ganz normal mit RUN, beendet mit RUN/STOP + RESTORE. Die Anzahl der Schiffe lässt sich in jedem Programm oben über NUM_SHIPS einstellen (1 bis 8).
- Teil 1 – Live-Berechnung mit den ROM-Routinen (roter Rahmen = Rechenzeit)
- Teil 2 – Lookup-Table, beim Programmstart berechnet (blauer Rahmen = Tabellenaufbau)
- Teil 3 – Lookup-Table, vom Assembler berechnet (nur mit C64 Studio, ACME-Zeile siehe oben)
- Demo aus dem Intro – Teil 3 mit Parallax-Sternenhintergrund, ohne Zeitmessung
Teil 1: sinus_live.asm – Live-Berechnung
; ============================================================
; sinus_live.asm - Teil 1: Flugbahn wird LIVE berechnet
;
; NUM_SHIPS Raumschiffe (1..8) fliegen hintereinander auf
; einer Sinusbahn von links nach rechts. Die Y-Position jedes
; Schiffs wird in jedem Schritt mit den Fliesskomma-Routinen
; des BASIC-ROMs berechnet:
;
; Y = 140 + 50 * SIN(phase * PI / 128)
;
; Fuer das Video: erst mit NUM_SHIPS = 1, dann mit 3 assemblieren.
; Alles andere (Startpositionen, Farben, Sprite-Freigabe)
; ergibt sich automatisch aus NUM_SHIPS.
;
; Messung: Waehrend der Berechnung ist der Rahmen rot.
;
; Assembler: ACME Start: RUN (SYS 2061)
; Abbruch: RUN/STOP + RESTORE
; ============================================================
!to ”sinus_live.prg”, cbm
; — BASIC-ROM Fliesskomma-Routinen ————————-
GIVAYF = $b391 ; Integer (A = hi, Y = lo) -> FAC
FMULT = $ba28 ; FAC = FAC * Konstante (A = lo, Y = hi)
FADD = $b867 ; FAC = FAC + Konstante (A = lo, Y = hi)
SIN = $e26b ; FAC = SIN(FAC)
GETADR = $b7f7 ; FAC -> 16 Bit ohne Vorzeichen (Y = lo, A = hi)
; — KERNAL ————————————————–
CLRSCR = $e544 ; Bildschirm loeschen
; — VIC-II ————————————————–
SP0X = $d000 ; Sprite n X = $d000 + 2*n
SP0Y = $d001 ; Sprite n Y = $d001 + 2*n
SPXMSB = $d010
RASTER = $d012
SPENA = $d015
BORDER = $d020
BGCOL = $d021
SP0COL = $d027 ; Sprite n Farbe = $d027 + n
; — Parameter ———————————————–
NUM_SHIPS = 3 ; Anzahl Schiffe (1..8)
SPACING = 40 ; Abstand zwischen den Schiffen in Pixeln
SPRITE_DATA = $2000 ; Spritedaten (64-Byte-Block Nr. $80)
SPRITE_PTR = $07f8 ; Spritezeiger Sprite 0
SYNC_LINE = 252 ; Rasterzeile fuer den Bildtakt
PHASE_STEP = 2 ; Phasenzuwachs pro Pixel (128 Pixel = 1 Welle)
X_END = 344 ; ab hier ist das Sprite rechts ausserhalb
MEASURE_COL = 2 ; Rahmenfarbe waehrend der Berechnung (rot)
IDLE_COL = 0 ; Rahmenfarbe sonst (schwarz)
!if NUM_SHIPS < 1 {
!error ”NUM_SHIPS muss mindestens 1 sein”
}
!if NUM_SHIPS > 8 {
!error ”NUM_SHIPS darf hoechstens 8 sein (8 Hardware-Sprites)”
}
; — BASIC-Zeile: 10 SYS2061 ——————————–
* = $0801
!byte $0b,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00
; ============================================================
; Hauptprogramm
; ============================================================
* = $080d
start:
jsr init
mainloop:
jsr wait_frame ; auf Bildanfang warten
lda #MEASURE_COL ; — Messung Anfang —
sta BORDER
ldx #0 ; fuer jedes Schiff Y berechnen
calc_loop:
stx ship ; X wird vom ROM veraendert -> sichern
lda phase,x
jsr calc_y ; A = Y-Position (live berechnet)
ldx ship
sta ypos,x
inx
cpx #NUM_SHIPS
bne calc_loop
lda #IDLE_COL ; — Messung Ende —
sta BORDER
jsr set_sprites ; Positionen in den VIC schreiben
jsr move_ships ; X und Phase weiterzaehlen
jmp mainloop
; ============================================================
; init - Bildschirm und Sprites vorbereiten
; ============================================================
init:
lda #0
sta BORDER
sta BGCOL
jsr CLRSCR
ldx #NUM_SHIPS-1
init_loop:
lda #SPRITE_DATA/64 ; alle Schiffe nutzen dieselben Spritedaten
sta SPRITE_PTR,x
lda ship_col,x ; Farbe
sta SP0COL,x
lda start_x_lo,x ; Startposition (16 Bit)
sta xpos_lo,x
lda start_x_hi,x
sta xpos_hi,x
lda start_phase,x ; passende Startphase
sta phase,x
dex
bpl init_loop
lda #(1 << NUM_SHIPS) - 1 ; genau NUM_SHIPS Sprites einschalten
sta SPENA
rts
; ============================================================
; wait_frame - wartet genau einmal auf SYNC_LINE
; (erst Zeile verlassen, dann auf Zeile warten, damit ein
; schneller Durchlauf nicht zweimal in derselben Zeile landet)
; ============================================================
wait_frame:
wf_leave:
lda RASTER
cmp #SYNC_LINE
beq wf_leave
wf_wait:
lda RASTER
cmp #SYNC_LINE
bne wf_wait
rts
; ============================================================
; calc_y - LIVE-Berechnung ueber das BASIC-ROM
; Eingabe: A = Phase (0..255)
; Ausgabe: A = 140 + 50 * SIN(phase * PI / 128)
; Veraendert A, X, Y
; ============================================================
calc_y:
tay ; lo = Phase
lda #0 ; hi
jsr GIVAYF ; FAC = Phase
lda #<c_step
ldy #>c_step
jsr FMULT ; FAC = Phase * PI/128
jsr SIN ; FAC = SIN(FAC)
lda #<c_amp
ldy #>c_amp
jsr FMULT ; FAC = FAC * 50
lda #<c_center
ldy #>c_center
jsr FADD ; FAC = FAC + 140
jsr GETADR ; Y = lo, A = hi (hier immer 0)
tya ; Ergebnis nach A
rts
; ============================================================
; set_sprites - X, Y und X-Bit 8 aller Schiffe in den VIC
; Durchlauf von hinten nach vorn: jedes Bit 8 wird per ROL
; in msb_tmp geschoben, Schiff 0 landet so in Bit 0.
; ============================================================
set_sprites:
lda #0
sta msb_tmp
ldx #NUM_SHIPS-1
ss_loop:
txa ; Y = 2 * Schiffnummer (VIC-Registerpaar)
asl
tay
lda xpos_lo,x
sta SP0X,y
lda ypos,x
sta SP0Y,y
lda xpos_hi,x ; 0 oder 1
lsr ; -> Carry
rol msb_tmp ; Carry in msb_tmp schieben
dex
bpl ss_loop
lda msb_tmp
sta SPXMSB
rts
; ============================================================
; move_ships - Phase und X-Position aller Schiffe weiter
; ============================================================
move_ships:
ldx #0
ms_loop:
lda phase,x ; Phase weiterdrehen (laeuft bei 256 ueber)
clc
adc #PHASE_STEP
sta phase,x
inc xpos_lo,x ; X (16 Bit) + 1
bne ms_check
inc xpos_hi,x
ms_check:
lda xpos_hi,x ; X_END erreicht?
cmp #>X_END
bne ms_next
lda xpos_lo,x
cmp #<X_END
bne ms_next
lda #0 ; ja: wieder von links, Phase zuruecksetzen
sta xpos_lo,x
sta xpos_hi,x
sta phase,x
ms_next:
inx
cpx #NUM_SHIPS
bne ms_loop
rts
; ============================================================
; Fliesskomma-Konstanten (5-Byte-Format des BASIC-ROMs)
; Byte 0: Exponent + 128
; Byte 1-4: Mantisse, Bit 7 von Byte 1 = Vorzeichen
; ============================================================
c_step: !byte $7b,$49,$0f,$da,$a2 ; PI/128 = 0.0245436926
c_amp: !byte $86,$48,$00,$00,$00 ; 50
c_center: !byte $88,$0c,$00,$00,$00 ; 140
; — Startwerte je Schiff (vom Assembler erzeugt) ————
; Schiff 0 fuehrt, die anderen folgen im Abstand SPACING:
; Schiff i startet bei X = (NUM_SHIPS-1-i) * SPACING
; Startphase = (Start-X * PHASE_STEP) mod 256
start_x_lo:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING) & $ff
}
start_x_hi:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING) >> 8
}
start_phase:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING * PHASE_STEP) & $ff
}
; — Farben (die ersten NUM_SHIPS Eintraege werden benutzt) –
ship_col: !byte 1, 13, 7, 3, 10, 14, 15, 5 ; weiss, hellgruen, gelb, cyan,
; hellrot, hellblau, hellgrau, gruen
; — Variablen je Schiff ————————————-
xpos_lo: !fill NUM_SHIPS, 0 ; X-Position, untere 8 Bit
xpos_hi: !fill NUM_SHIPS, 0 ; X-Position, Bit 8
phase: !fill NUM_SHIPS, 0 ; Winkel (0..255 = 0..2*PI)
ypos: !fill NUM_SHIPS, 0 ; berechnete Y-Position
; — Hilfsvariablen ——————————————
ship: !byte 0 ; aktuelles Schiff
msb_tmp: !byte 0 ; Sammelbyte fuer $D010
; ============================================================
; Spritedaten: Raumschiff, Nase nach rechts (24 x 21)
; ============================================================
* = SPRITE_DATA
!byte $00,$00,$00 ; 0
!byte $00,$00,$00 ; 1
!byte $c0,$00,$00 ; 2 ##
!byte $f0,$00,$00 ; 3 ####
!byte $7c,$00,$00 ; 4 #####
!byte $3f,$00,$00 ; 5 ######
!byte $1f,$e0,$00 ; 6 ########
!byte $0f,$fe,$00 ; 7 ###########
!byte $0f,$ff,$c0 ; 8 ##############
!byte $1f,$ff,$f8 ; 9 ##################
!byte $3f,$ff,$ff ; 10 ######################
!byte $1f,$ff,$f8 ; 11 ##################
!byte $0f,$ff,$c0 ; 12 ##############
!byte $0f,$fe,$00 ; 13 ###########
!byte $1f,$e0,$00 ; 14 ########
!byte $3f,$00,$00 ; 15 ######
!byte $7c,$00,$00 ; 16 #####
!byte $f0,$00,$00 ; 17 ####
!byte $c0,$00,$00 ; 18 ##
!byte $00,$00,$00 ; 19
!byte $00,$00,$00 ; 20
Teil 2: sinus_lut.asm – Tabelle beim Start berechnet
; ============================================================
; sinus_lut.asm - Teil 2: Flugbahn aus VORBERECHNETER Tabelle
;
; Gleiches Programm wie Teil 1 (NUM_SHIPS Schiffe). Unterschied:
; - calc_y (unveraendert aus Teil 1) wird beim Start genau
; 256-mal aufgerufen und fuellt die Tabelle SINTAB.
; - In der Hauptschleife wird je Schiff nur nachgeschlagen:
; LDY phase,X / LDA SINTAB,Y
;
; Alles andere (Startpositionen, Farben, Sprite-Freigabe)
; ergibt sich automatisch aus NUM_SHIPS.
;
; Messung: Waehrend der Positionsbestimmung ist der Rahmen rot.
; Waehrend des Tabellenaufbaus ist der Rahmen blau.
;
; Assembler: ACME Start: RUN (SYS 2061)
; Abbruch: RUN/STOP + RESTORE
; ============================================================
!to ”sinus_lut.prg”, cbm
; — BASIC-ROM Fliesskomma-Routinen ————————-
GIVAYF = $b391 ; Integer (A = hi, Y = lo) -> FAC
FMULT = $ba28 ; FAC = FAC * Konstante (A = lo, Y = hi)
FADD = $b867 ; FAC = FAC + Konstante (A = lo, Y = hi)
SIN = $e26b ; FAC = SIN(FAC)
GETADR = $b7f7 ; FAC -> 16 Bit ohne Vorzeichen (Y = lo, A = hi)
; — KERNAL ————————————————–
CLRSCR = $e544 ; Bildschirm loeschen
; — VIC-II ————————————————–
SP0X = $d000 ; Sprite n X = $d000 + 2*n
SP0Y = $d001 ; Sprite n Y = $d001 + 2*n
SPXMSB = $d010
RASTER = $d012
SPENA = $d015
BORDER = $d020
BGCOL = $d021
SP0COL = $d027 ; Sprite n Farbe = $d027 + n
; — Parameter ———————————————–
NUM_SHIPS = 3 ; Anzahl Schiffe (1..8)
SPACING = 40 ; Abstand zwischen den Schiffen in Pixeln
SPRITE_DATA = $2000 ; Spritedaten (64-Byte-Block Nr. $80)
SPRITE_PTR = $07f8 ; Spritezeiger Sprite 0
SINTAB = $2100 ; Sinustabelle, 256 Byte, seitenbuendig
SYNC_LINE = 252 ; Rasterzeile fuer den Bildtakt
PHASE_STEP = 2 ; Phasenzuwachs pro Pixel (128 Pixel = 1 Welle)
X_END = 344 ; ab hier ist das Sprite rechts ausserhalb
MEASURE_COL = 2 ; Rahmenfarbe waehrend der Positionsbestimmung (rot)
BUILD_COL = 6 ; Rahmenfarbe waehrend des Tabellenaufbaus (blau)
IDLE_COL = 0 ; Rahmenfarbe sonst (schwarz)
!if NUM_SHIPS < 1 {
!error ”NUM_SHIPS muss mindestens 1 sein”
}
!if NUM_SHIPS > 8 {
!error ”NUM_SHIPS darf hoechstens 8 sein (8 Hardware-Sprites)”
}
; — BASIC-Zeile: 10 SYS2061 ——————————–
* = $0801
!byte $0b,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00
; ============================================================
; Hauptprogramm
; ============================================================
* = $080d
start:
jsr init
jsr build_table ; einmalig: Tabelle berechnen
mainloop:
jsr wait_frame ; auf Bildanfang warten
lda #MEASURE_COL ; — Messung Anfang —
sta BORDER
ldx #0 ; fuer jedes Schiff Y nachschlagen
lut_loop:
ldy phase,x ; Phase als Tabellenindex
lda SINTAB,y
sta ypos,x
inx
cpx #NUM_SHIPS
bne lut_loop
lda #IDLE_COL ; — Messung Ende —
sta BORDER
jsr set_sprites ; Positionen in den VIC schreiben
jsr move_ships ; X und Phase weiterzaehlen
jmp mainloop
; ============================================================
; init - Bildschirm und Sprites vorbereiten
; ============================================================
init:
lda #0
sta BORDER
sta BGCOL
jsr CLRSCR
ldx #NUM_SHIPS-1
init_loop:
lda #SPRITE_DATA/64 ; alle Schiffe nutzen dieselben Spritedaten
sta SPRITE_PTR,x
lda ship_col,x ; Farbe
sta SP0COL,x
lda start_x_lo,x ; Startposition (16 Bit)
sta xpos_lo,x
lda start_x_hi,x
sta xpos_hi,x
lda start_phase,x ; passende Startphase
sta phase,x
dex
bpl init_loop
lda #(1 << NUM_SHIPS) - 1 ; genau NUM_SHIPS Sprites einschalten
sta SPENA
rts
; ============================================================
; build_table - SINTAB mit 256 Werten fuellen
; Ruft fuer Phase 0..255 die Routine calc_y auf.
; ============================================================
build_table:
lda #BUILD_COL
sta BORDER
lda #0
sta tab_index
bt_loop:
lda tab_index
jsr calc_y ; A = Y-Wert fuer diese Phase
ldx tab_index
sta SINTAB,x ; in Tabelle ablegen
inc tab_index
bne bt_loop ; 256 Durchlaeufe
lda #IDLE_COL
sta BORDER
rts
; ============================================================
; wait_frame - wartet genau einmal auf SYNC_LINE
; ============================================================
wait_frame:
wf_leave:
lda RASTER
cmp #SYNC_LINE
beq wf_leave
wf_wait:
lda RASTER
cmp #SYNC_LINE
bne wf_wait
rts
; ============================================================
; calc_y - unveraendert aus Teil 1
; Eingabe: A = Phase (0..255)
; Ausgabe: A = 140 + 50 * SIN(phase * PI / 128)
; Veraendert A, X, Y
; ============================================================
calc_y:
tay ; lo = Phase
lda #0 ; hi
jsr GIVAYF ; FAC = Phase
lda #<c_step
ldy #>c_step
jsr FMULT ; FAC = Phase * PI/128
jsr SIN ; FAC = SIN(FAC)
lda #<c_amp
ldy #>c_amp
jsr FMULT ; FAC = FAC * 50
lda #<c_center
ldy #>c_center
jsr FADD ; FAC = FAC + 140
jsr GETADR ; Y = lo, A = hi (hier immer 0)
tya ; Ergebnis nach A
rts
; ============================================================
; set_sprites - X, Y und X-Bit 8 aller Schiffe in den VIC
; Durchlauf von hinten nach vorn: jedes Bit 8 wird per ROL
; in msb_tmp geschoben, Schiff 0 landet so in Bit 0.
; ============================================================
set_sprites:
lda #0
sta msb_tmp
ldx #NUM_SHIPS-1
ss_loop:
txa ; Y = 2 * Schiffnummer (VIC-Registerpaar)
asl
tay
lda xpos_lo,x
sta SP0X,y
lda ypos,x
sta SP0Y,y
lda xpos_hi,x ; 0 oder 1
lsr ; -> Carry
rol msb_tmp ; Carry in msb_tmp schieben
dex
bpl ss_loop
lda msb_tmp
sta SPXMSB
rts
; ============================================================
; move_ships - Phase und X-Position aller Schiffe weiter
; ============================================================
move_ships:
ldx #0
ms_loop:
lda phase,x ; Phase weiterdrehen (laeuft bei 256 ueber)
clc
adc #PHASE_STEP
sta phase,x
inc xpos_lo,x ; X (16 Bit) + 1
bne ms_check
inc xpos_hi,x
ms_check:
lda xpos_hi,x ; X_END erreicht?
cmp #>X_END
bne ms_next
lda xpos_lo,x
cmp #<X_END
bne ms_next
lda #0 ; ja: wieder von links, Phase zuruecksetzen
sta xpos_lo,x
sta xpos_hi,x
sta phase,x
ms_next:
inx
cpx #NUM_SHIPS
bne ms_loop
rts
; ============================================================
; Fliesskomma-Konstanten (5-Byte-Format des BASIC-ROMs)
; ============================================================
c_step: !byte $7b,$49,$0f,$da,$a2 ; PI/128 = 0.0245436926
c_amp: !byte $86,$48,$00,$00,$00 ; 50
c_center: !byte $88,$0c,$00,$00,$00 ; 140
; — Startwerte je Schiff (vom Assembler erzeugt) ————
; Schiff 0 fuehrt, die anderen folgen im Abstand SPACING:
; Schiff i startet bei X = (NUM_SHIPS-1-i) * SPACING
; Startphase = (Start-X * PHASE_STEP) mod 256
start_x_lo:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING) & $ff
}
start_x_hi:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING) >> 8
}
start_phase:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING * PHASE_STEP) & $ff
}
; — Farben (die ersten NUM_SHIPS Eintraege werden benutzt) –
ship_col: !byte 1, 13, 7, 3, 10, 14, 15, 5 ; weiss, hellgruen, gelb, cyan,
; hellrot, hellblau, hellgrau, gruen
; — Variablen je Schiff ————————————-
xpos_lo: !fill NUM_SHIPS, 0 ; X-Position, untere 8 Bit
xpos_hi: !fill NUM_SHIPS, 0 ; X-Position, Bit 8
phase: !fill NUM_SHIPS, 0 ; Winkel (0..255 = 0..2*PI)
ypos: !fill NUM_SHIPS, 0 ; nachgeschlagene Y-Position
; — Hilfsvariablen ——————————————
tab_index: !byte 0 ; Zaehler fuer build_table
msb_tmp: !byte 0 ; Sammelbyte fuer $D010
; ============================================================
; Spritedaten: Raumschiff, Nase nach rechts (24 x 21)
; ============================================================
* = SPRITE_DATA
!byte $00,$00,$00 ; 0
!byte $00,$00,$00 ; 1
!byte $c0,$00,$00 ; 2 ##
!byte $f0,$00,$00 ; 3 ####
!byte $7c,$00,$00 ; 4 #####
!byte $3f,$00,$00 ; 5 ######
!byte $1f,$e0,$00 ; 6 ########
!byte $0f,$fe,$00 ; 7 ###########
!byte $0f,$ff,$c0 ; 8 ##############
!byte $1f,$ff,$f8 ; 9 ##################
!byte $3f,$ff,$ff ; 10 ######################
!byte $1f,$ff,$f8 ; 11 ##################
!byte $0f,$ff,$c0 ; 12 ##############
!byte $0f,$fe,$00 ; 13 ###########
!byte $1f,$e0,$00 ; 14 ########
!byte $3f,$00,$00 ; 15 ######
!byte $7c,$00,$00 ; 16 #####
!byte $f0,$00,$00 ; 17 ####
!byte $c0,$00,$00 ; 18 ##
!byte $00,$00,$00 ; 19
!byte $00,$00,$00 ; 20
; ============================================================
; AUSBLICK: Die Tabelle kann auch schon der Assembler beim
; Assemblieren berechnen. Dann entfallen build_table und calc_y
; komplett - siehe Teil 3 (sinus_lut_asm.asm).
; ============================================================
Teil 3: sinus_lut_asm.asm – Tabelle vom Assembler berechnet
; ============================================================
; sinus_lut_asm.asm - Teil 3: Tabelle vom ASSEMBLER erzeugt
;
; Gleiches Programm wie Teil 2 (NUM_SHIPS Schiffe). Unterschied:
; - Die Sinustabelle SINTAB wird nicht mehr auf dem C64
; berechnet, sondern vom Assembler (C64 Studio) beim Assemblieren (!for-Schleife
; am Ende der Datei). Die 256 Werte stehen fertig in der .prg.
; - build_table, calc_y und die Fliesskomma-Konstanten
; entfallen. Der C64 rechnet keinen einzigen Sinus mehr.
; - Kein Tabellenaufbau beim Start -> kein blauer Rahmen.
;
; Die Formel ist dieselbe wie in Teil 1 und 2:
; Y = 140 + 50 * SIN(phase * PI / 128)
;
; Messung: Waehrend der Positionsbestimmung ist der Rahmen rot.
;
; Assembler: C64 Studio (math.sin, math.floor) Start: RUN
; Abbruch: RUN/STOP + RESTORE
; ============================================================
!to ”sinus_lut_asm.prg”, cbm
; — KERNAL ————————————————–
CLRSCR = $e544 ; Bildschirm loeschen
; — VIC-II ————————————————–
SP0X = $d000 ; Sprite n X = $d000 + 2*n
SP0Y = $d001 ; Sprite n Y = $d001 + 2*n
SPXMSB = $d010
RASTER = $d012
SPENA = $d015
BORDER = $d020
BGCOL = $d021
SP0COL = $d027 ; Sprite n Farbe = $d027 + n
; — Parameter ———————————————–
NUM_SHIPS = 3 ; Anzahl Schiffe (1..8)
SPACING = 40 ; Abstand zwischen den Schiffen in Pixeln
SPRITE_DATA = $2000 ; Spritedaten (64-Byte-Block Nr. $80)
SPRITE_PTR = $07f8 ; Spritezeiger Sprite 0
SYNC_LINE = 252 ; Rasterzeile fuer den Bildtakt
PHASE_STEP = 2 ; Phasenzuwachs pro Pixel (128 Pixel = 1 Welle)
X_END = 344 ; ab hier ist das Sprite rechts ausserhalb
MEASURE_COL = 2 ; Rahmenfarbe waehrend der Positionsbestimmung (rot)
IDLE_COL = 0 ; Rahmenfarbe sonst (schwarz)
; — Parameter der Sinuskurve (nur fuer den Assembler) ——-
Y_CENTER = 140 ; Mittellage
Y_AMP = 50 ; Amplitude
!if NUM_SHIPS < 1 {
!error ”NUM_SHIPS muss mindestens 1 sein”
}
!if NUM_SHIPS > 8 {
!error ”NUM_SHIPS darf hoechstens 8 sein (8 Hardware-Sprites)”
}
; — BASIC-Zeile: 10 SYS2061 ——————————–
* = $0801
!byte $0b,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00
; ============================================================
; Hauptprogramm
; ============================================================
* = $080d
start:
jsr init ; kein build_table mehr noetig
mainloop:
jsr wait_frame ; auf Bildanfang warten
lda #MEASURE_COL ; — Messung Anfang —
sta BORDER
ldx #0 ; fuer jedes Schiff Y nachschlagen
lut_loop:
ldy phase,x ; Phase als Tabellenindex
lda SINTAB,y
sta ypos,x
inx
cpx #NUM_SHIPS
bne lut_loop
lda #IDLE_COL ; — Messung Ende —
sta BORDER
jsr set_sprites ; Positionen in den VIC schreiben
jsr move_ships ; X und Phase weiterzaehlen
jmp mainloop
; ============================================================
; init - Bildschirm und Sprites vorbereiten
; ============================================================
init:
lda #0
sta BORDER
sta BGCOL
jsr CLRSCR
ldx #NUM_SHIPS-1
init_loop:
lda #SPRITE_DATA/64 ; alle Schiffe nutzen dieselben Spritedaten
sta SPRITE_PTR,x
lda ship_col,x ; Farbe
sta SP0COL,x
lda start_x_lo,x ; Startposition (16 Bit)
sta xpos_lo,x
lda start_x_hi,x
sta xpos_hi,x
lda start_phase,x ; passende Startphase
sta phase,x
dex
bpl init_loop
lda #(1 << NUM_SHIPS) - 1 ; genau NUM_SHIPS Sprites einschalten
sta SPENA
rts
; ============================================================
; wait_frame - wartet genau einmal auf SYNC_LINE
; ============================================================
wait_frame:
wf_leave:
lda RASTER
cmp #SYNC_LINE
beq wf_leave
wf_wait:
lda RASTER
cmp #SYNC_LINE
bne wf_wait
rts
; ============================================================
; set_sprites - X, Y und X-Bit 8 aller Schiffe in den VIC
; Durchlauf von hinten nach vorn: jedes Bit 8 wird per ROL
; in msb_tmp geschoben, Schiff 0 landet so in Bit 0.
; ============================================================
set_sprites:
lda #0
sta msb_tmp
ldx #NUM_SHIPS-1
ss_loop:
txa ; Y = 2 * Schiffnummer (VIC-Registerpaar)
asl
tay
lda xpos_lo,x
sta SP0X,y
lda ypos,x
sta SP0Y,y
lda xpos_hi,x ; 0 oder 1
lsr ; -> Carry
rol msb_tmp ; Carry in msb_tmp schieben
dex
bpl ss_loop
lda msb_tmp
sta SPXMSB
rts
; ============================================================
; move_ships - Phase und X-Position aller Schiffe weiter
; ============================================================
move_ships:
ldx #0
ms_loop:
lda phase,x ; Phase weiterdrehen (laeuft bei 256 ueber)
clc
adc #PHASE_STEP
sta phase,x
inc xpos_lo,x ; X (16 Bit) + 1
bne ms_check
inc xpos_hi,x
ms_check:
lda xpos_hi,x ; X_END erreicht?
cmp #>X_END
bne ms_next
lda xpos_lo,x
cmp #<X_END
bne ms_next
lda #0 ; ja: wieder von links, Phase zuruecksetzen
sta xpos_lo,x
sta xpos_hi,x
sta phase,x
ms_next:
inx
cpx #NUM_SHIPS
bne ms_loop
rts
; — Startwerte je Schiff (vom Assembler erzeugt) ————
; Schiff 0 fuehrt, die anderen folgen im Abstand SPACING:
; Schiff i startet bei X = (NUM_SHIPS-1-i) * SPACING
; Startphase = (Start-X * PHASE_STEP) mod 256
start_x_lo:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING) & $ff
}
start_x_hi:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING) >> 8
}
start_phase:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING * PHASE_STEP) & $ff
}
; — Farben (die ersten NUM_SHIPS Eintraege werden benutzt) –
ship_col: !byte 1, 13, 7, 3, 10, 14, 15, 5 ; weiss, hellgruen, gelb, cyan,
; hellrot, hellblau, hellgrau, gruen
; — Variablen je Schiff ————————————-
xpos_lo: !fill NUM_SHIPS, 0 ; X-Position, untere 8 Bit
xpos_hi: !fill NUM_SHIPS, 0 ; X-Position, Bit 8
phase: !fill NUM_SHIPS, 0 ; Winkel (0..255 = 0..2*PI)
ypos: !fill NUM_SHIPS, 0 ; nachgeschlagene Y-Position
; — Hilfsvariablen ——————————————
msb_tmp: !byte 0 ; Sammelbyte fuer $D010
; ============================================================
; Spritedaten: Raumschiff, Nase nach rechts (24 x 21)
; ============================================================
* = SPRITE_DATA
!byte $00,$00,$00 ; 0
!byte $00,$00,$00 ; 1
!byte $c0,$00,$00 ; 2 ##
!byte $f0,$00,$00 ; 3 ####
!byte $7c,$00,$00 ; 4 #####
!byte $3f,$00,$00 ; 5 ######
!byte $1f,$e0,$00 ; 6 ########
!byte $0f,$fe,$00 ; 7 ###########
!byte $0f,$ff,$c0 ; 8 ##############
!byte $1f,$ff,$f8 ; 9 ##################
!byte $3f,$ff,$ff ; 10 ######################
!byte $1f,$ff,$f8 ; 11 ##################
!byte $0f,$ff,$c0 ; 12 ##############
!byte $0f,$fe,$00 ; 13 ###########
!byte $1f,$e0,$00 ; 14 ########
!byte $3f,$00,$00 ; 15 ######
!byte $7c,$00,$00 ; 16 #####
!byte $f0,$00,$00 ; 17 ####
!byte $c0,$00,$00 ; 18 ##
!byte $00,$00,$00 ; 19
!byte $00,$00,$00 ; 20
; ============================================================
; Sinustabelle - wird vom Assembler (C64 Studio) berechnet
; 256 Eintraege, seitenbuendig bei $2100.
; Fuer jede Phase i (0..255):
; Y = Y_CENTER + Y_AMP * SIN(i * PI / 128)
;
; Hinweis C64 Studio: math.sin erwartet GRAD, nicht Bogenmass.
; i * PI / 128 im Bogenmass entspricht i * 360 / 256 Grad,
; also i * 1.40625 Grad (als Kommazahl, damit keine
; Ganzzahldivision die Nachkommastellen abschneidet).
; math.floor schneidet die Nachkommastellen ab - genau wie
; GETADR in Teil 1 und 2 (alle Werte sind positiv).
; ============================================================
* = $2100
SINTAB:
!for i, 0, 255 {
!byte math.floor( Y_CENTER + Y_AMP * math.sin( i * 1.40625 ) )
}
Intro-Demo: sinus_lut_sterne.asm – mit Sternenhintergrund
; ============================================================
; sinus_lut_sterne.asm - Teil 3 mit Sternenhintergrund
;
; Wie sinus_lut_asm.asm (Tabelle vom Assembler erzeugt), dazu:
; - 25 Sterne (einer pro Textzeile) scrollen von rechts nach
; links. Drei Geschwindigkeiten mit drei Helligkeiten
; (dunkelgrau = langsam/fern, grau = mittel, weiss = schnell/nah)
; ergeben einen einfachen Parallax-Effekt.
; - Die Sterne sind Textzeichen. Damit sie pixelweise statt in
; 8er-Schritten wandern, gibt es 8 eigene Zeichen: in jedem
; sitzt der Sternpunkt an einer anderen Pixelspalte (0..7).
; Dafuer wird ein eigener (sonst leerer) Zeichensatz bei
; $3000 angelegt.
;
; Diese Fassung ist als Einstieg ins Video gedacht (”so soll es
; aussehen”) und hat deshalb KEINE Zeitmessung (kein roter Rahmen).
;
; Assembler: C64 Studio (math.sin, math.floor) Start: RUN
; Abbruch: RUN/STOP + RESTORE (setzt auch den Zeichensatz zurueck)
; ============================================================
!to ”sinus_lut_sterne.prg”, cbm
; — KERNAL ————————————————–
CLRSCR = $e544 ; Bildschirm loeschen
; — VIC-II ————————————————–
SP0X = $d000 ; Sprite n X = $d000 + 2*n
SP0Y = $d001 ; Sprite n Y = $d001 + 2*n
SPXMSB = $d010
RASTER = $d012
SPENA = $d015
VMCSB = $d018 ; Lage von Bildschirm und Zeichensatz
BORDER = $d020
BGCOL = $d021
SP0COL = $d027 ; Sprite n Farbe = $d027 + n
; — Speicher ————————————————
SCREEN = $0400 ; Bildschirmspeicher
COLRAM = $d800 ; Farbspeicher
CHARSET = $3000 ; eigener Zeichensatz (2 KB, $3000-$37FF)
; — Zeropage-Zeiger (im BASIC-Betrieb frei) —————–
ZP_SCR = $fb ; $fb/$fc: Zeiger in den Bildschirm
ZP_COL = $fd ; $fd/$fe: Zeiger in den Farbspeicher
; — Parameter ———————————————–
NUM_SHIPS = 3 ; Anzahl Schiffe (1..8)
SPACING = 40 ; Abstand zwischen den Schiffen in Pixeln
SPRITE_DATA = $2000 ; Spritedaten (64-Byte-Block Nr. $80)
SPRITE_PTR = $07f8 ; Spritezeiger Sprite 0
SYNC_LINE = 252 ; Rasterzeile fuer den Bildtakt
PHASE_STEP = 2 ; Phasenzuwachs pro Pixel (128 Pixel = 1 Welle)
X_END = 344 ; ab hier ist das Sprite rechts ausserhalb
NUM_STARS = 25 ; ein Stern pro Textzeile
STAR_CHAR = 1 ; Sternzeichen belegen die Codes 1..8
BLANK_CHAR = $20 ; Leerzeichen (im eigenen Zeichensatz leer)
; — Parameter der Sinuskurve (nur fuer den Assembler) ——-
Y_CENTER = 140 ; Mittellage
Y_AMP = 50 ; Amplitude
!if NUM_SHIPS < 1 {
!error ”NUM_SHIPS muss mindestens 1 sein”
}
!if NUM_SHIPS > 8 {
!error ”NUM_SHIPS darf hoechstens 8 sein (8 Hardware-Sprites)”
}
; — BASIC-Zeile: 10 SYS2061 ——————————–
* = $0801
!byte $0b,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00
; ============================================================
; Hauptprogramm
; ============================================================
* = $080d
start:
jsr init
jsr init_charset ; Sternzeichen anlegen, Zeichensatz umschalten
mainloop:
jsr wait_frame ; auf Bildanfang warten
ldx #0 ; fuer jedes Schiff Y nachschlagen
lut_loop:
ldy phase,x ; Phase als Tabellenindex
lda SINTAB,y
sta ypos,x
inx
cpx #NUM_SHIPS
bne lut_loop
jsr set_sprites ; Positionen in den VIC schreiben
jsr move_ships ; X und Phase weiterzaehlen
jsr move_stars ; Sternenhintergrund weiterscrollen
jmp mainloop
; ============================================================
; init - Bildschirm und Sprites vorbereiten
; ============================================================
init:
lda #0
sta BORDER
sta BGCOL
jsr CLRSCR
ldx #NUM_SHIPS-1
init_loop:
lda #SPRITE_DATA/64 ; alle Schiffe nutzen dieselben Spritedaten
sta SPRITE_PTR,x
lda ship_col,x ; Farbe
sta SP0COL,x
lda start_x_lo,x ; Startposition (16 Bit)
sta xpos_lo,x
lda start_x_hi,x
sta xpos_hi,x
lda start_phase,x ; passende Startphase
sta phase,x
dex
bpl init_loop
lda #(1 << NUM_SHIPS) - 1 ; genau NUM_SHIPS Sprites einschalten
sta SPENA
rts
; ============================================================
; init_charset - eigenen Zeichensatz bei $3000 anlegen
; 1. alle 2 KB loeschen (alle Zeichen leer, auch das Leerzeichen)
; 2. Zeichen STAR_CHAR+0 .. STAR_CHAR+7 erhalten je einen
; Punkt in Pixelzeile 3, Pixelspalte 0..7
; 3. VIC auf den neuen Zeichensatz umschalten
; ============================================================
init_charset:
lda #0
tax
ic_clear:
sta CHARSET,x ; 8 Seiten a 256 Byte
sta CHARSET+$100,x
sta CHARSET+$200,x
sta CHARSET+$300,x
sta CHARSET+$400,x
sta CHARSET+$500,x
sta CHARSET+$600,x
sta CHARSET+$700,x
inx
bne ic_clear
ldx #0 ; X = Pixelspalte 0..7
ldy #0 ; Y = Abstand zum naechsten Zeichen (8 Byte)
ic_stars:
lda star_bits,x
sta CHARSET + STAR_CHAR*8 + 3,y ; Pixelzeile 3 des Zeichens
tya
clc
adc #8
tay
inx
cpx #8
bne ic_stars
lda #$1c ; Bildschirm $0400, Zeichensatz $3000
sta VMCSB
rts
; ============================================================
; wait_frame - wartet genau einmal auf SYNC_LINE
; ============================================================
wait_frame:
wf_leave:
lda RASTER
cmp #SYNC_LINE
beq wf_leave
wf_wait:
lda RASTER
cmp #SYNC_LINE
bne wf_wait
rts
; ============================================================
; set_sprites - X, Y und X-Bit 8 aller Schiffe in den VIC
; Durchlauf von hinten nach vorn: jedes Bit 8 wird per ROL
; in msb_tmp geschoben, Schiff 0 landet so in Bit 0.
; ============================================================
set_sprites:
lda #0
sta msb_tmp
ldx #NUM_SHIPS-1
ss_loop:
txa ; Y = 2 * Schiffnummer (VIC-Registerpaar)
asl
tay
lda xpos_lo,x
sta SP0X,y
lda ypos,x
sta SP0Y,y
lda xpos_hi,x ; 0 oder 1
lsr ; -> Carry
rol msb_tmp ; Carry in msb_tmp schieben
dex
bpl ss_loop
lda msb_tmp
sta SPXMSB
rts
; ============================================================
; move_ships - Phase und X-Position aller Schiffe weiter
; ============================================================
move_ships:
ldx #0
ms_loop:
lda phase,x ; Phase weiterdrehen (laeuft bei 256 ueber)
clc
adc #PHASE_STEP
sta phase,x
inc xpos_lo,x ; X (16 Bit) + 1
bne ms_check
inc xpos_hi,x
ms_check:
lda xpos_hi,x ; X_END erreicht?
cmp #>X_END
bne ms_next
lda xpos_lo,x
cmp #<X_END
bne ms_next
lda #0 ; ja: wieder von links, Phase zuruecksetzen
sta xpos_lo,x
sta xpos_hi,x
sta phase,x
ms_next:
inx
cpx #NUM_SHIPS
bne ms_loop
rts
; ============================================================
; move_stars - alle Sterne um ihre Geschwindigkeit nach links
; Jeder Stern hat:
; star_row Textzeile (0..24)
; star_cx Textspalte (0..39)
; star_px Pixel innerhalb des Zeichens (0..7)
; star_spd Pixel pro Frame (1..3)
; Ablauf je Stern:
; 1. Zeiger auf den Zeilenanfang setzen
; 2. alten Stern loeschen (Leerzeichen schreiben)
; 3. star_px -= star_spd; bei Unterlauf +8 und eine
; Spalte nach links; links raus -> rechts wieder rein
; 4. neues Zeichen (STAR_CHAR + star_px) und Farbe schreiben
; ============================================================
move_stars:
ldx #NUM_STARS-1
mst_loop:
ldy star_row,x ; 1. Zeiger fuer diese Zeile
lda row_lo,y
sta ZP_SCR
sta ZP_COL
lda row_hi,y
sta ZP_SCR+1
clc
adc #>(COLRAM - SCREEN) ; gleiche Stelle im Farbspeicher
sta ZP_COL+1
ldy star_cx,x ; 2. alten Stern loeschen
lda #BLANK_CHAR
sta (ZP_SCR),y
lda star_px,x ; 3. nach links bewegen
sec
sbc star_spd,x
bcs mst_store ; kein Unterlauf: gleiche Spalte
adc #8 ; Unterlauf (Carry ist 0): +8 …
sta star_px,x
dec star_cx,x ; … und eine Spalte nach links
bpl mst_draw
lda #39 ; links raus -> ganz rechts wieder rein
sta star_cx,x
jmp mst_draw
mst_store:
sta star_px,x
mst_draw:
ldy star_cx,x ; 4. neuen Stern zeichnen
lda star_px,x
clc
adc #STAR_CHAR
sta (ZP_SCR),y
lda star_col,x
sta (ZP_COL),y
dex
bpl mst_loop
rts
; — Startwerte je Schiff (vom Assembler erzeugt) ————
; Schiff 0 fuehrt, die anderen folgen im Abstand SPACING:
; Schiff i startet bei X = (NUM_SHIPS-1-i) * SPACING
; Startphase = (Start-X * PHASE_STEP) mod 256
start_x_lo:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING) & $ff
}
start_x_hi:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING) >> 8
}
start_phase:
!for i, 0, NUM_SHIPS-1 {
!byte ((NUM_SHIPS-1-i) * SPACING * PHASE_STEP) & $ff
}
; — Farben (die ersten NUM_SHIPS Eintraege werden benutzt) –
ship_col: !byte 1, 13, 7, 3, 10, 14, 15, 5 ; weiss, hellgruen, gelb, cyan,
; hellrot, hellblau, hellgrau, gruen
; — Variablen je Schiff ————————————-
xpos_lo: !fill NUM_SHIPS, 0 ; X-Position, untere 8 Bit
xpos_hi: !fill NUM_SHIPS, 0 ; X-Position, Bit 8
phase: !fill NUM_SHIPS, 0 ; Winkel (0..255 = 0..2*PI)
ypos: !fill NUM_SHIPS, 0 ; nachgeschlagene Y-Position
; — Hilfsvariablen ——————————————
msb_tmp: !byte 0 ; Sammelbyte fuer $D010
; — Sternzeichen: Punkt an Pixelspalte 0..7 —————–
star_bits: !byte $80, $40, $20, $10, $08, $04, $02, $01
; — Zeilenanfaenge im Bildschirmspeicher (vom Assembler) —-
row_lo:
!for i, 0, 24 {
!byte <(SCREEN + i * 40)
}
row_hi:
!for i, 0, 24 {
!byte >(SCREEN + i * 40)
}
; — Sterne (je Textzeile einer) —————————–
; Geschwindigkeit 1 = dunkelgrau (11), 2 = grau (12), 3 = weiss (1)
star_row: !byte 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
!byte 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24
star_cx: !byte 3, 27, 14, 36, 8, 22, 31, 1, 17, 39, 11, 25, 5
!byte 33, 19, 29, 9, 37, 2, 23, 13, 35, 6, 28, 16
star_px: !byte 0, 3, 6, 1, 4, 7, 2, 5, 0, 3, 6, 1, 4
!byte 7, 2, 5, 0, 3, 6, 1, 4, 7, 2, 5, 0
star_spd: !byte 1, 3, 2, 1, 2, 1, 3, 2, 1, 2, 3, 1, 2
!byte 1, 3, 2, 1, 2, 1, 3, 2, 1, 3, 1, 2
star_col: !byte 11, 1, 12, 11, 12, 11, 1, 12, 11, 12, 1, 11, 12
!byte 11, 1, 12, 11, 12, 11, 1, 12, 11, 1, 11, 12
; ============================================================
; Spritedaten: Raumschiff, Nase nach rechts (24 x 21)
; ============================================================
* = SPRITE_DATA
!byte $00,$00,$00 ; 0
!byte $00,$00,$00 ; 1
!byte $c0,$00,$00 ; 2 ##
!byte $f0,$00,$00 ; 3 ####
!byte $7c,$00,$00 ; 4 #####
!byte $3f,$00,$00 ; 5 ######
!byte $1f,$e0,$00 ; 6 ########
!byte $0f,$fe,$00 ; 7 ###########
!byte $0f,$ff,$c0 ; 8 ##############
!byte $1f,$ff,$f8 ; 9 ##################
!byte $3f,$ff,$ff ; 10 ######################
!byte $1f,$ff,$f8 ; 11 ##################
!byte $0f,$ff,$c0 ; 12 ##############
!byte $0f,$fe,$00 ; 13 ###########
!byte $1f,$e0,$00 ; 14 ########
!byte $3f,$00,$00 ; 15 ######
!byte $7c,$00,$00 ; 16 #####
!byte $f0,$00,$00 ; 17 ####
!byte $c0,$00,$00 ; 18 ##
!byte $00,$00,$00 ; 19
!byte $00,$00,$00 ; 20
; ============================================================
; Sinustabelle - wird vom Assembler (C64 Studio) berechnet
; 256 Eintraege, seitenbuendig bei $2100.
; Fuer jede Phase i (0..255):
; Y = Y_CENTER + Y_AMP * SIN(i * PI / 128)
; math.sin erwartet GRAD: i * 360 / 256 = i * 1.40625 Grad.
; ============================================================
* = $2100
SINTAB:
!for i, 0, 255 {
!byte math.floor( Y_CENTER + Y_AMP * math.sin( i * 1.40625 ) )
}
Viel Spaß beim Ausprobieren – und wie immer: Fragen und Anmerkungen gerne in die Kommentare.


