Skip to content

Latest commit

 

History

375 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dotcl MonoGame Common Lisp Dungeon Slime

Note: This project targets DotCL 0.1.17.

This is an implementation of the MonoGame 2D Dungeon Slime demo in as Lispy a fashion as possible. It is a learning vehicle for how to build MonoGame games in DotCL Common Lisp. Learnings will also be passed on to the DotCL project as feature suggestions and bug reports.

The goals of this project are:

  1. Move as much of the functionality into basic Common Lisp and CLOS as possible.

  2. Make a clean set of Lispy functions for interacting with MonoGame.

  3. Build various utilities and frameworks to make interacting with C#, CLR and MonoGame (and other non-Common Lisp platform things) ergonomic, reliable and well-documented.

This is heavily a work in progress. The code may not be as clean as I would like, but this is primarily a proof of concept for creating another game in SANO-san's awesome DotCL Common Lisp later.

Package Generator

This package uses my C# lisp package generator (dotcl-packagegen), which originally was a part of this code, but is now standalone.

See Use and Integration of C# Assemblies as Lisp Packages below for more details.

Installation and Use

Please clone the package generator and run make build test deploy to get the dotcl-packagegen command line tool installed. This is needed by the build system if you wish to run make cspackages. If you do not edit the cspackages, you do not need to install the tool.

How to Use

I've built and tested this on Linux on x64 only. I make no guarantees that this will work on any other platform.

You will need to check out dotcl in a sibling directory to this one and build and install the dotcl tool. This demo uses various files assuming they are in that sibling directory. These references are in the DungeonSlime.csproj file.

DotCL Version

The game is tested and works out-of-the-box on DotCL 0.1.17. This updated version includes massive performance boosts (via typed dotnet:invoke natively compiling to callvirt, and unboxed float/int64 numerics), async/await, and a drastically improved interop model (like automatically inferring receiver types, extension methods, and resolving generics).

A custom-patched DotCL is no longer necessary.

Build & Launch Steps

Preparation:

  1. To use the MonoGame Content Builder, in this project's home directory: dotnet tool restore
    • This installs dotnet-mgcb and dotnet-mgcb-editor and dotnet-mgcb-editor-linux among other things.

The Upgraded Build System (DotCL 0.1.17)

As of DotCL 0.1.15+ (currently targeting 0.1.17), the project build system is migrated to a pure NuGet package reference structure (<PackageReference Include="DotCL.Runtime" Version="0.1.17" />). This removes the need for a local sibling dotcl repository check-out.

Key features of the updated build system:

  1. Quicklisp Integration via Build-Init: The project utilizes DotCL's <DotclBuildInit> task pointing to build-setup.lisp. At build time, this script loads the local Quicklisp installation (~/quicklisp/setup.lisp) to register ASDF system search hooks. External systems (such as anaphora) are resolved, compiled, and bundled into the output directory automatically without requiring manual CL_SOURCE_REGISTRY environment variables or wrappers.
    • Note: a DotCL Patched QuickLisp is available. I recommend installing QuickLisp normally with SBCL (or whatever existing CL implementation you have that is supported by QL) then edit the installed ~/quicklisp/ files per the patch above, and then just use it normally with DotCL.
  2. Automated Reference Copying: A custom MSBuild target (CopyReferencesBeforeLisp) copies all referenced NuGet assemblies (like MonoGame.Framework.dll) to the output folder before Lisp compilation runs, in an attempt to prevent compile-time assembly load errors. (They still happen, though.)
  3. Decoupled Compile-time Assemblies: The Lisp compilation process does not require loading DungeonSlime.dll during the :compile-toplevel phase (avoiding circular dependency or file missing errors on clean builds). The assembly is dynamically loaded only during runtime :load-toplevel and :execute phases, so that custom C# types (such as MonoUtilsRegistrar) are successfully resolved during standalone interactive REPL sessions.
  4. Self-Contained Executable Bundle: Compiled dependency FASLs (e.g. anaphora.fasl, dotcl-repl.fasl) and the load manifest (dotcl-deps.txt) are placed next to the executable in the dotcl-fasl/ directory. Additionally, the standard contrib/ Lisp library is copied directly from the restored NuGet package cache folder ($(_DotCLContribDir)) to the build output directory, allowing the entire bin/ directory to be copied and run standalone on another machine.

