Kandria

An Overview of Kandria's Development with Lisp

https://filebox.tymoon.eu//file/TWpVeU5nPT0=

This is a short overview of the development that's gone into Kandria, the open world action RPG I've been working on in recent years.

With the Kickstarter for Kandria making the rounds recently, there's been some increased interest in how exactly I go about making games like it in Common Lisp. I thought I'd try distilling some of that curiosity and write about it specifically for Kandria, but it's a bit difficult to speak about such a broad thing as game development generally. I've decided to cut things into a rough six sections, and will talk a bit about each of them.

If there's a specific section or detail you would like to have more detail on, please feel free to let me know, and I'll consider writing a more in-depth follow-up article. For now, the sections are ordered in roughly the same sequence as one considers things when developing a game.

Engine & Support Systems

Kandria runs on top of a multitude of support libraries. At the core of it all is the Trial engine, which ties a lot of these libraries together and builds a base set of tools and systems to make games with. Most importantly, it takes care of many of the tedious bits such as:

  • Managing the OpenGL context and OS window

  • Dealing with inputs from keyboard, mouse, and gamepads

  • Mapping inputs to actions and tracking action states

  • Presenting an event loop and scene graph

  • Handling resource allocations and asset loading

  • Generating geometry

  • Manipulating data in GL buffers (VBO, UBO, SSBO)

  • Integrating with Harmony for sound

  • Offering a system to combine shaders and manage render passes

  • Automating deployment (more on that later)

  • Storage of settings and save files

  • Handling of localisation strings

  • Error handling on target systems

It also offers some specialised support for 2D games, such as:

  • Animated sprite rendering and import from exported Aseprite (a pixel art tool) data sheets

  • 2D bounding volume hierarchy spatial query structure

  • 2D camera control

There's more it can do, though these are the bits that are most relevant to Kandria. There's also a bunch of 2D tech in Kandria that I would like to backport into Trial, most notably the tilemap rendering system and collision solver. It's a bit tricky though, as I need to generalise things when backporting, which brings some architectural problems to solve. I haven't had the time to solve those yet.

Many of the support systems Trial makes use of I had to write myself, such as libmixed, cl-mixed, and Harmony for sound, cl-steamworks for Steam integration, cl-gamepad to handle gamepad input sources, glsl-toolkit to parse and merge GLSL shaders, systems to send crash data and display emergency messageboxes to the user, and more.

A lot of these libraries started before work on Kandria went full-time, but even during Kandria's full production time, I've had to rewrite large chunks of some of them, not to mention Trial itself, to fit new requirements, or fix long-standing bugs. All of this is to say that there's a tonne of work that goes into support systems, and even with all of the above it's still a far cry from being as complete as I'd like it to be. For instance, it's missing a lot of support for 3D games, physics simulations, etc.

One thing I do have to mention though is that the workflow in Lisp allows me to create these support libraries much faster than I can in other languages. I know this for a fact, as I've also ported libraries to other languages, so I have a pretty good idea of what my comparative speed is. I think for the most part this comes down to two things for me: first, because Lisp has many comfortable properties such as easy macros, convenient program flow, etc. I can build a first draft very quickly. And second, because of the interactive development environment I can test and fix issues much faster to get the library to a working state.

Whenever I have to build a library in C or another language with a decoupled build phase, writing, recompiling, and re-evaluating the tests is many orders of magnitude slower and more tedious, which both just makes the process itself slower, but also just makes me less motivated to test stuff in the first place.

Anyway, with all this base functionality in place, let's move on to the step where a game actually gets made.

Gameplay Systems

Building on top of the base that Trial provides, I create the various game-specific systems. To give you a brief idea of what that looks like in code, here's the default demo file for Trial, which creates some assets, a player that can move about, and finally constructs a sample scene.

