2 Punkte von GN⁺ 2024-10-19 | 1 Kommentare | Auf WhatsApp teilen
  • Dieses Tutorial erweitert die in Common Lisp eingerichtete Spieleentwicklungsumgebung um eine ECS-Architektur und metalanguage-orientierte Programmierung anhand eines konkreten Dungeon-Crawler-Beispiels
  • Tiled-XML-Maps werden mit cl-tiled eingelesen; statt die CLOS-Objekte direkt zu verwenden, werden sie in ECS-Komponenten überführt, um Rendering, Kollisionen und Speicherverwaltung zu trennen
  • Tile-Prefabs, Image-Pointer, Parent-Child-Indizes und Finalizer werden kombiniert, um doppeltes Laden und Double Free zu vermeiden und Tiled-Custom-Properties wie Daten zu nutzen
  • Spieler und Gegner führen Bewegung, Animationswechsel und Kollisionsbehandlung über ECS-Systeme aus; Gegner verfolgen den Spieler per A*-Pfadsuche auf Basis von cl-astar und umgehen dabei Wände
  • Mit einer UI auf Basis von Nuklear, narrativen Objekten, Pause-Funktion und Siegbedingung entsteht ein kleiner Dungeon-Crawler mit rund 500 Zeilen Code

Projektstart und grundlegende Ausführung

  • Mit der in Teil 1 behandelten Entity-Component-System-Architektur und metalanguage-orientierten Programmiertechniken wird ein kleiner Dungeon-Crawler mit UI gebaut
  • Ein ausführbares Demo-Binary und der Source Code befinden sich im GitHub-Repository ecs-tutorial-2
  • Die Entwicklungsumgebung setzt die Common-Lisp-Spieleentwicklungsumgebung aus Teil 1 voraus; in der SBCL-REPL wird die Quicklisp-Distribution aktualisiert
    • (ql-util:without-prompting (ql:update-all-dists))
  • Mit dem Template cookiecutter-lisp-game wird ein neues Projekt ecs-tutorial-2 erstellt; im Beispiel wird liballegro als Backend gewählt
  • Nachdem das Projektverzeichnis mit Quicklisps local-projects verknüpft wurde, wird die Fenstergröße in src/main.lisp auf 1280×800 geändert
  • Beim Ausführen von (ql:quickload :ecs-tutorial-2) und (ecs-tutorial-2:main) erscheinen ein schwarzes Fenster in der angegebenen Auflösung und ein FPS-Counter

Tiled-Map und ECS-Storage

  • Für die Erstellung der Dungeon-Map wird der Open-Source-Map-Editor Tiled verwendet
    • Tiled ist ein plattform- und engineübergreifendes Tool und speichert Map-Daten als XML
    • In Common Lisp lädt cl-tiled Tiled-Dateien als Lisp-Objekte
  • Als Beispiel-Tileset wird Dungeon Tileset II - Extended verwendet
    • Da die ursprünglichen 16×16-Tiles klein sind, werden sie mit ImageMagick um 200 % vergrößert und als 32×32-Tiles genutzt
    • level1.tmx und die Tileset-Datei sind über die im Tutorial bereitgestellte Resources.zip erhältlich
  • In ecs-tutorial-2.asd wird die Abhängigkeit cl-tiled ergänzt, und mit src/map.lisp wird eine neue Datei angelegt, um Code zum Laden und Anzeigen der Map zu trennen
  • In src/package.lisp wird cl-tiled unter dem lokalen Nickname tiled registriert

Warum CLOS-Objekte in ECS-Komponenten überführt werden

  • cl-tiled gibt Map-Daten als CLOS-Objekte zurück, was die Erkundung in der REPL erleichtert
  • Werden diese Objekte direkt in der Game Loop verwendet, können die Kosten für Runtime-Dispatch steigen
    • Um ein Fenster von 1280×800 mit 32×32-Tiles zu füllen, werden mindestens 40×25 = 1000 Tiles benötigt
    • In einer separaten Demo fällt die FPS-Zahl auf einem 12-Core Ryzen 5 3600 bei aktiviertem Map-Rendering von 20.000 auf 600
    • Pro Frame kommen etwa 1/600 - 1/20000 = 0.0016 Sekunden hinzu, also mehr als 1,5 ms
  • Werden die von cl-tiled gelesenen Daten in den Storage von cl-fast-ecs überführt, lässt sich Dispatch reduzieren und die CPU-Cache-Nutzung verbessern
  • Die Abhängigkeit cl-fast-ecs wird ergänzt; in init wird ecs:make-storage und in update ecs:run-systems aufgerufen