You can use the provided Makefile to build, test, and run the project:

  • Rebuild C# Packages: make cspackages
  • Build the project: make build (runs the consolidated dotnet build command in a single step; also gates on make check-parens and make content-fonts first, see below)
  • Build then run the test suite: make check (the one command to run before declaring a change done)
  • Run the test suite: make test (runs the game in --test mode; does not build first, use make build test or make check)
  • Run the game: make run (runs the GUI game; does not build first, use make build run)
  • Clean build files: make clean (cleans temporary directories and compiled FASL files)
  • Deep-clean build files: make deep-clean (make clean plus the second, independent ASDF FASL cache under ~/.cache/common-lisp/ that make clean does not touch; see doc/implementation-notes.md)
  • Run MonoGame Content Builder: make mgcb
  • Check Lisp parentheses balance: make check-parens (runs automatically as part of make build)

NOTE: Font Rebuilds: The MonoGame Content Builder (MGCB) task only monitors changes to the .spritefont XML descriptor, not the raw .ttf font file under Content/fonts/ that it references, so an edited/replaced .ttf would otherwise silently fail to rebuild. make build runs make content-fonts first, which touches any .spritefont whose referenced .ttf is newer, so this no longer needs to be done by hand.

Or manually run the steps:

  1. To build: dotnet build DungeonSlime.csproj -v d -c Debug

    • -v n makes Lisp compilation errors be shown.
    • -v d shows more details.
    • -v diag shows the most, but I haven't really noticed a difference.
  2. To run: bin/Debug/net10.0/ubuntu.24.04-x64/DungeonSlime (or whatever your platform-specific path is)

    • To run in test mode, add --test to the command line, and it will not invoke the game.
  3. To generate C# assembly lisp packages: make cspackages

  4. If you want to edit the .mgcb file, execute: dotnet tool run mgcb-editor-linux

  5. To check Lisp parentheses balance manually: find . -type f \( -name "*.lisp" -o -name "*.asd" \) ! -path "*/obj/*" ! -path "*/bin/*" ! -path "*/.git/*" ! -path "*/scratch/*" | xargs python3 check_parens.py (this is what make check-parens runs; the scratch/ exclusion matters because that gitignored directory holds throwaway files that are sometimes intentionally unbalanced)

    • Parentheses balance is usually not much of a problem for human coders due to IDE support, but it seems to trip up AI assistance a lot. The problem is compounded due to DotCL's mechanism of concatenating all Lisp files into a single input during compilation phase.

How to Load in REPL

First, build the game per the above. This ensures the C# files are compiled and copied into the output directory, and the ContentManager assets are generated.

Invoke DotCL REPL using the provided Makefile target: make repl (Which executes dotcl --eval '(load "load-repl.lisp")' --eval '(in-package :dungeon-slime)' repl directly).

Alternatively, invoke DotCL manually with rlwrap --always-readline dotcl (or omit rlwrap if you prefer), then:

;; Load all the necessary dependencies and make a game instance as
;; dungeon-slime::*mg-game*
(load "load-repl.lisp")
(in-package :dungeon-slime)

;; Execute the game then, with this:
(dotnet:invoke *mg-game* "Run")

;; After the game executes and returns to the REPL, clean up with this:
(dotnet:invoke *mg-game* "Dispose")
;; otherwise the game window will just sit there and cause grief.

;; To run the tests from the REPL:
(dungeon-slime-tests::run-all-tests)

It is not possible to run the game a second time after it exits under the current MonoGame system. It seems others have tried; see this issue. In my case, it shows the game window and then segfaults out.

Test with Another User

This test ensures that there are no unmet dependencies when the application is built and run by another user (as if on another computer).

