Hallo Besucher, der Thread wurde 45k mal aufgerufen und enthält 204 Antworten

letzter Beitrag von 8R0TK4$T3N am

Projekt Caren 2

  • War echt ganz cool in Berlin ;-)
    Stefan Höltgen sprach mich Anfang des Jahres mal speziell auf den Aspekt 'Spiel im Spiel' bei Caren an. Also die Implementierung des Pong Spiels welches man in Caren spielen kann am Atari 2600.
    (ein World First, hehe :)
    Ich hatte dazu einige Sachen rausgesucht und beschrieben und gebe das auch gerne hier einmal wieder gekuerzt.
    Die specs zu den SFX kommen direkt hierher:
    http://cs.au.dk/~dsound/Digita…enfoot/Pong.dir/Pong.html


    Ich las diesen Artikel und das triggerte erst die Idee, das als Spiel-im-Spiel umzusetzen.
    Ich draengte Jammer dazu, die 100% so umzusetzen - trotz der extremen Zeitnot zur deadline damals.
    Wenn man Kamil kennt weiss man, dass er da auch Perfektionist ist, so dass er dazu noch einen Filter
    drumbastelte damit es klingt als kaeme es aus SO EINEM TV-Lautsprecher wie im Spiel ;-)
    Also alles hochwissenschaftlich, versteht sich :)


    MrSid zeigte sich von Anfang an interessiert an unserem Caren-Projekt. Die ein oder andere Sache
    hatte ich mit ihm bereits diskutiert und ich fragte, ob er ein 'Pixel Pong' als Gast-Auftritt coden moechte
    in hires chars als 4x3 Feld (Ja, 4:3 Ratio MUSS sein natuerlich ;-)
    Meine Vorgabe war diese Char-Anordnung
    0123
    4567
    89ab
    und eben separate Aufrufe 'pro Spielframe'. Das heisst ich muss die Pong-Engine einpaar pro Frame aufrufen
    koennen und sie darf dann nicht mehr als vielleicht ein Drittel des Frames verbraten an Zeit.


    MrSid machte daraus ein
    0369
    147a
    258b


    Hielt sich aber sonst komplett an die Vorgabe ;-)
    Die Caren Engine startet pro Raum ein Script.


    Hier ist ein laenglicher Ausschnitt aus diesem Script. Kuerzungen markiere ich mit "(...)"
    Das ist einer der ersten Raeume und das Script etwas unaufgeraeumter als spaeter.


    script_caren1f.sh:

    name: tv
    box: 121 42 179 78
    scripts: _scp_handle_tv _lookattv _scp_handle_tv _deny
    variables: on=0 null2=0 null3=3 frame=0


    name: vcs
    box: 143 88 168 103
    scripts: _lookvcs _lookvcs _vcs_use _scp_handle_vcs
    variables: cart=0 score=0 joy=0 frame=0
    (...)
    _init_room:
    .asm .byte $ff
    call _load_stuff
    set game_control 0
    set game_play_tune 0
    setcurrent tv
    setcurrent fridge
    setcurrent vcs
    setcurrent oven
    setcurrent trapdoor
    set game_num_objects_in_room 9
    set game_box_num 10
    set game_bg_col0 9
    set game_bg_col1 12
    set game_bg_col2 0
    set game_npc_x 0
    set game_mask_active 1
    set game_mask2_col 12
    set game_mirror_active 0
    set game_mirror_col 6
    launch _watch_masks every 3 frames in 3 frames
    place_caren 82 103
    set game_current_box 1
    call game_face_se
    set game_control 1
    if fridge_open reconfig_area fridge 296 89 319 115
    if oven_open reconfig_area oven 214 111 239 118
    if not oven_open reconfig_object pot click none
    if oven_open reconfig_object pot click _lookpot
    if pot_taken reconfig_object pot click none
    if trapdoor_open enable_exit trapdoor
    if trapdoor_open reconfig_area trapdoor 1 1 51 40
    if oven_open reconfig_area oven 212 111 239 125
    break for 3 frames
    call game_screen_on
    end
    (...)
    _vcs_use:
    if vcs_cart goto label_playthe2600
    ifown cartridge goto label_owning
    say "Irgendwo habe ich noch ein Modul.",0,"I have a cartridge for it somewhere.",0
    end
    #
    label_owning
    say "Noch steckt kein Modul drin.",0,"I need to plug a cartridge in first.",0
    end
    #
    label_playthe2600
    if not vcs_joy goto label_joystickmissing
    goto _play_pong
    end
    #
    label_joystickmissing
    say "Ohne Joystick ist das doof.",0,"There's no joystick, that's no fun.",0
    end


    _play_pong:
    walk2xy 180 122
    keep_walking
    say "Ok, 5 Punkte mache ich mal schnell.",0,"Ok, I'll have a quick 5 point game.",0
    keep_listening
    set game_control 0
    call game_face_nw
    break for 3 frames
    set game_current_mood 2
    animate tv every 1 frames: 7
    break for 3 frames
    /asm-------------
    lda #$00
    sta $5800+6*40+16+$0
    lda #$03
    sta $5800+6*40+16+$1
    lda #$06
    sta $5800+6*40+16+$2
    lda #$09
    sta $5800+6*40+16+$3
    lda #$01
    sta $5800+6*40+16+$28
    lda #$04
    sta $5800+6*40+16+$29
    lda #$07
    sta $5800+6*40+16+$2a
    lda #$0a
    sta $5800+6*40+16+$2b
    lda #$02
    sta $5800+6*40+16+$50
    lda #$05
    sta $5800+6*40+16+$51
    lda #$08
    sta $5800+6*40+16+$52
    lda #$0b
    sta $5800+6*40+16+$53
    \asm------------
    .asm jmp playlabel
    .asm #include "tvpong3.asm"
    .asm playlabel
    /asm------------------
    lda #11
    sta Player1Y
    sta Player2Y
    lda #$00
    sta PointsPlayer1
    sta PointsPlayer2
    jsr initBall
    \asm------------------
    .asm label_mainp
    .asm jsr updatePong
    .asm jsr renderPong
    .asm inc FrameCount
    .asm lda PointsPlayer2
    .asm sta caren1f_vcs_score
    break for 1 frames
    animate tv every 1 frames: 7
    if vcs_score<5 goto label_mainp
    set game_control 1
    say "Geschafft! Ich schalte wieder aus.",0,"Great! I'd better turn it off again.",0
    keep_listening
    say "Diese Version war von_",0,"This version was by_",0
    keep_listening
    say "Andreas Varga.",0,"Andreas Varga.",0
    /asm-------------
    ldx #$00
    stx $5800+6*40+16+$0
    inx
    stx $5800+6*40+16+$1
    inx
    stx $5800+6*40+16+$2
    inx
    stx $5800+6*40+16+$3
    inx
    stx $5800+6*40+16+$28
    inx
    stx $5800+6*40+16+$29
    inx
    stx $5800+6*40+16+$2a
    inx
    stx $5800+6*40+16+$2b
    inx
    stx $5800+6*40+16+$50
    inx
    stx $5800+6*40+16+$51
    inx
    stx $5800+6*40+16+$52
    inx
    stx $5800+6*40+16+$53
    \asm------------
    animate tv every 1 frames: 5 6 5 6 5 5 6 6 6 5 5 6 5 5 6 5 6 6 5 6 6 6 5 5 6 5 0
    end


    ------------------------------------------------------
    Interessant in Bezug auf Pong ist es ab hier:



    _play_pong:
    walk2xy 180 122
    keep_walking
    say "Ok, 5 Punkte mache ich mal schnell.",0,"Ok, I'll have a quick 5 point game.",0
    keep_listening
    set game_control 0
    call game_face_nw
    break for 3 frames
    set game_current_mood 2
    animate tv every 1 frames: 7



    Klartext:
    Caren geht zu x,y = 180,122
    Die Engine wartet bis sie dort ist (andere Dinge im Hintergrund laufen aber weiter, nur DIESES Skript wartet)
    Caren sagt etwas
    Die Engine wartet bis sie fertig ist damit
    Der Spieler verliert die Kontrolle (Cursor aus)
    Caren schaut nach nord-west
    Mini-Pause.
    Caren freut sich (auch wenn man das von hinten nicht sieht ;-)
    Der TV-Bildschirm wird auf frame 7 animiert (siehe g_caren1f_tv_4-3.png)


    Dann kommt etwas inline assembler, ganz stumpf um die char-matrix nach MrSids vorgaben anzulegen.
    Hinterher setze ich sie wieder zurueck, so dass die uebrige TV Animation passt.
    Das ginge eleganter, aber so ging es damals schnell und ich habe das nicht mehr veraendert danach ;-)
    Zur Deadline war ja massiv Stress.


    .asm jmp playlabel
    .asm #include "tvpong3.asm"
    .asm playlabel


    Hier wird der komplette pong-code in inline assembler eingebungden und uebersprungen.


    Es folgt die PONG game loop:
    .asm label_mainp
    .asm jsr updatePong
    .asm jsr renderPong
    .asm inc FrameCount
    .asm lda PointsPlayer2
    .asm sta caren1f_vcs_score
    break for 1 frames
    animate tv every 1 frames: 7
    if vcs_score<5 goto label_mainp



    Das "break for 1 frames" ist der Clou dabei.
    Dazu gleich.
    Die Loop laesst sich direkt lesen denke ich.
    Ich schrieb einen eigenen Scriptcompiler fuer Caren. Das ist keine VM sondern Caren fuehrt direkten 6502-code aus in Scripten.


    Spannend ist zB das hier:
    "break for 1 frames" wird compiliert zu:


    ldx #<(*+9)
    ldy #>(*+7)
    lda #01 ;in that many frames
    jmp _break


    X,Y werden auf die Stelle NACH dem jmp _break gesetzt.
    Einmal +9, einmal +7 weil ja ein LDX #XX dazwischen kommt :)


    _break ist entscheidend und sieht so aus (Teil der Engine selbst).


    _break
    .(
    sta frames_delay
    stx script_data+5 ;X
    sty script_data+6 ;Y
    jsr find_free_slot
    lda #1
    sta slots_msb,x
    frames_delay=*+1
    lda #1
    sta slots_lsb,x
    txa
    asl
    asl
    asl
    tax
    ldy #0
    loop
    lda script_data,y
    sta slots_data,x
    inx
    iny
    cpy #8
    bne loop
    end
    rts


    script_data ; a x y
    .byte 0,0,<_enter_script, >_enter_script, 9, 9, 9 ,0
    .)

    ;---------------------------------------
    _enter_script
    .(
    stx target+1
    sty target+2
    target
    jmp $babe
    .)


    Break sucht also einen freien multi-threading slot und setzt dann die funktion _enter_script
    dort hinein. Sie hat zwei benutzte Parameter, naemlich einen 16bit pointer auf genau die Stelle nach jmp _break.
    Das ganze passiert nach frames_delay frames welches im Akku an _break uebergeben wird.


    Im naechsten frame arbeitet die Engine also alle 32 Slots ab, zaehlt den 16bit counter runter, trifft nach frames_delay
    auf einen unterlauf, fuehrt den slot aus und laedt dabei A,X,Y so wie es im Slot gesetzt wurde.
    So wie ich _break umgesetzt habe, beleiben also keine CPU-Flags oder Register erhalten.
    Das war zuerst drin, erwies sich dann aber in der Praxis als unnoetig - der grosse Vorteil wenn man in Assembler programmiert.
    In C wuerde man sich das nicht trauen ;-)


    Nach 5 Punkten erhaelt der Spieler die Kontrolle zurueck, Caren sagt ihren Text, die Charmatrix fuer den TV wird
    wieder zurueckgesetzt und
    animate tv every 1 frames: 5 6 5 6 5 5 6 6 6 5 5 6 5 5 6 5 6 6 5 6 6 6 5 5 6 5 0
    Flippt zwischen den beiden noise-frames 5 und 6 (siehe wieder g_caren1f_tv_4-3.png) hin und her.
    Das g_ in g_caren1f_tv_4-3.png verraet meiner Engine, dass es eine Animation ist die pro Frame ein eigenes Farbram mitbringt,
    so dass ich white noise und natuerlich Pong in hires darstellen kann.


    Ein wichtiges Detail ist diese Zeile im Script:
    animate tv every 1 frames: 7
    nach:
    .asm jsr updatePong
    .asm jsr renderPong


    Das heisst die Pong-Engine plottet nicht in den Bildschirm, sondern in reserviertes RAM.
    Erst danach wird dieses Speicherbereich dann in den aktuell angezeigten Zeichensatz kopiert.
    Das erledigt: animate tv every 1 frames: 7
    D.h. heisst soviel wie, fuelle diejenigen Zeichen welche per Maske fuer den 'TV' reserviert sind mit dem Inhalt des 7. Frames und zwar im naechsten
    Engine-Frame, d.h. also sofort (das ist das 'every 1').
    Die Zeile: animate tv every 1 frames: 5 6 5 6 5 5 6 6 6 5 5 6 5 5 6 5 6 6 5 6 6 6 5 5 6 5 0
    wechselt also in 50Hz zwischen Fraems 5 und 6 (das ist das statische Rauschen) und am Ende zeigt es Frame 0, das entspricht dem ausgeschalteten Fernseher.
    (siehe auch angehaengtes PNG)


    Vielleicht interessiert diese Herangehensweise oder ein wenig Hintergrund zu Caren ja ;-)

  • Sehr interessant. Vielen Dank für die schöne Erklärung, Enthusi.

    Das ist keine VM sondern Caren fuehrt direkten 6502-code aus in Scripten.

    Eine Frage hätte ich aber noch, wenn's erlaubt ist: Wie ist das obige Zitat zu verstehen? Bedeutet dies, daß das komplette Script von Deinem Compiler nach 6502 kompiliert wird, indem z. B. ein Befehl wie "CALL _load_stuff" umgewandelt wird in "JSR _load_stuff"? Für diesen Fall, aber auch für den inline-Assemblercode taucht dann nämlich die Frage auf, ob der verwendete Code für eine absolute Adresse erzeugt wird oder relokatibel angelegt ist (z. B. mittels Header, der die zu patchenden Befehle enthält wie beim AmigaOS). Und sollte mal irgendwann jemand ähnlich wie bei SCUMM auf die Idee kommen, einen Caren-Interpreter für andere Maschinen zu schreiben, müßte der dann nur einen 6502-Emulator schreiben oder bedarf es einer gesonderten Interpreter-Engine? ^^

  • Caren-Interpreter für andere Maschinen

    Caren für Z80 :schreck!: ? Oob, komm schnell! :D

  • Für jeden Raum entsteht am Ende ein komplettes Assemblerlisting. Auch alle globalen Variablen sind dort via label eingebunden. Zu dieser Zeit ist das also noch voll relativ an jede Stelle assemblierbar. Im Spiel selbst wurde es aber absolut für DAS Scriptoffset im Spiel aSsembliert. Für z80 würde man keinen 6502 emu bauen, sondern die scripte in z80 compilieren. Allerdings verwende ich bei Bedarf direkt 6502 inline Assembler auch im script. Daher die enorme Flexibilität gegenüber einer VM. Meist sind das aber geschlossene Effekte oder Routinen die man dann separat in z80 umsetzen müsste und könnte. z80 hab ich mir ja mal beibringen müssen :-)

  • Ich BIN kein häßlicher Bug...häßlich OK, aber sechs Gliedmaßen habe ich nicht!

    Du heißt ja auch sicherlich nicht Gregor Samsa :D

    Heute konnte ein haesslicher bug der zwar nur einmal auftrat aber dennoch sehr stoerte gefixt werden \o/

    Es geht fleißig voran :ilikeit:

  • Gestern kam meine ReGame Ausgabe an ;-)
    Sehr schoen geworden, freut mich.
    Recht ausfuehrlich zu technischen Details bei Caren aber auch nette Interviews von anderen. Z.B Peiselullis und Vetos Donkey Kong!

  • Habe ein kleines Tool/GUI gecodet um das Einbinden neuer Raeume zu erleichtern.
    Ich hatte das Gefuehl, dass wir es noch sehr oft brauchen werden ;-)
    Vor allem die Verbindungen der Tueren aber auch wo Caren steht und ich welche Richtung sie schaut abhaengig davon woher sie kam.
    Die Animationsframes der Tueren werden auch gleich mit beruecksichtigt.

  • Klasse :thumbup:
    Ich denke mal, dass die Entwicklung damit um einiges beschleunigt wird, oder?


    Wenn Caren II dann mal fertig ist, könnte man ja ein Adventure-CK daraus machen ;)

  • Hm. Bin ich persönlich kein Freund von. Das ganze Steuerungskonzept ist auf direkte Interaktion mit Objekten und 4-Wege-Menüführung ausgelegt. Das müsste man komplett auf links drehen, wenn das sich halbwegs gut anfühlen soll und ich sehe dort kaum Mehrwert bis auf zusätzliche Einschränkungen bei den Ressourcen.


    Bzw was wir in der Entwicklung bereits vorantreiben, ist eine Verschlankung der Steuerung. Der Doppelklick ist beispielsweise nicht mehr notwendig. Hotspots bekommen etwas Spielraum. Solche Dinge...