Map-, Tile- und Prefab-Komponenten

  • map ist eine Tag-Komponente, die die geladene Map-Entity markiert
  • map-tile steht für ein einzelnes Tile und besitzt einen Boolean-Slot obstacle, der angibt, ob es ein Hindernis wie eine Wand oder eine geschlossene Tür ist
  • Die Komponente parent gibt an, zu welcher Map-Entity Tiles und mapbezogene Objekte gehören
    • Im Slot entity wird :index children angegeben, um Child-Entities eines bestimmten Parents schnell zu finden
    • Der Index basiert auf einer Open-Addressing-Hash-Tabelle und bietet im Mittel O(1)-Lookups, verursacht aber Aktualisierungskosten beim Erzeugen und Löschen
  • Zu ecs:*entity-deleting-hook* wird ein Hook hinzugefügt, der beim Löschen einer Parent-Entity auch die über den Index children gefundenen Child-Entities mitlöscht
  • Die Komponente image speichert nur einen C-Pointer auf ALLEGRO_BITMAP
    • Aus dem Tileset-Bild werden mit al_create_sub_bitmap 32×32-Ausschnitte erzeugt und deren Pointer gespeichert
  • map-tile-prefab ist ein Tile-Prefab mit der globalen ID gid eines Tiled-Tiles
    • Für gid wird :index map-tile-prefab :unique t angegeben, um über die ID eine einzelne Prefab-Entity zu finden
    • Die tatsächlichen Tiles auf der Map kopieren etwa das image des Prefabs, besitzen ihre Position aber über eine eigene position-Komponente
  • Der Finalizer von image ruft al_destroy_bitmap nur auf, wenn die Entity ein map-tile-prefab ist
    • Dadurch wird ein Double Free vermieden, da mehrere Map-Tiles denselben ALLEGRO_BITMAP-Pointer teilen
  • position und size speichern Bildschirmkoordinaten und Größen als single-float
    • Da liballegro Bildschirmkoordinaten wegen OpenGL-Kompatibilität als Single-Precision-Floats behandelt, wird dieselbe Vorgehensweise verwendet

Image-Rendering und Laden der Map

  • Das System render-images rendert Entities mit position und image
    • Mit al_hold_bitmap_drawing wird Sprite Batching ein- und ausgeschaltet
    • Mit al_draw_bitmap wird das Bild an den angegebenen Koordinaten gezeichnet
    • Prefabs haben keine position und werden daher von diesem System nicht verarbeitet
  • load-bitmap ist eine Funktion zum Laden von Bildern, die al_load_bitmap mit al:ensure-loaded umschließt
  • tile->spec erzeugt eine ECS-Objektspezifikation für die Erstellung eines Tile-Prefabs
    • Parent-Map-Entity
    • Tile-Bildausschnitt
    • globale Tiled-Tile-ID
    • Tile-Größe
  • load-tile-prefab prüft über den Index map-tile-prefab, ob das Prefab bereits geladen wurde, und erzeugt es andernfalls mit make-object
  • load-tile kopiert beim Erstellen einer tatsächlichen Map-Tile-Entity Komponenten aus dem Prefab und fügt position hinzu
  • load-map durchläuft die Tilesets und Layer des mit tiled:load-map eingelesenen CLOS-Objekts
    • Das Tileset-Bild wird geladen, und jedes Tile wird als Prefab erzeugt
    • Jede Zelle eines Tile-Layers wird als Entity angelegt und übernimmt die Prefab-Daten
  • Die Layer-Reihenfolge von Tiled bleibt in der Editor-Reihenfolge erhalten, und make-entity garantiert aufsteigende Entity-Nummern
    • Da Systeme ältere Entities zuerst verarbeiten, werden Tiles aus oberen Layern später gezeichnet und überdecken die darunterliegenden Layer
  • Alle Tiles als separate Entities zu speichern ist nicht die einzige mögliche Lösung; statische Maps können auch vorab in einen Buffer gerendert werden