Create another user, I use one called dummy.

  • cp -a bin /tmp to copy the built application there
  • Enable the user to write to your X session: xhost +si:localuser:dummy
  • su - dummy
    • export DISPLAY=:0
    • /tmp/bin/Debug/net10.0/arch-x64/DungeonSlime to run the game; ensure it works

Portable Execution & Multi-User Testing Considerations

To ensure the application is fully portable and runs successfully when executed by another user account (or from a different directory), several design and build system choices are implemented.

Portable Design Features Implemented

  1. Raw Sound File Copying: Raw .wav audio files are explicitly copied to the output folder via the MSBuild project file (DungeonSlime.csproj) so they are available on disk for native filesystem loading.
  2. C# Interop Path Qualification: Relative paths passed to filesystem-based constructors (like SoundEffect.FromFile and Song.FromUri) are resolved relative to the executable's directory using qualify-path. The paths are coerced to strings using uiop:native-namestring to prevent interop method signature mismatch crashes.
  3. Flexible URI Parsing: Theme song URIs are constructed using system-uri-kind:+relative-or-absolute+ to ensure they parse Unix absolute paths correctly without throwing formatting exceptions.
  4. Graceful Subsystem Fallback: Audio hardware initialization and loading is protected by handler-case blocks. If the target environment blocks sound server connections (e.g. PipeWire socket permissions), the game runs in silent mode rather than crashing.

Guidelines for Future Portable Development

  • Qualify Filesystem Paths: Never pass raw relative strings directly to foreign filesystem methods. Wrap them in qualify-path to ensure directory independence.
  • Ensure String Conversion: Always call uiop:native-namestring on resolved pathnames before passing them to C# constructors to avoid type dispatch failures.
  • Declare Custom Assets in Project File: Any raw asset loaded directly via the filesystem (bypassing the MonoGame ContentManager pipeline) must be declared as <Content> with <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> in DungeonSlime.csproj.
  • Trap Host Subsystem Exceptions: Wrap host hardware and driver initialization routines (sound, graphics, network) in handler-case blocks so the game degrades gracefully on restricted host environments.

A Note on ML/AI & Tooling

Much of the code herein is written by myself. I enjoy the act of writing code (and especially documentation) and knowing how everything is put together.

However, I do use ML coding tools, primarily Antigravity CLI/Gemini, but I have also been dabbling in OpenCode with various local models and even tried Claude Code CLI.

I have found the tools to be extremely useful are in several cases:

  • Explanations: Gemini 3.5 Flash especially has been extremely good at explaining what is going on in SANO-san's DotCL and how it interacts with C# / CLR. I have found its explanations to be so clear that I have been saving some of them in various .md files herein.

  • Troubleshooting: When I hit a wall and can't figure something out, Gemini often is able to get me going again. And, I mean, much faster than my traditional Googling it and reading docs and staring at the screen. Embarrassingly, although I've been Lisping for almost 40 years now, I still sometimes make dumb errors like having a paren in the wrong place, which it finds very quickly. (I don't use paredit or other structural editing tools. I am happy this way. I do love "rainbow parentheses" though!)

  • Planning: Fleshing out my plans into more detailed plans, or finding problems or gaps in my plans.

  • Coding: When I have something well defined that I just don't feel like writing (usually because I've written it a jillion times before), I will ask Gemini to do it. I usually overspecify my instructions compared to what I have seen other tool users do, but it also means that in general the output gets pretty close to my standards and I can use it with only some edits.

Functionality Implemented

