Montag, September 28, 2026

C64 Assembler: Sinusflyer – live berechnen oder Lookup-Table?

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

Raumschiff auf einer Sinusbahn

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:

  1. mal 50 – aus -1 bis +1 wird -50 bis +50. Das Schiff schwingt 50 Pixel nach oben und nach unten (die Amplitude).
  2. 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.
  3. 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

lut_tabelle.png

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

fazit_live_vs_lut.png

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.

  1. n1trox_digital

    Dienstag, September 29, 2026 um 22:05:57

    Moin,
    ich bin der n1troX (”Masterfred)” und bin auch Hobby Retro Programmierer.
    Der Code hat noch hohes Optimierungspotential.
    Die Tabellen könnten anders auf- und eingebaut werden.
    Auch ohne C64 Studio math.sin Funktion.
    Ich habe mir erlaubt deinen Code zu optimieren und auf Variante 2 “blauer rahmen während tabellenbau” anzuwenden. Das Ergebnis kann sich durchaus sehen lassen, obwohl ich das blaue garnicht sehe 😄
    Das Programm funktioniert wie vorher, hat jedoch durchaus noch mehr Vorteile:
    Basic ROM ($a000-bfff) kann aus für mehr ram (hab ich schon), Kernal ROM könnte auch noch aus.
    Denn wenn man ein ganz großes Programm von $0801-9fff schreibt, braucht bzw hat man ja noch platz. Allerdings kann man $a000-bfff und $e000-fff9 garnicht im code als adresse definieren (weil ja zu der Zeit der kernal an ist). Daher kann man als trick im Code den kernal/basic ausmachen und dann danach zur Laufzeit seine Daten dorthinkopieren.
    Es gibt so viel zu entdecken.
    Vielleicht finde ich einen Weg, dir deine optimierte sinus_lut_stars zukommen zu lassen.
    Liebe Grüße und Happy Retro Computing!

  2. journal-admin

    Mittwoch, September 30, 2026 um 00:39:05

    @n1troX

    😮 Hm, jetzt bin ich natürlich mega-neugierig darauf welche Änderungen du vorgenommen hast.
    Wenn ich raten müsste würde ich - ausgehend davon dass Du schreibst, dass Du das Basic-Rom abschaltest - vermuten, dass Du eine eigene Routine zur Erstellung der Sinustabelle verwendest.
    Infrage kämen da die Ausnutzung von Symmetrie (Viertelsinus extern vorausberechnen und dann den Rest spiegeln), gar keine Sinusfunktion, sondern eine Parabelnäherung mit Ganzzahlen, oder Rekursionsformel (Festkomma-Arithmetik).
    Und ja, das hätte (nicht so sehr in meinem kleinen Programm, aber bei größeren Projekten) durchaus mehrere Vorteile. Man ist nicht mehr vom Basic-ROM abhängig, sondern kann den Speicher für eigenen Code nutzen und bei der Parabellösung wäre in der PRG gar keine Tabelle mehr vorhanden, sondern nur der Code zur Erstellung (kleinere Datei). Am raffiniertesten wäre die Verwendung der Rekursion. Aber auch am Schwersten für Anfänger wie mich.

    Ich habe jetzt auf Anhieb auch keine Lösung parat wie du mir deinen Code zuschicken könntest. Vielleicht per Email? Die kannst Du dir auf meinem YT-Kanal einfach in der Kanalbeschreibung rauspicken.
    Oder Du teilst mir einfach mit, welche Methode Du verwendet hast. Den Rest lasse ich mir dann von der K.I. erklären. Ich bin auf jeden Fall sehr gespannt! Bis dann. Lass es dir gut gehen!

Kommentar hinzufügen

Die Felder Name und Kommentar sind Pflichtfelder.