Tile-Animationen

  • Tiled unterstützt animierte Tiles, sodass sich Elemente wie Fackeln oder magische Brunnen darstellen lassen
  • common.lisp und animation.lisp werden hinzugefügt, um gemeinsame Komponenten sowie animationsbezogene Komponenten und Systeme zu trennen
  • Die Komponente animation-frame repräsentiert einen einzelnen Frame einer Animation
    • sequence ist der Animationsname und wird als Typ keyword gespeichert
    • Über den Index sequence-frames werden die Frames einer bestimmten Animation gefunden
    • duration ist die Frame-Dauer in Sekunden
  • animation-state speichert den aktuellen Zustand eines animierten Tiles auf der tatsächlichen Map
    • aktuelle sequence
    • aktueller frame
    • aktuelle Frame-duration
    • die elapsed Zeit, seit der aktuelle Frame angezeigt wird
  • Die Abhängigkeit let-plus wird hinzugefügt, um den Code für Frame-Wechsel kompakt zu schreiben
  • Das System update-animations erhöht elapsed um dt und wechselt zum nächsten Frame, wenn die Dauer überschritten wird
    • Da die Frame-Zeit kleiner sein kann als ein großes dt, wird mit floor berechnet, wie viele Frames übersprungen werden müssen
    • Mit truncate wird zyklisch zum Anfang zurückgekehrt, wenn die Frame-Nummer über die Listenlänge hinausgeht
    • Der Bitmap-Pointer von image wird auf die Bitmap des Prefabs für den nächsten Frame geändert
  • Animationsdauern werden in Tiled in Millisekunden gespeichert und daher in animation->spec in Sekunden umgerechnet
  • instantiate-animation erstellt animation-state für die eigentliche Tile-Entity und initialisiert elapsed mit einer Zufallszahl zwischen 0 und duration, damit gleiche Animationen nicht vollständig synchron laufen
  • Animierte Tiles müssen in Tiled die Eigenschaft "sequence" haben
    • Fehlt diese Eigenschaft, werden sie mit dem Namen NIL geladen, können nicht über den erwarteten Animationsnamen gefunden werden und können einen Typfehler auslösen

Spielercharakter und Steuerung

  • character.lisp wird hinzugefügt und definiert die Komponente character für bewegliche Charaktere
    • speed ist die Geschwindigkeit in Pixeln pro Sekunde
    • target-x und target-y sind die Zielkoordinaten der Bewegung
    • Die anfänglichen Zielwerte werden auf single-float-nan gesetzt, damit ein neuer Charakter nicht ohne Grund nach links oben läuft
  • Die Tag-Komponente player verwendet einen bit-Slot und :index player-entity :unique t
    • Diese Struktur dient dazu, die Spieler-Entity mit (player-entity 1) in O(1) zu finden
    • Die Spieler-Entity wird nicht in einer globalen Variable gespeichert
  • In der ersten Implementierung wird aus dem Tileset ein Ork-Bild ausgeschnitten, als player.png erstellt und der Spieler mit load-player hartcodiert erzeugt
    • Position ist (64.0, 64.0)
    • Größe ist 32×32
    • Geschwindigkeit ist 100.0
  • Das System move-characters bewegt Charaktere bis zum Zielpunkt
    • Wenn NaN-Zielkoordinaten vorhanden sind, werden sie mit der aktuellen Position initialisiert
    • Statt direkter Fließkomma-Vergleiche wird approx-equal verwendet
    • Neue Koordinaten werden mit atan, cos, sin, Geschwindigkeit und dt berechnet
  • Das System control-player liest Tasteneingaben über W, A, S, D und aktualisiert die Zielkoordinaten
    • Es verwendet al:with-current-keyboard-state und al:key-down
    • Mit clamp wird verhindert, dass der Spieler außerhalb der Bildschirmgrenzen gelangt
    • Durch :after (move-characters) läuft es nach dem Bewegungssystem und vermeidet so das Initialisierungsproblem mit NaN