MonoGame Dungeon Slime features:

  • All Chapters 1-9 content
    • Classes are implemented as CLOS classes
      • The base CLOS Core class does callbacks into a MonoGame proxy Game class.
    • We don't use an XML file to define the Texture Atlas; we use a Lisp property list.
  • All Chapter 11 content (input management)
    • KeyboardInfo, MouseInfo, GamePadInfo, and InputManager CLOS classes
    • Frame-to-frame state tracking for "just pressed" / "just released" detection
    • Timed vibration management for gamepads
  • All Chapter 12 content (collision detection)
    • circle CLOS class with boundary functions and intersection testing
    • Screen boundary blocking for the player slime
    • Screen boundary bouncing for the enemy bat via Vector2.Reflect
    • Slime-vs-bat trigger response with random respawn
    • Rectangle intersection and containment utilities
  • All Chapter 13 content (tilemaps)
    • Tilemap loaded via S-expression lists instead of XML
    • Tilemap and Tileset CLOS classes
    • Room boundaries scaled according to tilemap
  • All Chapter 14 content (audio)
    • Looped background music playback using media-player static methods
    • Bounce sound effects triggered on screen boundary collisions
    • Collect sound effects triggered on slime-vs-bat collisions
    • Automated loading verification test suite in audio-test.lisp
  • All Chapter 15 content (audio controller)
    • audio-controller CLOS class for centralized audio lifecycle management
    • Tracks and disposes of SoundEffectInstance objects automatically
    • Global mute state (M key) and volume controls (+ / - keys)
  • All Chapter 16 content (SpriteFonts)
    • sprite-font.lisp with load-font, measure-string, draw-string helpers
    • Score tracking system (migrated to gameplay-scene)
    • Score increments by 100 when slime collides with bat
    • Score text rendered in top-left corner using 04B_30.ttf pixel font
    • SpriteFont loaded via proper MonoGame content pipeline
  • All Chapter 17 content (Scene Management)
    • Pure CLOS-based scene lifecycle (scene class) specializing initialize, load-content, update, draw, and dispose.
    • Staged transition queue (active-scene and next-scene slots on core) to prevent frame-middle transitions.
    • Custom title-scene displaying a title, centered logo, and pulsing "Press Enter to Play" text.
    • Custom gameplay-scene housing the slime-eating-bat gameplay loop.
    • Return to title screen on Escape from the gameplay scene; exit only from the title screen.
    • Explicit scene disposal and CLR garbage collection invocation.
  • All Chapter 18 content (Texture Sampling)
    • Scrolling tiled background pattern on the title screen using sampler-state:+point-wrap+.
    • Two separate sprite-batch rendering blocks in a single frame to apply different sampler states (PointWrap for tiling background, PointClamp for crisp UI/text rendering).
    • Scrolling offsets updated using delta time and wrapped seamlessly via Common Lisp's standard mod function.
    • Fully uses the generated cspackages/ wrappers to avoid direct C# or dotnet: interop calls.

Basic in-game REPL:

  • Launches a super simplistic REPL background thread that uses standard input and output. Control-D will exit the REPL.
  • Demonstrate the REPL interacting with live game state with (setf color-cycle-period 1.0) to speed the color cycling.
  • Evaluate (exit) to quit the game.
  • Submitted change for dotcl-repl with console-read-key-interruptable to use a busy wait loop checking Console.KeyAvailable and sleeping for 50ms intervals, trapping ThreadInterruptedException so that the REPL thread can be interrupted cleanly.
  • REPL is started and stopped by the MonoGame Game.Run() by hooking into BeginRun() and EndRun()
  • Note that the background REPL spawned when running from a top level REPL session does not interact well if the top level REPL is using rlwrap like rlwrap --always-readline dotcl.

MonoUtils Lisp Package written in C#:

  • Proof of concept "MonoUtils" package written in C#.
  • Provide a shim in monoutils.lisp.
  • Have the top level .asd file include this shim and make the main.lisp depend on it.
  • Proof of concept "Add3" function exposed to Lisp as: (monoutils:add3 1 2 3).
  • Implement a monoutils:invoke-generic as an analog to the existing dotnet:static-generic. It supports resolving generic type arguments via Lisp type aliases (e.g., '("TEXTURE2D") instead of the fully qualified '("Microsoft.Xna.Framework.Graphics.Texture2D")) registered in dotnet::*type-aliases*.
    • Used for calling Load<Texture2D>() dynamically.
  • dotnet-p: Tells if the argument is a Lisp-embedded dotnet/C#/CLR object.
  • boxed-dotnet-p: Tells if the argument is a boxed dotnet/C#/CLR object, created with dotnet:box.
  • get-type: Returns the Type object of a DotNet object, or nil if not a DotNet object. But if the arg is a string, tries to get the DotNet type of that name.
  • get-type-full-name: Returns the string of the type name from get-type or nil. So if you pass it a string, it should return the same thing.

