- 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-2erstellt; im Beispiel wirdliballegroals Backend gewählt - Nachdem das Projektverzeichnis mit Quicklisps
local-projectsverknüpft wurde, wird die Fenstergröße insrc/main.lispauf 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.tmxund die Tileset-Datei sind über die im Tutorial bereitgestellteResources.ziperhältlich
- In
ecs-tutorial-2.asdwird die Abhängigkeitcl-tiledergänzt, und mitsrc/map.lispwird eine neue Datei angelegt, um Code zum Laden und Anzeigen der Map zu trennen - In
src/package.lispwirdcl-tiledunter dem lokalen Nicknametiledregistriert
Warum CLOS-Objekte in ECS-Komponenten überführt werden
cl-tiledgibt 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.0016Sekunden hinzu, also mehr als 1,5 ms
- Werden die von
cl-tiledgelesenen Daten in den Storage voncl-fast-ecsüberführt, lässt sich Dispatch reduzieren und die CPU-Cache-Nutzung verbessern - Die Abhängigkeit
cl-fast-ecswird ergänzt; ininitwirdecs:make-storageund inupdateecs:run-systemsaufgerufen
Map-, Tile- und Prefab-Komponenten
mapist eine Tag-Komponente, die die geladene Map-Entity markiertmap-tilesteht für ein einzelnes Tile und besitzt einen Boolean-Slotobstacle, der angibt, ob es ein Hindernis wie eine Wand oder eine geschlossene Tür ist- Die Komponente
parentgibt an, zu welcher Map-Entity Tiles und mapbezogene Objekte gehören- Im Slot
entitywird:index childrenangegeben, 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
- Im Slot
- Zu
ecs:*entity-deleting-hook*wird ein Hook hinzugefügt, der beim Löschen einer Parent-Entity auch die über den Indexchildrengefundenen Child-Entities mitlöscht - Die Komponente
imagespeichert nur einen C-Pointer aufALLEGRO_BITMAP- Aus dem Tileset-Bild werden mit
al_create_sub_bitmap32×32-Ausschnitte erzeugt und deren Pointer gespeichert
- Aus dem Tileset-Bild werden mit
map-tile-prefabist ein Tile-Prefab mit der globalen IDgideines Tiled-Tiles- Für
gidwird:index map-tile-prefab :unique tangegeben, um über die ID eine einzelne Prefab-Entity zu finden - Die tatsächlichen Tiles auf der Map kopieren etwa das
imagedes Prefabs, besitzen ihre Position aber über eine eigeneposition-Komponente
- Für
- Der Finalizer von
imageruftal_destroy_bitmapnur auf, wenn die Entity einmap-tile-prefabist- Dadurch wird ein Double Free vermieden, da mehrere Map-Tiles denselben
ALLEGRO_BITMAP-Pointer teilen
- Dadurch wird ein Double Free vermieden, da mehrere Map-Tiles denselben
positionundsizespeichern Bildschirmkoordinaten und Größen alssingle-float- Da
liballegroBildschirmkoordinaten wegen OpenGL-Kompatibilität als Single-Precision-Floats behandelt, wird dieselbe Vorgehensweise verwendet
- Da
Image-Rendering und Laden der Map
- Das System
render-imagesrendert Entities mitpositionundimage- Mit
al_hold_bitmap_drawingwird Sprite Batching ein- und ausgeschaltet - Mit
al_draw_bitmapwird das Bild an den angegebenen Koordinaten gezeichnet - Prefabs haben keine
positionund werden daher von diesem System nicht verarbeitet
- Mit
load-bitmapist eine Funktion zum Laden von Bildern, dieal_load_bitmapmital:ensure-loadedumschließttile->specerzeugt eine ECS-Objektspezifikation für die Erstellung eines Tile-Prefabs- Parent-Map-Entity
- Tile-Bildausschnitt
- globale Tiled-Tile-ID
- Tile-Größe
load-tile-prefabprüft über den Indexmap-tile-prefab, ob das Prefab bereits geladen wurde, und erzeugt es andernfalls mitmake-objectload-tilekopiert beim Erstellen einer tatsächlichen Map-Tile-Entity Komponenten aus dem Prefab und fügtpositionhinzuload-mapdurchläuft die Tilesets und Layer des mittiled:load-mapeingelesenen 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-entitygarantiert 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.lispundanimation.lispwerden hinzugefügt, um gemeinsame Komponenten sowie animationsbezogene Komponenten und Systeme zu trennen- Die Komponente
animation-framerepräsentiert einen einzelnen Frame einer Animationsequenceist der Animationsname und wird als Typkeywordgespeichert- Über den Index
sequence-frameswerden die Frames einer bestimmten Animation gefunden durationist die Frame-Dauer in Sekunden
animation-statespeichert den aktuellen Zustand eines animierten Tiles auf der tatsächlichen Map- aktuelle
sequence - aktueller
frame - aktuelle Frame-
duration - die
elapsedZeit, seit der aktuelle Frame angezeigt wird
- aktuelle
- Die Abhängigkeit
let-pluswird hinzugefügt, um den Code für Frame-Wechsel kompakt zu schreiben - Das System
update-animationserhöhtelapsedumdtund wechselt zum nächsten Frame, wenn die Dauer überschritten wird- Da die Frame-Zeit kleiner sein kann als ein großes
dt, wird mitfloorberechnet, wie viele Frames übersprungen werden müssen - Mit
truncatewird zyklisch zum Anfang zurückgekehrt, wenn die Frame-Nummer über die Listenlänge hinausgeht - Der Bitmap-Pointer von
imagewird auf die Bitmap des Prefabs für den nächsten Frame geändert
- Da die Frame-Zeit kleiner sein kann als ein großes
- Animationsdauern werden in Tiled in Millisekunden gespeichert und daher in
animation->specin Sekunden umgerechnet instantiate-animationerstelltanimation-statefür die eigentliche Tile-Entity und initialisiertelapsedmit einer Zufallszahl zwischen 0 undduration, 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
NILgeladen, können nicht über den erwarteten Animationsnamen gefunden werden und können einen Typfehler auslösen
- Fehlt diese Eigenschaft, werden sie mit dem Namen
Spielercharakter und Steuerung
character.lispwird hinzugefügt und definiert die Komponentecharacterfür bewegliche Charakterespeedist die Geschwindigkeit in Pixeln pro Sekundetarget-xundtarget-ysind die Zielkoordinaten der Bewegung- Die anfänglichen Zielwerte werden auf
single-float-nangesetzt, damit ein neuer Charakter nicht ohne Grund nach links oben läuft
- Die Tag-Komponente
playerverwendet einenbit-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
- Diese Struktur dient dazu, die Spieler-Entity mit
- In der ersten Implementierung wird aus dem Tileset ein Ork-Bild ausgeschnitten, als
player.pngerstellt und der Spieler mitload-playerhartcodiert erzeugt- Position ist
(64.0, 64.0) - Größe ist 32×32
- Geschwindigkeit ist 100.0
- Position ist
- Das System
move-charactersbewegt Charaktere bis zum Zielpunkt- Wenn
NaN-Zielkoordinaten vorhanden sind, werden sie mit der aktuellen Position initialisiert - Statt direkter Fließkomma-Vergleiche wird
approx-equalverwendet - Neue Koordinaten werden mit
atan,cos,sin, Geschwindigkeit unddtberechnet
- Wenn
- Das System
control-playerliest Tasteneingaben überW,A,S,Dund aktualisiert die Zielkoordinaten- Es verwendet
al:with-current-keyboard-stateundal:key-down - Mit
clampwird verhindert, dass der Spieler außerhalb der Bildschirmgrenzen gelangt - Durch
:after (move-characters)läuft es nach dem Bewegungssystem und vermeidet so das Initialisierungsproblem mitNaN
- Es verwendet
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-tileerstellt und ein Boolean-Memberobstaclehinzugefügt- Den Wand-Tiles wird die Eigenschaft
map-tilehinzugefügt, undobstaclewird aktiviert
- Den Wand-Tiles wird die Eigenschaft
- Die Funktion
properties->specwandelt 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-prefabnimmt das Ergebnis vonproperties->specin die Prefab-Spezifikation auf- Wenn keine Eigenschaften vorhanden sind, wird mit
spec-adjoindie Standardkomponentemap-tilehinzugefügt, undobstaclehat den Standardwertnil
- Wenn keine Eigenschaften vorhanden sind, wird mit
- Der Komponente
positionwerden ein Slottile-hashund ein Indextileshinzugefügttile-hashwandeltxundyin 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
tilesgefunden
tile-startgibt die linke obere Koordinate des Grid-Tiles zurück, zu dem eine beliebige Koordinate gehörttile-obstacle-pprüft, ob sich unter den Entities an derselben Koordinate ein Tile befindet, dasmap-tileist und bei demobstaclewahr istobstaclepprüft für eine beliebige Koordinate, ob das entsprechende Tile ein Hindernis istcontrol-playerprü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
characterundplayerhinzugefügtcharacterhat nur das Float-Memberspeedtarget-xundtarget-ywerden weggelassen, damit die Standardwerte verwendet werdenplayerhat ein Int-Memberplayermit dem Standardwert 1
- In einem Tiled-Objektlayer wird der Spielercharakter als Tile-Objekt platziert und mit den Eigenschaften
characterundplayerversehen load-mapwird erweitert, sodass auchtiled:object-layerverarbeitet wird- Objekteigenschaften werden mit
properties->specin ECS-Komponenten umgewandelt tiled:tile-objectkopiert mitload-tiledie Tile-Daten und Animationen und setzt die Position- Da Tiled-Objektkoordinaten am linken unteren Punkt ausgerichtet sind, wird die Objekthöhe von
yabgezogen, um sie an der linken oberen Ecke auszurichten
- Objekteigenschaften werden mit
- Der hartcodierte Aufruf von
load-playersowie 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-idleundorc-rundes 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-framesgefunden, undanimation-statesowieimage-bitmapwerden aktualisiert
move-characterswechselt zu:orc-idle, wenn der Charakter stillsteht, und zu:orc-run, wenn er sich bewegt
Gegner, Game Over und A*-Pfadsuche
- Die Komponente
enemyhat zwei Slots, die für das Verhalten von Gegnern nötig sindvision-range: die Distanz, ab der der Gegner den Spieler sieht und zu reagieren beginntattack-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-enemiesholt 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 mitYou diedangezeigt
- Die Gegneranimationen verwenden die Sequenzen
demon-idleunddemon-runmove-characterswählt abhängig vom Ergebnis vonhas-player-pfü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-pointbesitztx,yundtraveller;travellerhat einenpath-points-Indexpathspeichert das endgültige Zieldestination-x,destination-y- Die Zielkoordinaten von
characterbezeichnen den nächsten Wegpunkt,pathdas endgültige Ziel
- Das System
follow-pathholt 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
pathgelöscht
- Wird der Punkt erreicht, wird die entsprechende
find-pathwird mita*:define-path-finderdefiniert- 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-floatpraktisch unmöglich gemacht - Als Heuristik wird Octile Distance verwendet
- Falls bereits ein Pfad existiert, werden die Wegpunkte gelöscht und ein neuer
pathzugewiesen - Jeder Punkt des Ergebnis-Pfads wird als Entity mit
path-pointundparenterzeugt
handle-enemiesruftfind-pathauf, 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
liballegrogezeichnet wird - Als UI-Bibliothek wird Nuklear verwendet
- Für die Nutzung mit
liballegrogibt es das Common-Lisp-Binding cl-liballegro-nuklear - Das Binding stellt auch eine DSL für deklarative Interfaces bereit
- Für die Nutzung mit
- Die Abhängigkeit
cl-liballegro-nuklear/declarativewird hinzugefügt, außerdem wirdsrc/narrative.lispals neue Datei angelegt - Im Package wird der lokale Nickname
uiregistriert, umcl-liballegro-nuklear/declarativekürzer referenzieren zu können - Als UI-Schrift wird Alegreya von Google Fonts verwendet; der Dateiname wird in
alegreya-sc.ttfgeändert ui:defwindow narrativedefiniert die Funktion für das narrative Fenster- Die Fensterposition wird für den mittleren Bildschirmbereich berechnet
- Mit
ui:label-wrapwird 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.lisplädt die UI-Schrift und initialisiert mitnk:allegro-initden UI-Kontext- In der Event-Schleife werden
nk:input-begin,nk:allegro-handle-eventundnk:input-endaufgerufen - Beim Rendering wird
nk:allegro-renderaufgerufen - Beim Beenden werden
nk:allegro-shutdownundnk:allegro-font-delaufgerufen
- In der Event-Schleife werden
UI-Skin und narrative Objekte
- Da die Standard-UI eintönig ist, wird sie mit den Bild-Assets
fantasy-ui-bordersvon Kenney gestaltet - In den globalen Variablen
*window-background*,*button-normal-background*,*button-hover-background*und*button-active-background*werden UI-Bilder gespeichert load-uilädt Bilder mitnk:allegro-create-image,unload-uigibt die C-seitigen Bildressourcen mitnk:allegro-del-imagefrei- In
initwirdload-uiaufgerufen, beim Ende der Hauptschleifeunload-ui - Über das Argument
:stylesvonui:defwindowwerden Hintergrund, Bilder für die verschiedenen Button-Zustände und Textfarbe festgelegt - Die Komponente
narrativerepräsentiert Objekte für Environmental Storytellingtext: der anzuzeigende Textshown: ob er bereits einmal angezeigt wurdeactive: ob das aktuelle Fenster aktiv ist- Für
activegibt es den Indexactive-narratives
- Das System
show-narrativezeigt 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
Egedrückt wird - Es wird über den
Ok-Button oder mitEsc,SpaceoderEntergeschlossen
- Die Interaktionsdistanz wird aus
- In Tiled wird der Custom Type
narrativeangelegt und eintext-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-charactersundcontrol-playerwird:when (null (active-narratives t))hinzugefügt - Gibt es ein aktives narratives Fenster, werden die Systeme für Bewegung und Steuerung nicht ausgeführt
- Zu
- Die Siegbedingung wird als Tag-Komponente
winhinzugefügt- Wird ein Fenster an einem Objekt geschlossen, das auch
narrativebesitzt, wird*should-quit*auf wahr gesetzt und das Spiel endet
- Wird ein Fenster an einem Objekt geschlossen, das auch
Abschluss und Umfang
- Das finale Beispiel nutzt
cl-fast-ecs,cl-tiled,cl-astarundcl-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
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)
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.shund 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.
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
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.
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
loopeine vollwertige domänenspezifische Sprache für Iteration ist. Ob man sie mag oder nicht ;)loophttps://iterate.common-lisp.dev/ verwenden? Keine fremde Nicht-S-Expression-Syntax, und man braucht keindo, um zurück zur Lisp-Syntax zu kommen.Man verwendet normales
if/whenstatt hässlichemelse/end, und insgesamt kommen noch nützliche Funktionen dazu.loopzu 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.