Kollisionen und Objekt-Loading über Tiled-Eigenschaften

  • Anfangs sind Wände normale Bilder wie Bodentiles, sodass der Spieler durch Wände hindurchläuft
  • Mit einem Custom Type in Tiled wird die Klasse map-tile erstellt und ein Boolean-Member obstacle hinzugefügt
    • Den Wand-Tiles wird die Eigenschaft map-tile hinzugefügt, und obstacle wird aktiviert
  • Die Funktion properties->spec wandelt die Hash-Tabelle der Tiled-Eigenschaften in eine ECS-Objektspezifikation um
    • Tiled-Custom-Classes werden als Komponenten behandelt
    • Klassen-Member werden als Komponenten-Slots behandelt
    • Ein Beispiel hat die Form ((:map-tile :obstacle t))
  • load-tile-prefab nimmt das Ergebnis von properties->spec in die Prefab-Spezifikation auf
    • Wenn keine Eigenschaften vorhanden sind, wird mit spec-adjoin die Standardkomponente map-tile hinzugefügt, und obstacle hat den Standardwert nil
  • Der Komponente position werden ein Slot tile-hash und ein Index tiles hinzugefügt
    • tile-hash wandelt x und y in Integer um und packt sie dann in einen einzelnen 64-Bit-Integer
    • Alle Entities an der linken oberen Koordinate eines bestimmten Tiles werden über den Index tiles gefunden
  • tile-start gibt die linke obere Koordinate des Grid-Tiles zurück, zu dem eine beliebige Koordinate gehört
  • tile-obstacle-p prüft, ob sich unter den Entities an derselben Koordinate ein Tile befindet, das map-tile ist und bei dem obstacle wahr ist
  • obstaclep prüft für eine beliebige Koordinate, ob das entsprechende Tile ein Hindernis ist
  • control-player prüft je nach Bewegungsrichtung die relevanten Eck-Tiles des Charakter-Rechtecks und setzt die Zielkoordinate auf die aktuelle Position zurück, wenn ein Hindernis vorhanden ist
  • Dieses Kollisionsverfahren ist nicht vollständig
    • Würde man das Design auf die Mittelpunktkoordinate des Charakters ausrichten, könnten Mathematik und Code einfacher werden, aber das Beispiel bleibt bei der aktuellen Methode, um zusätzliche Komplexität zu vermeiden

Laden von Spieler und animiertem Charakter aus der Map

  • In Tiled werden die Custom Classes character und player hinzugefügt
    • character hat nur das Float-Member speed
    • target-x und target-y werden weggelassen, damit die Standardwerte verwendet werden
    • player hat ein Int-Member player mit dem Standardwert 1
  • In einem Tiled-Objektlayer wird der Spielercharakter als Tile-Objekt platziert und mit den Eigenschaften character und player versehen
  • load-map wird erweitert, sodass auch tiled:object-layer verarbeitet wird
    • Objekteigenschaften werden mit properties->spec in ECS-Komponenten umgewandelt
    • tiled:tile-object kopiert mit load-tile die Tile-Daten und Animationen und setzt die Position
    • Da Tiled-Objektkoordinaten am linken unteren Punkt ausgerichtet sind, wird die Objekthöhe von y abgezogen, um sie an der linken oberen Ecke auszurichten
  • Der hartcodierte Aufruf von load-player sowie die Funktion werden entfernt
  • Diese Struktur liest Tiled-Map-Daten direkt als ECS-Objekte ein und kommt damit datengetriebener Programmierung näher
  • Für die Charakteranimation werden die Sequenzen orc-idle und orc-run des Orks im Tileset definiert und verwendet
  • change-animation-sequence ändert die aktuelle Animation einer Entity
    • Wenn bereits dieselbe Sequenz aktiv ist, geschieht nichts
    • Der erste Frame der neuen Sequenz wird über den Index sequence-frames gefunden, und animation-state sowie image-bitmap werden aktualisiert
  • move-characters wechselt zu :orc-idle, wenn der Charakter stillsteht, und zu :orc-run, wenn er sich bewegt