MonoGame Framework Classes: Texture Regions, Sprites and Texture Atlases:

  • Implemented the CLOS classes texture-region, sprite and texture-atlas to manage sprite sheet subdivisions.
  • animation and animated-sprite contain sprite lists (and delays), and then implement them for MonoGame tapping both the Draw and Update callbacks.
  • Implemented ta-from-file in texture-atlas.lisp which loads a texture-atlas from a Lisp form description file (like Content/test-atlas.lisp), converting symbol/keyword region names to strings for the atlas registry. It is accompanied by a validation test that runs on startup.

Lisp utility functionality:

  • safe-read-form-from-file is used to securely load Lisp-based texture atlas descriptions without read-time evaluation.

Where to Go Next

Credits

The font 04B_30.ttf is the correct pixel font from FontSpace (04b03 font), which is used as Content/fonts/04B_30.ttf in this project.

Chapter 20 Notes

Source Code

  • Core is just the Game1 class renamed

Some Helpful REPL Commands

Package Generator Metadata

After make repl:

;; Find stuff in the metadata - a CL list of plists
(defparameter y (safe-read-form-from-file "cspackages/MonoGameGum.lispy.metadata")) 
;; All the classes FQN
(mapcar (lambda (i) (getf i :fully-qualified-name)) y)
;; All the classes names
(mapcar (lambda (i) (getf i :name)) y)
;; A single class
(find "ContainerRuntime" y :test #'string= :key (lambda (plist) (getf plist :name)))

Use and Integration of C# Assemblies as Lisp Packages

The cspackages/ directory is machine-generated (never hand-edited) by the external dotcl-packagegen tool, (also designed by this package's author), driven by the --assembly/--class list in the Makefile's cspackages target. make cspackages regenerates it; the result is committed, since generation itself needs a already-built project's assemblies present to reflect over. Each requested C# class gets its own small Lisp package wrapping its constructors, methods, and properties as ordinary Lisp functions (e.g. v2:new, rect:contains), plus one shared csharp-generics package unifying same-named methods across classes into CLOS generic functions dispatching on eac object's .NET runtime type. Most generated packages call dotnet:resolve-type (safely deferred via define-symbol-macro, so it's fine as an ordinary ASDF dependency).

However, csharp-generics.lisp specifically resolves classes at Lisp read time via #.(dotnet:class-for-type ...) specializers, that one file can only be compiled once the .NET assemblies it wraps are already loaded into the process.

So, dungeon-slime.asd pulls in cspackages/csharp-assembly-packages.asd as a normal ASDF system via :depends-on (after setting up ASDF's search path), but the --no-csharp-generic-in-asd excludes csharp-generics.lisp from the generated ASDF system. dungeon-slime.asd then re-adds that single file as an ordinary file component, positioned after type-aliases.lisp, the file that loads C# assembliens such as MonoGame.Framework.dll.

From there, application code just uses the generated packages like any other Lisp package, typically through a short :local-nickname declared in packages.lisp (e.g. v2 for microsoft-xna-framework-vector2), calling things like (v2:new 1.0 2.0) or referencing a generated constant directly, e.g. v2:+zero+. Most functions are also available via the generics package, e.g., csg:+ or csg:dispose. This makes using C# classes & assemblies much more Lispy, though there are some sharp edges.

See doc/issue-49-continued.md for the full story of why this split was necessary. Hopefully a future enhancement to DotCL will obviate this requirement; see DotCL issue 49 for details.

About

dotcl Common Lisp implementation of the MonoGame 2D example Dungeon Slime

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages