Hello, Guest the thread was viewed4.8k times and contains 36 replies

last post from BIF at the

BASIC und Assembler mischen

  • Moin,


    ich frage mich schon länger, wie man früher in Spielen BASIC und Assembler gemischt hat, bspw. in Pirates!, Rings of Medusa, Spirit of Adventure oder Die Dunkle Dimension - also Spielen, wo ein großer Teil der Spielelogik wirklich in BASIC geschrieben war, die Spiele aber nicht so aussahen. Meine Idee wäre:

    • Es gibt einen Assembler-Teil mit verschiedenen Routinen, die über SYS-Befehle aufgerufen werden.
    • Dieser Assembler-Teil wird am Anfang des BASIC-Programms über LOAD geladen.

    Daraus ergeben sich aber einige Fragen für mich:

    1. Wie bindet man Schriftarten, Sprites, etc. ein? Das läuft wahrscheinlich über die Assembler-Routinen, nehm ich an?
    2. Wird der BASIC-Speicher dadurch nicht extrem klein?
    3. Hat man ggf. mehrere BASIC-Programme die je nach Bedarf geladen und ausgeführt werden?
    4. Wird für so etwas auch ein BASIC-Compiler eingesetzt?

    Vielen Dank schonmal für eure Antworten.

  • Eine typische Möglichkeit war, dass man jede Menge DATA Zeilen hatte die dann per Poke in den entsprechenden Bereich geschrieben wurden. Die Adresse $C000... bietet sich ganz gut für Assemblerroutinen an.

    Das funktioniert aber nur bei kurzen Assembler-Routinen. Ansonsten wird das ziemlich lange dauern. :)

    Schau mal bei Pirates rein, das dürfte einige Fragen beantworten.


    https://www.c64-wiki.de/wiki/Pirates!#Sonstiges

    Hab ich schon. Und ich hab mir auch das Pirates!-Video von 8bit Show&Tell angeschaut, wo er den Quellcode zeigt und Stellen daraus erklärt. Da sieht man auch SYS-Befehle, also lieg ich nicht so verkehrt. Aber ich seh das gerade aus Planungs- und Optimierungssicht und da fällt mir mindestens ein Problem bei auf:


    Wenn ich eine Assembler-Bibliothek schreibe und die verschiedenen Routinen über SYS-Befehle ausführe, was passiert, wenn ich einen Bug in der Bibliothek habe? Dann verschieben sich die ganzen Routinen und ich darf den Quellcode nach SYS-Befehlen durchsuchen und den Code verändern. Alternativ könnte ich für diese Routinen Blöcke im Speicher reservieren, so dass bspw. für jede Routine 256 Byte zur Verfügung stehen - oder weniger oder mehr - je nach Routine. Dann hätte ich am Ende eine Memory-Map, wo diese Befehle liegen und damit eine Liste von SYS-Befehlen, die ich verwenden kann.


    Ich finde es gerade sehr anstrengend so ein Programm zu planen. Heute programmiert man einfach drauf los und schaut, was am Ende rauskommt. Bei einem reinen BASIC- oder Assembler-Programm stell ich mir das auch recht einfach vor - in der Planung wohlgemerkt. Aber sobald ich beides mische, seh ich da einen echt großen Aufwand - oder ich denke irgendwas falsch.

  • Wenn ich eine Assembler-Bibliothek schreibe und die verschiedenen Routinen über SYS-Befehle ausführe, was passiert, wenn ich einen Bug in der Bibliothek habe? Dann verschieben sich die ganzen Routinen und ich darf den Quellcode nach SYS-Befehlen durchsuchen und den Code verändern. Alternativ könnte ich für diese Routinen Blöcke im Speicher reservieren, so dass bspw. für jede Routine 256 Byte zur Verfügung stehen - oder weniger oder mehr - je nach Routine. Dann hätte ich am Ende eine Memory-Map, wo diese Befehle liegen und damit eine Liste von SYS-Befehlen, die ich verwenden kann.

    In dem Fall würde ich eine Sprungleiste an den Anfang deiner Assemblerroutinen legen. Die bleibt dann von Basic aus gesehen unverändert. Sind jedenfalls unabhängig von der Länge deiner Assembler-Routinen.

  • Wenn ich eine Assembler-Bibliothek schreibe und die verschiedenen Routinen über SYS-Befehle ausführe, was passiert, wenn ich einen Bug in der Bibliothek habe? Dann verschieben sich die ganzen Routinen und ich darf den Quellcode nach SYS-Befehlen durchsuchen und den Code verändern. Alternativ könnte ich für diese Routinen Blöcke im Speicher reservieren, so dass bspw. für jede Routine 256 Byte zur Verfügung stehen - oder weniger oder mehr - je nach Routine. Dann hätte ich am Ende eine Memory-Map, wo diese Befehle liegen und damit eine Liste von SYS-Befehlen, die ich verwenden kann.

    In dem Fall würde ich eine Sprungleiste an den Anfang deiner Assemblerroutinen legen. Die bleibt dann von Basic aus gesehen unverändert.

    Oh, du meinst, dass ich an den Anfang der Assembler-Bibliothek eine Menge JMPs packe, die dann auf die eigentliche Routine springen? Und dann muss ich nur noch SYS49152, SYS49155, SYS49158, etc. nutzen?

  • Exakt.

  • Wo genau ist denn jetzt das Problem ?

    Videospeicher in den Bereich $C000 - $FFFF legen.

    In diesen Bereich dann Zeichensatz und Sprites reinlegen.

    Für einzelne Unterprogramme in Assembler den Bereich $8000-$BFFF nutzen und mit SYS xxxx von Basic aus anspringen.

    Der Speicher von $0801 - $7FFF für Basic nutzen. Sollte wohl reichen 🤷‍♂️

    Wenn das Programm fertig ist alles mit einem Compacker-Programm zusammenklatschen, das ein File raus wird.

    Z.B. ECA Compacker von Anno Klick.

  • Das funktioniert aber nur bei kurzen Assembler-Routinen. Ansonsten wird das ziemlich lange dauern. :)

    Kurz ist relativ.In den diversen Zeitschriften gab es endlos lange DATA Wüsten zum Abtippen, das waren keine kurzen Assembler Routinen sondern richtige Programme. Und wenn man das startet, dann wartet man eben eine Zeitlang bis das durchgelaufen ist. Gab deshalb auch welche bei denen das fertige Programm dann auf Disc geschrieben wurde, damit man nicht jedesmal warten musste.

    Ansonsten - was Hucky oben schrieb. Die Speicherbereiche entsprechend anlegen und dann zusammenpacken.

  • Man verwendet Assemblerprogramme in Kombination mit Basic Code im Normalfall als Unterstützung.

    So sind Zeitkritische Interrupt Routienen anderst nicht möglich oder Routienen zur Unterstützung für den Basic code.


    Einfache Beispiele sind da z.b. Anzeigen einer Zeichensatz Map auf dem Bildschirm, was per Basic zu lahm ist.

    Am ende hat man dann ein paar Wenige SYS Befehle die eine Routiene Aufrufen.

    Da sollte man den Überblick locker behalten können.


    Ich habe meine Programme früher auf 2-3 Extra Dateien aufgeteilt, die nachgeladen werden.

    z.b. Zeichensatz Scrolling (für wasser) als IRQ , Ein Bildschirmscrolling , Extra Zeichensatz & Die Spieldaten (Map)


    Dabei kommt man im Endeffekt auf eine Hand voll Routienen.

  • Heute programmiert man einfach drauf los und schaut, was am Ende rauskommt.

    Berufsprogrammierer machen das im Allgemeinen nicht so, weder damals noch heute. Ich vermute stark, dass auch bei Pirates ein Plan vorlag, welche Funktionalität man in Assembler implementieren wollte und welche in Basic. So hätte man z.B. erst den Assemblerteil komplett fertigstellen und testen können, und dann den Basicteil in einem zweiten Schritt.

  • Selbstverständlich kann man auch Ass-Routinen in einem Basic-Programm benutzen und mit SYS aufrufen.

    a) Installation mit Datas
    b) Installation in Stings sys string.
    c) Geladen werden: z.B. ins obere RAM ab 49152 oder ins String-RAM.
    d) als Anhang ans Basic-Programm angehangen werden.
    e) sogar die Installation inerhalb des Basic-Quelltext ist möglich.

    Schönen Gruß.

  • was soll denn das String-RAM sein ?! 🤔

  • Wird der BASIC-Speicher dadurch nicht extrem klein?

    Nein, denn der Basic-Speicher selbst ist ja eher ziemlich klein und läßt viel Ram des C64 ungenutzt.

    Da wäre zuerst das Ram von $c000..$cfff (49152-53247), auf das man auch von Basic aus direkt zugreifen kann, und das sich daher gut eignet, um hier einen Programmcode mit Sprungliste und/oder einen neuen Textschirm unterzubringen. Letzteres verwendet man gerne, wenn man a) einen eigenen Zeichensatz oder b) viele Sprites benutzen möchte.

    Diese werden dann in den Bereich $d000..$ffff gelegt unter den IO-Bereich und das Kernal-Rom. Zugriff darauf hat man dann lesend zwar nur noch per Assembler, da es sich aber meistens um konstante Daten handelt, ist dies nicht weiter schlimm.

    Neben dem Bereich bei $c000..$cfff gibt es aber auch noch den Speicher zwischen $a000 und $bfff (40960-49151), d. h. unter dem Basic-Rom. Von Basic aus lassen sich in dieses Ram zwar nur Werte hineinschreiben, aber ein Assemblerprogramm kann relativ leicht Werte auch daraus lesen. "Relativ leicht" bedeutet, daß für das Lesen lediglich das Basic-Rom weggeschaltet werden muß, wohingegen das Kernal-Rom aktiviert bleibt. Es ist folglich nicht notwendig, die Interrupts zu sperren. Diese zusammenhängenden 8 kb Ram eignen sich gleichermaßen gut für Programmcode als auch große Daten wie z. B. eine Map oder Wortlisten. So hatte ich beispielsweise früher mal ein Adventure programmiert, welches einen Parser in Assembler hatte, der bei $c000 lag, und das recht große Vokabular nach $a000 verlagerte. Diese Verteilung hat das Basic-Programm sehr entlastet, da Strings wie z. B. Wortlisten sehr viel Platz im Basic-Speicher verbrauchen, der dadurch frei bleiben kann für weitere Programmlogik.

    Mit einer Mischung aus Basic und Assembler läßt sich sehr viel erreichen. Für viele Spiele wie Rollenspiele, Adventures oder Strategiespiele ist diese Kombination auch ausreichend. Jedoch stellt sie eine große Gefahr dar: Man unterliegt schnell der Versuchung, immer mehr Routinen nach Assembler zu verlagern, bis man vor der Frage steht, ob man überhaupt noch Basic verwenden soll. ^^

  • Berufsprogrammierer [...]

    Na danke. :P Bin halt bloß Webentwickler. :D Aber da ich weder am C64 beruflich programmiert habe und ich denke, dass auch bei vielen Indiegame-Entwicklern die Planung (anfangs) hinten an steht, ist mein Punkt schon valide.


    Aber beim Rest hab ich mir gedacht, dass das so gemacht wurde. Ich hatte auch überlegt, dass ich erstmal schaue, welche Assembler-Routinen ich brauchen könnte. Da fand ich den Hinweis von detlef mit den JMP-Listen schon sehr hilfreich.


    Ich habe meine Programme früher auf 2-3 Extra Dateien aufgeteilt, die nachgeladen werden.

    z.b. Zeichensatz Scrolling (für wasser) als IRQ , Ein Bildschirmscrolling , Extra Zeichensatz & Die Spieldaten (Map)

    Waren das zufällig Rollenspiele? Klingt gerade so.

  • Mit einer Mischung aus Basic und Assembler läßt sich sehr viel erreichen. Für viele Spiele wie Rollenspiele, Adventures oder Strategiespiele ist diese Kombination auch ausreichend. Jedoch stellt sie eine große Gefahr dar: Man unterliegt schnell der Versuchung, immer mehr Routinen nach Assembler zu verlagern, bis man vor der Frage steht, ob man überhaupt noch Basic verwenden soll. ^^

    Tatsächlich geht es mir auch um Rollenspiele. Deshalb hatte ich bspw. "Die Dunkle Dimension" in meinem Eingangspost erwähnt. So etwas würde ich gerne bauen. Ich hab mir in den vergangenen Wochen einige alte DSA-Boxen in der Bucht geschossen, um mich inspirieren zu lassen. Und auf Twitter wurde ich dann scherzhaft gefragt, warum ich das nicht für den C64 mache. Aktuell wüsste ich aber null, wie ich das umsetzen soll, gerade was das Scrolling oder die Map-Daten angeht. Deshalb wollte ich mal die Forum64-Schwarmintelligenz nutzen.

  • Bei einem Rollenspiel läuft es doch (auf dem C64) eher auf ein von "Raum" zu "Raum" gehen hinaus. Scrolling ist da eher wenig (also nix). Wobei es auch (wenige) andere Beispiele gibt (die mir vor dem Auge herumflattern aber wo mir der Name nicht einfällt). Das sind dann aber echte "Großprojekte". So ein von "Raum" zu "Raum" wollte ich mal für DSA umsetzen, das Wollen ist aber auch schon über 30 Jahre alt... :-(

  • Tatsächlich geht es mir auch um Rollenspiele.

    Da Du DDD erwähnst, gehe ich davon aus, daß Du die Ultima-Darstellung meinst, bei der die Landschaftsanzeige aus 11x11 Kacheln besteht, wobei jede Kachel 2x2 Zeichen groß ist. Das Landschaftsscrollen erledigst Du am besten in Assembler. Ich meine mich zu erinnern, daß DDD für die Berechnung der Sichtbarkeit der Kacheln ebenfalls auf eine Assemblerroutine zurückgegriffen hat. Die Frage wäre, wie gut Deine Assemblerkenntnisse sind, um solch eine Scrollroutine zu schreiben. (Besonders schwierig ist es nicht.)

    Was die Speichereinteilung anbelangt, so würde ich Dir zu dem oben genannten Modell raten, d. h. die Landschaft wird ab $a000 abgelegt. Bei $cc00 befindet sich der neue Zeichenspeicher (Textram) und ab $e000 könnte man einen neuen Zeichensatz ablegen. (Ich vermute mal, Du möchtest wie bei DDD den Zeichensatzmodus verwenden trotz aller Einschränkungen und nicht wie Ultima die Hires-Bitmap-Darstellung.)

    Normalerweise wird bei solchen Rollenspielen die Karte beschränkt auf eine Größe von 32x32 oder 64x64, wobei ein Byte einer bestimmten Kachel (wie z. B. Mauer oder Wald) entspricht. Für eine Karte von 32x32 benötigt man folglich 32*32=1024 Bytes = 1 kb. Oder halt 64*64 = 4096 Bytes = 4 kb.

    Große Landschaften wie z. B. die Außenwelt würde man in verschiedene Karten zerstückeln. Die einzelnen Kartenteile werden dann automatisch nachgeladen, wenn die Spielfigur eine Karte verläßt. Hierbei muß rundherum ein Rand definiert werden von 5 Kacheln, die zwar angezeigt, aber nicht betreten werden können. Dieser Rand ist identisch mit den ersten 5 Kacheln der umliegenden Karten, d. h. an dieser Stelle überlappen sich die Karten. Wird die Spielfigur in die Randzone bewegt, wird die neue Karte geladen, die ihrerseits eine Randzone von 5 Kacheln hat, die der alten Karte entsprechen. (Ich hoffe, das war klar genug ausgedrückt... :schande:)

    Bei DDD wird zu jeder Stadt auch stets ein neues Basicprogramm geladen, weshalb die Tastenkommandos auch unterschiedlich sind. Das gesamte Spiel besteht also aus jeder Menge einzelner Basicprogramme. Da sollte man sich vorab genau überlegen, welche Programmteile in jedem Basicprogramm vorkommen, und ob man diese nicht vielleicht in Assembler schreibt und in eine Programmbibliothek packt, die dann bei $c000 liegt.

    Was die Ausgabe von Text auf dem Bildschirm betrifft, so kann man die Zeiger für den Textspeicher in der Zeropage etc umändern, so daß die Kernal-Zeichenausgabe auch für den verlegten Textspeicher bei $cc00 verwendet werden kann. Es fragt sich jedoch, ob nicht auch hier eine Zeichenausgabe in Assembler die bessere Wahl ist, da man hierbei gleichzeitig einen automatischen Wortumbruch einbauen kann, so daß man sich später beim Programmieren von Textnachrichten nicht mehr darum kümmern muß. Schreibt man die Ausgabe in Assembler, so bietet es sich an, einer Variablen in Basic, z. B. S zu Beginn des Programms den Wert der Sprungadresse in der Sprungliste für diese Routine zuzuweisen. Im Programm selbst braucht man dann nur noch zu schreiben SYS S"Text...", und die Assemblerroutine liest direkt aus dem Basic-Programm die Textnachricht heraus und schreibt sie auf den Bildschirm.

    Aber da ich weder am C64 beruflich programmiert habe und ich denke, dass auch bei vielen Indiegame-Entwicklern die Planung (anfangs) hinten an steht, ist mein Punkt schon valide.

    Davon möchte ich dringend abraten. Ein Rollenspiel ist schon ein großes Programm. Ohne ausreichende Planung vorab geht da gar nichts. Es gibt mehrere Sachen, die Du unbedingt geklärt haben mußt, bevor Du auch nur eine Zeile programmierst:

    - Was genau soll Dein Programm machen. Welche Tastendrücke/Menüs soll es geben? Was soll passieren?

    - Wie sieht die Anzeige aus? Wo steht was auf dem Bildschirm?

    - Wie sehen die Algorithmen aus für die Kämpfe? Welche Spielmechanik soll verwendet werden?

    - Welche Ausrüstungsgegenstände gibt es? Welche Charakterattribute? Was bewirken sie?

    - Welche weitere Elemente sollen verwendet werden? Nahrung wie bei Ultima? Tag- und Nachtzyklus? Eingeschränkte Sichtbarkeit der Landschaft?

    - Wie ist die Landschaft organisiert? Gibt es nur eine große Karte oder wird beim Betreten einer Stadt etc in die Karte gezoomt?

    - Welche Gegner gibt es? Wie werden diese gehandhabt? Wann erscheinen sie? Wie verhalten sie sich?

    - Wie funktioniert die Interaktion mit NPCs? Erscheinen bei Kontakt fertige Menüs, z. B. zum Kaufen von Gegenständen oder zur Auswahl von bestimmten Fragen, oder kann man wie bei Ultima Stichwörter eingeben, auf die die Charaktere reagieren?

    - Was passiert mit Gegenständen z. B. Schatztruhen oder Pferden, die vom Spieler nicht mitgenommen werden? Verbleiben diese am Ort? Falls ja, wie wird das gespeichert? Oder verschwinden sie nach einiger Zeit?

    Und noch viele weitere Grundfragen mehr. Und wie man sieht, sind dies nur technische Fragen und keine Fragen zum (Handlungs)Inhalt des Rollenspiels.


    Nebenbei: Routinen, die man in Assembler schreiben sollte (aus Gründen der Geschwindigkeit):

    - Kopieren der Karte in den Landschaftsausschnitt auf dem Bildschirm. Hierbei müssen die Kachelwerte aus der Karte in die 2x2-Kacheln umgewandelt werden.

    - Falls gewünscht: Eingeschränkte Sichtbarkeit

    - IRQ für Animation des Zeichensatzes (z. B. Wellen oder Feuer)

    - Scrollen des Textausgabefensters

    - Weitere häufig genutzte Routinen für die Ein- und Ausgabe wie z. B. Einlesen eines Strings oder Ausgabe eines Textes auf den Bildschirm.

    Wie gesagt, am besten packt man eine ganze Reihe von Routinen in die Assemblerbibliothek, damit sie a) schnell ausgeführt werden, so daß das Spiel nicht zu sehr lahmt (wie das bei DDD manchmal der Fall sein kann) und b) nur einmal programmiert werden müssen und immer wieder von verschiedenen Basic-Programmen aufgerufen werden können.