Gegner, Game Over und A*-Pfadsuche

  • Die Komponente enemy hat zwei Slots, die für das Verhalten von Gegnern nötig sind
    • vision-range: die Distanz, ab der der Gegner den Spieler sieht und zu reagieren beginnt
    • attack-range: Angriffsreichweite
  • Zum Beenden des Spiels wird die globale Variable *should-quit* hinzugefügt; die Hauptschleife endet, wenn dieser Wert wahr ist
  • Das System handle-enemies holt die Koordinaten des Spielers und vergleicht sie mit denen der Gegner
    • Befindet sich der Spieler im Sichtbereich, werden die Zielkoordinaten des Gegners auf die Position des Spielers gesetzt
    • Befindet sich der Spieler in Angriffsreichweite, wird *should-quit* auf wahr gesetzt und eine native Messagebox mit You died angezeigt
  • Die Gegneranimationen verwenden die Sequenzen demon-idle und demon-run
    • move-characters wählt abhängig vom Ergebnis von has-player-p für den Spieler Ork-Animationen und für Gegner Dämonen-Animationen aus
  • Beim direkten Verfolgen würden auch Gegner durch Wände laufen, daher wird A*-Pfadsuche hinzugefügt
  • cl-astar wird als Abhängigkeit hinzugefügt
    • Diese Bibliothek erzeugt per Makro auf das jeweilige Problem optimierte Pfadsuchfunktionen
  • Der Pfad wird nicht als Array in einem Komponenten-Slot gespeichert, sondern jeder Wegpunkt wird als eigene Entity dargestellt
    • path-point besitzt x, y und traveller; traveller hat einen path-points-Index
    • path speichert das endgültige Ziel destination-x, destination-y
    • Die Zielkoordinaten von character bezeichnen den nächsten Wegpunkt, path das endgültige Ziel
  • Das System follow-path holt den ersten Wegpunkt und bewegt den Charakter dorthin
    • Wird der Punkt erreicht, wird die entsprechende path-point-Entity gelöscht
    • Gibt es keine weiteren Punkte, wird die Komponente path gelöscht
  • find-path wird mit a*:define-path-finder definiert
    • Die Weltgröße wird berechnet, indem die Fenstergröße durch die Tile-Größe geteilt wird
    • Es wird ein Row-Major-Indexer verwendet
    • Ob das Ziel erreicht ist, wird daran erkannt, ob die Tile-Koordinaten übereinstimmen
    • Nachbarn werden in 8 Richtungen aufgezählt
    • Hindernisse oder diagonale Bewegungen, die ein Hindernis schneiden, werden durch Kosten von most-positive-single-float praktisch unmöglich gemacht
    • Als Heuristik wird Octile Distance verwendet
    • Falls bereits ein Pfad existiert, werden die Wegpunkte gelöscht und ein neuer path zugewiesen
    • Jeder Punkt des Ergebnis-Pfads wird als Entity mit path-point und parent erzeugt
  • handle-enemies ruft find-path auf, wenn ein Gegner den Spieler sieht und das bisherige Pfadziel von der Spielerposition abweicht
  • Nach der Änderung verfolgen Gegner den Spieler, bewegen sich dabei aber um Hindernisse herum

Spiel-UI auf Nuklear-Basis

  • Für narrative Elemente wird eine GUI benötigt, doch klassische GUI-Bibliotheken wie Qt oder GTK eignen sich nicht für eine Spiel-UI, die in den Grafikkontext von liballegro gezeichnet wird
  • Als UI-Bibliothek wird Nuklear verwendet
    • Für die Nutzung mit liballegro gibt es das Common-Lisp-Binding cl-liballegro-nuklear
    • Das Binding stellt auch eine DSL für deklarative Interfaces bereit
  • Die Abhängigkeit cl-liballegro-nuklear/declarative wird hinzugefügt, außerdem wird src/narrative.lisp als neue Datei angelegt
  • Im Package wird der lokale Nickname ui registriert, um cl-liballegro-nuklear/declarative kürzer referenzieren zu können
  • Als UI-Schrift wird Alegreya von Google Fonts verwendet; der Dateiname wird in alegreya-sc.ttf geändert
  • ui:defwindow narrative definiert die Funktion für das narrative Fenster
    • Die Fensterposition wird für den mittleren Bildschirmbereich berechnet
    • Mit ui:label-wrap wird automatisch umbrochener Text angezeigt
    • ui:button-label "Ok" gibt beim Klick wahr zurück
  • Nuklear ist eine Immediate-Mode-UI-Bibliothek
    • Statt Widget-Objekte wie im Retained Mode im Speicher zu halten, werden Rendering und Verarbeitung in jedem Frame ausgeführt
    • Button-Klicks werden nicht über Callbacks, sondern über Rückgabewerte und Bedingungen pro Frame verarbeitet
  • main.lisp lädt die UI-Schrift und initialisiert mit nk:allegro-init den UI-Kontext
    • In der Event-Schleife werden nk:input-begin, nk:allegro-handle-event und nk:input-end aufgerufen
    • Beim Rendering wird nk:allegro-render aufgerufen
    • Beim Beenden werden nk:allegro-shutdown und nk:allegro-font-del aufgerufen