(define-pool demo)
(define-asset (demo cat) image #p"cat.png")
(define-asset (demo cube) mesh (make-cube 15))
(define-asset (demo grid) mesh (make-line-grid 10 100 100))

(define-shader-entity player (vertex-entity textured-entity located-entity listener)
  ((name :initform 'player)
   (texture :initform (// 'demo 'cat))
   (vertex-array :initform (// 'demo 'cube))))

(define-handler (player tick) (dt)
  (when (retained :w)
    (incf (vz (location player)) (* dt +50)))
  (when (retained :a)
    (incf (vx (location player)) (* dt +50)))
  (when (retained :s)
    (incf (vz (location player)) (* dt -50)))
  (when (retained :d)
    (incf (vx (location player)) (* dt -50))))

(defclass demo (main) ())

(defmethod setup-scene ((demo demo) scene)
  (enter (make-instance 'fps-counter) scene)
  (enter (make-instance 'debug-text :text "HELLO and welcome back to VIDEO GAMES") scene)
  (enter (make-instance 'vertex-entity :vertex-array (// 'demo 'grid)) scene)
  (enter (make-instance 'player) scene)
  (enter (make-instance 'following-camera :target (unit 'player scene) :location (vec 0 100 100)) scene)
  (enter (make-instance 'render-pass) scene))

https://filebox.tymoon.eu//file/TWpVeU13PT0=
The game put together by the demo file

So aside from assets and such, most of the work is done by defining classes for your game entities, attaching handlers so they can respond to events, adding new methods to customise the behaviour of various engine bits, and so forth. Almost all of the engine tech is based around the Common Lisp Object System, so that users can easily customise and extend it.

For Kandria there's a lot of gameplay systems that had to implement. The most important bits would be:

  • A collision testing and resolution system

  • The player movement mechanics

  • An advanced 2D camera

  • An efficient tilemap renderer

  • NPC AI and player interaction systems

  • A dialogue system

  • A quest system

Now, typically engines will provide systems for physics handling already. In Trial this is unfortunately not yet the case. However, for 2D platformers I also feel like the interaction between the objects has to be so extremely specific to work well, that in practise it might be easier to just roll your own. In any case, Kandria contains a very specific collision system that works for it, but probably not too much else. This has gone through a number of revisions and complete rewrites as well. Getting slopes and moving platforms down right is really tricky.

Similarly to collision itself, player movement is extremely particular for a platformer. I don't like writing big files of code, it makes it hard for me to keep track of where things are. Despite this, the player file in Kandria is almost 1500 lines long. Now, granted, it also has some other bits for interactions in there, but most of the code is to handle the player inputs and movement mechanics. There's a huge amount of special cases that have to be handled to make things feel good. All I can say is thank heck I can recompile functions and change constants on the fly, otherwise testing and refining all of this would have taken me forever.

The tilemap renderer is implemented as a fragment shader, looking up each tile in a texture. This means I can have virtually infinitely large tilemaps, all at a constant render cost. This is a technique that I'd definitely like to backport into Trial, but once again things are a bit more tricky. Kandria has a rather complicated lighting system with real time shadows (a very weird thing for a side scroller) and per-pixel normals. I'm currently not sure what the best way of going about generalising this system is, but I'll probably end up just backporting an unlit tilemap renderer, leaving advanced lighting extensions up to the user.

https://kandria.com/media/kandria.gif
An example of the real time lighting and shadows

While the previous systems can definitely be replicated easily in other languages, the dialogue system is one of the far more tricky bits. At the basis of it all is Speechless, a general dialogue scripting language. It's similar to languages like Ink in that it's an ascii markup language with features specifically made to handle complex dialogue branching and flow. Interesting for us in specific though is that the branch conditions, placeholders, and so forth can contain arbitrary lisp code. The entirety of the dialogue format, including the Lisp code bits, are compiled down when the dialogue is loaded, too.

? (not (null (active-race)))
| ~ catherine
| | You're already on the clock for a race right now. Do you want to abort it?
| - No
|   | You got it.
|   < quit
| - Yes
|   | Alright!

~ catherine
| [(null (active-race)) Ok, time for another race!|] This time, you'll have to go to "the far east side"(orange)!
| Ready... set... go!
! eval (setf (active-race) quest)

This allows our dialogue snippets to execute some rather complex logic bits, and even change arbitrary parts of the game state. This has been useful many times to handle more extravagant things such as showing a timer on the HUD for the time trial quests, without having to modify the dialogue language itself, or adding specific integration bits to allow that to happen.

The quest system is implemented as a bunch of classes and macros. Quests are then defined by just using these definition macros to lay down the metadata for all the quests. And again, we can make use of arbitrary Lisp code to change and query game state, removing the need for specific integration layers. Also fun is that the system allows live redefinition, so you can change quests and dialogue on the fly while the game is running, and even while the quest is active.

While all of these systems are definitely cool, they aren't really of any use if you can't see the game at all, so let's move on to the next step, which is to integrate visuals and assets.

Asset Integration

A large part of the asset integration is already provided by Trial out of the box; you can load images, sound effects, music tracks, sprite sheets, and even models pretty easily. All you have to do is define a pool where your asset files reside, and then define an asset for each file you would like to have loaded in, along with any needed parameters to map it correctly like texture scaling parameters and so forth. While that gets you pretty far, in Kandria I needed some more things besides that:

  • Combat frame data

  • Music track behaviour

  • Tileset metadata

Kandria is not just a platformer, but also includes action RPG elements, and in particular a combat system. For this the animation data stored in Aseprite alone is not enough. The characters also need to physically move according to their animations, and we need to define hurtboxes, effects, and other attributes like invincibility, stun-timing, and so forth. All of this amounts to a tonne of extra information for each frame that needs to be tracked somewhere.

I really wish Aseprite supported arbitrary metadata per frame, but alas. So instead we keep that information in an s-expression file alongside the Aseprite metadata json. When loading a sprite in, it then matches up the Aseprite metadata with the extra per-animation and per-frame data from our own extension file. Internally we track the extra info by extending the base sprite animation and animation frame classes from Trial with our own subclasses.

(:source "player.ase"
 :animation-data "player.json"
 :palette "../texture/player-palette.png"
 :animations
  ((STAND                :start   0 :end   8 :loop-to 0   :next STAND :cooldown 0.0)
   (LOOK-UP              :start   8 :end  11 :loop-to 10  :next LOOK-UP :cooldown 0.0)
   (LOOK-DOWN            :start  11 :end  15 :loop-to 14  :next LOOK-DOWN :cooldown 0.0)
   ...)
 :frames
  ((:damage 0   :stun-time 0.0 :flags #b0101 :effect NIL        :acceleration ( 0.0  0.0) :multiplier ( 0.8  0.8) :knockback ( 0.0  0.0) :hurtbox ( 0.0  2.0  0.0  0.0) :offset ( 0.0  0.0)) ;   1 STAND
   (:damage 0   :stun-time 0.0 :flags #b0101 :effect NIL        :acceleration ( 0.0  0.0) :multiplier ( 1.0  1.0) :knockback ( 0.0  0.0) :hurtbox ( 0.0  0.0  0.0  0.0) :offset ( 0.0  0.0)) ;   2 
 ...))

For our music, we have a rather complicated approach: each visible chunk of the game has an associated music "environment". This environment encapsulates two types of tracks, a primary music track, and an ambient sound track. For instance, on the surface the environment used would be "desert" and "faint winds", but in the surface camp it would be "camp" and "windy buildings", etc. Each of the different music tracks is further parametrised by an intensity level. Most of our tracks have an "ambient", "quiet", and "medium" variant, some also with additional variants that turn the vocals on or off. Each of these intensity levels is designed so it can be transitioned between at any point, resulting in what is called "horizontal mixing", allowing us to adjust the music to the intensity of the story.

Finally, we also require additional metadata for our tilesets. In particular, we need to know what tiles represent what kind of solid in order to create the 2D shadow volumes, and we require even more detailed information to allow the editor's auto-tiling to work. This data is again kept in a separate file, where I rather tediously have to associate a type of tile with every matching tile in the set.

A lot of this extra data also ties directly into the editing and tooling, so let's get on with that.

Editing & Tooling

Another very important part of game development is the tooling. Without good tools, everything is just so much more of a pain to do. Unfortunately, I also believe that this is where things are the most lacking. Powerful editors are a huge part of what make up a modern engine like Unity, Unreal, or Godot, after all.

In our case, I had to start developing a new UI toolkit from scratch. This is no simple task by any stretch, and would easily fill the better part of a decade of full time dev to get to a good state, if not an entire team for that time. Anyhow, what I use now is called Alloy. It powers all the UI used in Kandria, both in-game menus and hud elements, as well as all of the editing tools.

While it has the most base necessities I need to make things happen, there's a ton of rough edges and missing features that I would dearly like to have fixed, but simply lack the time to get to. This is also the aspect I would like the most to get help with. I think Common Lisp in general is in a rather dire situation UI-wise, and I desperately wish that we could improve that situation. There are other efforts, but each of them have severe issues: CommonQT (heavy, severely outdated, foreign blob, Qt5 port still not ready on Linux/MacOS), McCLIM (X dependent, hard to style, proper font support requires foreign blobs), CLOG (requires a full browser), etc.

It really seemed like the easiest way to get an OpenGL toolkit that didn't land me with even more memory faults than I knew what to do with, was to write one. And Alloy is very nice in that respect, there are no nasty segfaults or other surprises arising from a foreign blob. I think I got quite far with it for the very limited amount of time I put into it, and I also got to try out a few ideas I've had for UIs for a while. Some of those ideas worked out and others did not. I'd like to talk more about Alloy and its current state in another article, hopefully I'll get to writing that one sometime relatively soon.

In any case, written in Alloy we have a full in-game editor that you can toggle on and off at any time during gameplay. With the editor you can place and edit entities, place chunks and edit their tilemaps, advance frame-by-frame for editing, change the lighting conditions, inspect AI routing, tune and edit frame-by-frame combat data, and more.

https://filebox.tymoon.eu//file/TWpVeU5BPT0=
Kandria's in-game editor

It's really a quite capable system, though it's of course still quite rough in a few places. Fortunately we reached the second stretch goal of our Kickstarter campaign so the editor will receive a fresh round of polish and will see an official release.

There's a few things I'd like the editor to be able to do that currently require code to do, such as inspecting and changing quest state, viewing quest flow, assigning of tile types in tilesets, defining light settings, music environments, and backgrounds. I'm not sure if I'll get to all of those, as I'm not yet sure how the editor would best manipulate such state, it not being part of the actual map and all.

Anyhow, if I make any future games, I'll definitely spend a lot more time in pre-production developing tools for it, as production speed depends massively on having good, comfortable tools to work with. Good tooling can also help a lot with the next section:

Testing

As noted at before, testing is where Lisp really starts to shine. The ability to just try out random fragments of code at the REPL is extremely handy, but while many languages these days have REPLs, its usefulness only really starts to blossom when the entire environment is built around interactive development like Lisp is. Every part of the system – functions, classes, methods, macros, variables, etc. – can be changed on the fly, even while another thread is chugging along doing its stuff.

If that sounds scary to you: it is! It's super easy to break stuff if you just go around changing things, but fortunately it's also really easy to pause threads, such as when an error opens the debugger, and then carry out your changes while the program sits idly by waiting to resume once it's safe to do so.

In any case, this flexibility allows you to fix bugs without having to wait for a slow recompile cycle, program launch, and recreation of your program state before you can identify whether the bug has been fixed or not. In many cases you just leave the interactive debugger open, implement a fix, then select an appropriate restart point in the debugger, and let it retry the operation.

https://filebox.tymoon.eu//file/TWpVeU53PT0=
An example of live function redefinition, where I update the collision behaviour of the fall-through platform

These things are part of the standard Common Lisp toolkit, but there's also several things I've done with Trial and Kandria in specific that further aid the interactive testing approach. First among those would be the ability to watch over asset files.

When an asset file is changed, it'll automatically recompile any intermediate files such as texture atlases from the source, and then reload them in the running game. This lets us iterate on things like changes to our tileset very quickly, as we can see the results of the changes in-game in a flash... usually, anyway. The same doesn't apply to the player sprite, as Aseprite takes minutes to compile the atlas for the nearly 1000 frames we have. Whoops!

Similarly to assets, quests and dialogue scripts can be changed and the game will take care to update the state in the running image as best it can. We also have convenient functions to switch around the quest state, making it easy to test particular quests or sections of the game's story without having to amass a heap of save files or having to replay the game from the beginning.

Aside from the editor I've already talked about, another thing that's been instrumental for testing is the cheat system I've implemented. It's pretty simple – the game watches any keys you press and tries to match whatever you're typing in against all cheat codes continuously. So if you, say, type in i can see forever the game will activate a cheat that unlocks the full map. We have a bunch of these cheat codes and they, together with the editor, have been really useful to quickly construct specific game states. If need be, I also sometimes run specific code to manipulate entities directly, such as adjusting coordinates to an exact number, and so forth.

Alright, so now that the game is as thoroughly tested as we can think it to be, let's talk about getting it out there.

Deployment

I've been deploying applications with Lisp for a long time, so I've got quite a bit of tooling and experience together to ease the process. The basis for everything is the Deploy library, which automates the discovery and bundling of shared libraries. Trial extends its functionality, so that for a new game all you'll need to do to build a binary for it is add a couple lines to your build description, and then just sbcl --eval '(asdf:make :my-project)', and presto, in a few minutes a ready-to-deploy binary will be there for you to send off.

Importantly, Trial also takes care of copying the assets from the asset pools for you, and handling path resolution when the binary is started up on a target machine. Now, one thing that's particular to Lisp is that programs are compiled in much the same way as development happens: incrementally. This means that essentially you start your lisp implementation, and then it one by one loads in your source files. The resulting state in memory is then compressed and dumped out to disk. When starting up, it simply maps that image back into memory and "resumes".

Industrious readers at this point may notice that this creates a bit of a problem when it comes to deployment: you can only create a binary for the same platform as the one you're running on. Cross-compilation is a no-go. Fortunately, at least for the most important case of building on Linux for Windows, there's a way out: Wine.

SBCL runs perfectly well under Wine, and I simply have an sbcl.exe in my path that I launch through Wine to dump out a Windows executable. This means I can build for both Linux and Windows from my host machine, and run the whole deployment process without needing virtual machines or anything.

But hold on, building on Linux is not quite that straightforward either! While shared libraries on Windows are usually a no-brainer, on Linux the situation is quite dire due to the way glibc linkage works. The easiest workaround for this I've found is to build all the shared libraries and your SBCL host you depend on in a VM that targets a much older kernel and glibc. You can then just copy SBCL and the libraries over and the rest works fine.

So I have two extra copies of SBCL on my system, including all needed shared libraries, which thankfully aren't many. Trial also includes a system that automates driving the builds for all supported target systems, the bundling of the resulting binary into an archive, and ultimatively the uploading of the new build to platforms such as Steam, itch.io, or an FTP or HTTP server. This even integrates with unix pass to make the login to services secure without needing custom authentication files or such.

This is then integrated with a simple build definition, allowing me to run the whole build including bundling and publishing to services in a single command like so: sbcl --eval '(asdf:make :kandria-release)'. This has worked perfectly reliably for me for a long time now and has made pushing updates to testers through Steam an absolute breeze.

Finally, on target machines we also employ a system that automatically delivers reports to us in case the game crashes, or if the user would like to manually report an issue. This has been instrumental in catching buggy builds or other system-dependent problems. The source code for the system that gathers the data on a server is publicly available and its client-side utility that gathers system information and sends a report also has an integration with Trial that automates the whole thing.

https://filebox.tymoon.eu//file/TWpVeU1BPT0=
An example of an automated crash report

Conclusion

Again, this is only a small slice of everything that goes into making a game on this scale. Asset production, level design, and all of the little bits and pieces that glue everything together add up to a lot.

Not surprising to anyone that has used lisp before, the testing phase is by far where the advantages of using a custom approach like this shine through. Being able to interrupt, debug, and change pretty much any part of the system while it is still running makes iterations way faster, and generally makes identifying where issues lie much easier as well.

Similarly unsurprising, the biggest challenges lie in the lack of support libraries. A lot of these libraries I have to write myself, simply because nobody else has written them yet. Of course, now that I've done a lot of the groundwork, you will not have to anymore, thanks to all of these libraries being open source on GitHub. Though, I still often wish I could benefit in a similar way :)

By the way, I also wrote a technical paper for the Game Industry Conference last year, which gives an overview on the advantages Common Lisp itself can give for game development. It doesn't talk about Kandria, but if you're curious about Lisp in general, it's worth a read.

The Kickstarter for Kandria is still live at this time, and is now in its last few days. Please consider supporting us, especially since the stretch goals should be of quite some interest to you if you're reading this article. Check it out here: https://kandria.com/kickstarter