UI-Skin und narrative Objekte

  • Da die Standard-UI eintönig ist, wird sie mit den Bild-Assets fantasy-ui-borders von Kenney gestaltet
  • In den globalen Variablen *window-background*, *button-normal-background*, *button-hover-background* und *button-active-background* werden UI-Bilder gespeichert
  • load-ui lädt Bilder mit nk:allegro-create-image, unload-ui gibt die C-seitigen Bildressourcen mit nk:allegro-del-image frei
  • In init wird load-ui aufgerufen, beim Ende der Hauptschleife unload-ui
  • Über das Argument :styles von ui:defwindow werden Hintergrund, Bilder für die verschiedenen Button-Zustände und Textfarbe festgelegt
  • Die Komponente narrative repräsentiert Objekte für Environmental Storytelling
    • text: der anzuzeigende Text
    • shown: ob er bereits einmal angezeigt wurde
    • active: ob das aktuelle Fenster aktiv ist
    • Für active gibt es den Index active-narratives
  • Das System show-narrative zeigt ein Fenster an, wenn sich der Spieler in der Nähe eines narrativen Objekts befindet
    • Die Interaktionsdistanz wird aus +interact-distance-factor+ und der Tile-Größe des Spielers berechnet
    • Das Fenster wird angezeigt, wenn es bereits aktiv ist, noch nie angezeigt wurde oder die Taste E gedrückt wird
    • Es wird über den Ok-Button oder mit Esc, Space oder Enter geschlossen
  • In Tiled wird der Custom Type narrative angelegt und ein text-Member vom Typ string hinzugefügt
  • Für die Kollisionsprüfung müssen nicht passierbare Objekte am Tile-Grid ausgerichtet werden
  • Das Problem, dass sich der Dungeon weiterbewegt, während ein narratives Fenster offen ist, wird über Ausführungsbedingungen der Systeme verhindert
    • Zu move-characters und control-player wird :when (null (active-narratives t)) hinzugefügt
    • Gibt es ein aktives narratives Fenster, werden die Systeme für Bewegung und Steuerung nicht ausgeführt
  • Die Siegbedingung wird als Tag-Komponente win hinzugefügt
    • Wird ein Fenster an einem Objekt geschlossen, das auch narrative besitzt, wird *should-quit* auf wahr gesetzt und das Spiel endet

Abschluss und Umfang

  • Das finale Beispiel nutzt cl-fast-ecs, cl-tiled, cl-astar und cl-liballegro-nuklear, um einen Souls-like-Dungeon-Crawler mit Environmental Storytelling, Gegner-KI und GUI aufzubauen
  • Der Implementierungsumfang liegt bei etwa 500 Zeilen Code
  • Der vollständige Code befindet sich im GitHub-Repository und enthält neben dem Tutorial-Code auch optionale Typdeklarationen mit declaim
  • Sounddesign, Cutscenes, Hauptmenü, Levelwechsel, das „door problem“ und Ähnliches werden nicht behandelt
  • Der Autumn Lisp Game Jam 2024 findet am 25. Oktober 2024 auf itch.io statt; dabei werden über 10 Tage Spiele in Lisp-Dialekten entwickelt und gegenseitig bewertet sowie mit Feedback versehen
  • Dieser Teil basiert auf dem Beitrag Thoughtbound zum Spring Lisp Game Jam 2023
  • Der nächste Teil stellt in Aussicht, den Umfang zu vergrößern und mit fortgeschrittenerer KI ein Echtzeitstrategiespiel zu entwickeln

1 Kommentare

 
GN⁺ 2024-10-19
Meinungen auf Hacker News
  • Ich wünschte, alle technischen Tutorials sähen so aus. Der Artikel ist gut strukturiert, hat kaum Syntaxfehler, erklärt jedes neue Thema genau im richtigen Umfang und enthält vollständige Codebeispiele sowie Visualisierungen, die zeigen, was der Code tatsächlich tut.
    Er ist lang genug, um das Material gründlich zu behandeln, aber auch eigenständig genug, dass man folgen kann, selbst wenn man Teil 1 nicht gelesen hat und Common Lisp nur vor ein paar Jahren ein paar Monate lang ausprobiert hat. Mit Clojure und Emacs Lisp habe ich recht viel gemacht.
    Bravo, awkravchuk/Andrew :^)
    (Crossposted von https://mxjn.me/2024/10/17/1)

    • Vor allem ist es Text statt Video. Man kann kopieren und einfügen, klar lesen, im eigenen Tempo folgen und es in Ruhe konsumieren.
      Außerdem lässt es sich leicht für Offline-Nutzung oder Archivierung speichern und kommentieren, und es ist gut durchsuchbar.
  • Weniges im technischen Bereich bewegt mich so sehr wie ein gutes Projekt oder ein guter Artikel über Common Lisp. Das ist wirklich ein großes Geschenk.
    Ich habe Teil 1 gelesen, als er erschien, und freue mich sehr darauf, auch diesen Teil zu lesen. Lob an den Autor.

  • Allein package.sh und das gesamte Build-Management für drei Betriebssysteme sind schon eine Masterclass für sich. Ich habe schon beim Durchstöbern des GitHub-Repositorys viel gelernt.
    Normalerweise baue ich Common-Lisp-Command-Line-Apps mit SBCL oder LispWorks, aber vielleicht probiere ich es beim nächsten Mal mit ECL. Es ist schön, Builds für macOS und Linux zu haben, und es dürfte Spaß machen, etwas Neues auszuprobieren.

    • Ich baue diese CI-Konfiguration seit Jahren auf der CL-Infrastruktur auf, und sie geht offenbar ständig kaputt :D
  • Sehr guter Artikel. Ich entwickle gerade mit Lisp, genauer gesagt ClojureScript, einen multiplayerfähigen, Third-Person-Shooter auf Zauberbasis. Es ist ein webbasiertes 3D-Spiel, und ich habe vor, die Reise zu verbloggen, einschließlich der Tools und Abstraktionen, die ich für das Projekt gebaut habe.
    Falls es interessiert: Die Demo ist hier: https://wizardmasters.io

    • Jon Blow wollte vor langer Zeit ebenfalls so ein Spiel machen. Es könnte lehrreich sein, sich anzusehen, wie und warum es gescheitert ist.
  • Der Artikel selbst ist wirklich solide, aber wenn man sich den Setup-Prozess aus Teil 1 ansieht, der über Common Lisp selbst, Python, C und mehrere Schritte läuft, zeigt das wohl, warum CL gerade bei jüngeren Programmierern nicht besonders beliebt ist.
    Das ist schade, und ich wünschte, jemand würde sich die Mühe machen, die Sprache auf Installationsseite zugänglicher zu machen.

    • Es zielt wohl nicht exakt auf dasselbe Problem, aber https://ciel-lang.org/ ist zumindest ein Versuch, einen Teil des Problems der zu vielen Schritte zu lösen.
      Nach meinem Verständnis liegt der Fokus eher darauf, dass es zu viele Optionen gibt und alte Defaults veraltet wirken.
  • Die Event-Loop ist ein hervorragendes Beispiel dafür, wie sehr loop eine vollwertige domänenspezifische Sprache für Iteration ist. Ob man sie mag oder nicht ;)

    • Warum nicht statt loop https://iterate.common-lisp.dev/ verwenden? Keine fremde Nicht-S-Expression-Syntax, und man braucht kein do, um zurück zur Lisp-Syntax zu kommen.
      Man verwendet normales if/when statt hässlichem else/end, und insgesamt kommen noch nützliche Funktionen dazu.
    • Anfangs habe ich darüber gespottet, aber nach ein paar Jahren Programmieren in Common Lisp ist loop zu einem meiner liebsten CL-Konstrukte geworden.
  • Dieser Artikel erinnert an „Caves of Clojure“: https://stevelosh.com/blog/2012/07/caves-of-clojure-01/

  • Ausgerechnet diese Woche habe ich angefangen, in Python ein Roguelike zu entwickeln; es mit Lisp zu versuchen, wäre auch cool.

  • Ich fühle mich reingelegt. Ich kam her, um zu lernen, wie man ein einfaches Spiel baut, und habe am Ende unglaublich viel über Computing insgesamt gelernt.
    Wirklich gut.