Hacker News new | past | comments | ask | show | jobs | submit login
Technical overview of Kandria, a game and game engine developed in Common Lisp (tymoon.eu)
343 points by trocado on July 11, 2022 | hide | past | favorite | 32 comments



Heya, I'm the author of the article. I've been working on Kandria full time for close to two years now, and on-and-off for some years while I was doing my Master's at ETH. The engine it's running on, Trial, is a few years older still, and initially just started out of the good ole university boredom and curiosity. Games is how I got into programming when I was about 6, and that particular fever hasn't left me yet it seems, ha ha!

Kandria itself started as a "more serious" attempt at a game after I'd been making jam games with Trial for some years. I wanted to create something bigger, and had just finished playing through Celeste, so I just set out to replicate its mechanics in my own way. You can still find some demos of those very early days on my YouTube channel, if you're curious. In any case, the project went through several big changes until it turned into what it is now. During the pandemic in 2020 I finally decided to drop out of my Master's to pursue the project full time. At that point I also brought on a couple more people to help with the writing, art, and sound.

We're currently also running a Kickstarter to get us some extra funds for the project. It ends on Thursday 14th, 12:00 CEST, so there's not that much time left in it. If you'd like to support our work, please have a look: https://www.kickstarter.com/projects/shinmera/kandria The campaign page has a lot more details on the project and the team, if you're curious about that.

In the article I primarily cover the programming parts, giving a surface overview of the technologies involved and at least giving a brief hint at the methodology I employ to create games. I'm sure I've left out a lot of details that you might be curious about, so if you have any questions, please feel free to ask and I'll try my best to answer!


How have you funded yourself (prior to Kickstarter)?

I'm in a similar position to you (indie developer, working on my first project of size that I am polishing with intent to release). I haven't quite been able to square up a way to work on my game full time -- I suspect out of a lack of ingenuity or resourcefulness on my part. You seem to have solved this problem, though.


By being lucky enough to have savings, and having a room at my parents' house that doesn't require any rent payments. Without those, it definitely wouldn't have been possible.

Recently I've also received some grants, namely the Pro Helvetia interactive media grant, and the Poland Digital Dragons Accelerator. Both of those only offset a part of the money I had already personally invested though, and the Kickstarter barely makes a dent in anything.

Selling games is such a gamble, too. If the stats are to be believed, only about 15% of games on Steam recoup their investment, let alone making profit. For the next project's funding, I'm going to try and look at some other source like Patreon, to at least offset my own measly costs so I can spend longer in pre-production.

Making games is just really expensive, and any funding source is heavily fought over, or comes with huge caveats attached. I don't think I have this figured out yet either, so all I can say is best of luck!


This project looks awesome! I really like art, and it's exceptionally cool that you're using Common Lisp. As someone that often gets lost in the engineering (aka "fun") aspects of building things, my one piece of advice would be to make sure you put enough energy into marketing your game to ensure it reaches as wide an audience as possible and none of your efforts go to waste. Looking on reddit, I see a lot of posts in r/lisp, which is cool, but I'd work towards building as much buzz as you can in the indie gaming community (little teaser videos, etc). I'd also consider posting on the Tigsource forums, as that's a good way to build roots in the indie community. Anyway, looking great, and best of luck with your launch!


Thanks! Yeah, marketing is definitely a weak point, in large part because it's just something that goes very much against my character. I've tried posting to reddit before, but most communities' strict posting rules about promotions make that a pain, or simply not worth bothering with due to low conversion rates.


From the post:

> The same doesn't apply to the player sprite, as Aseprite takes minutes to compile the atlas for the nearly 1000 frames we have.

Why does compiling a sprite atlas for the player take 60ms/frame?


I'm sure Aseprite could be further optimised to press down the constant factor, but it is performing a rect packing algorithm among other things, which is an NP-hard problem.


The bin packing problem is NP-hard, but rectangle packing is "merely" NP.

However, most people just use a recursive biggest-fit-first ¹), a simple heuristic that is surprisingly hard to beat for most workloads.

I couldn't figure out what Aseprite is doing from their website or documentation, but if it's not that then it might be worth it writing your own sprite packer.

¹) https://codeincomplete.com/articles/bin-packing/


Yeah I don't know what they use either, but so far repacking that atlas has not been frequent enough that writing my own packer would save time.


This is a nice write up.

The dark side is that they've put years into developing the core infrastructure to empower their Lisp game journey. Ideally that's all usable by others to "stand on the shoulders", so to speak, but that's always a challenge.

I honestly have never had to work on a "running image" of any consequence. I've never worked on anything more than a few thousand lines of code. I've never needed any kind of source level "step" debugging. The ability to redefine a function quickly, I just throw in some prints to figure something out.

Reloading the entire source file was always fast enough to now warrant hunting down any of the more interactive mechanisms available. This is all across using simple vi, or emacs, or even LispWorks.

  (defun l () (load "src.lisp"))
Just save the file, and type that.

All that said, my current low level fantasy is something akin to Electron, only with Common Lisp. First class DOM, first class event hooks, into a CL runtime that you can deploy cross platform. Dunno if that exists or not.

I'm sure at some point my codebase will grow to the point where I would need to "level up" and become more intimate with SLIME, packages, system definitions, etc.

I've just not crossed that threshold yet.


> my current low level fantasy is something akin to Electron, only with Common Lisp. First class DOM, first class event hooks, into a CL runtime that you can deploy cross platform.

Have you checked out https://github.com/rabbibotton/clog ? I have only played with a couple of the tutorials, but it seems to be targetting something like that, albeit without an included browser.


The other direction is https://nyxt.atlas.engineer/ , which is a CL-driven browser. It's presented as a general-purpose browser, but you can also treat it as a standalone CL with a browser front-end, and I think that's the developer's long-term goal -- a bit like how emacs is a lisp with a text editor front-end.


> Reloading the entire source file was always fast enough

The point is that you might want to keep the state.


What kind of software do you usually create this way?


Essentially listener utilities (vs command line utilities). Code designed to solve specific problems, typically for their side effects and artifacts vs deployed code in production systems. One off simulations, etc.

No UI but the listener.


>> 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'm at that stage in my lisp journey (~18 months) where i know enough to be dangerous to others around me, i.e. i'm still a lisp n00b but have completed a few lisp & scheme books and written enough projects that i feel fairly productive. BUT i haven't reached, never mind come out the other side of, the "trough of disillusionment" yet. So everything lisp is unreasonably tainted with a positive glow in my mind.

TL;DR i still think lisp is pretty awesome. It's my newest hammer and everything looks like a lisp-shaped nail these days. So i have to be deliberate and careful not to over-use lisp but it's totally changed how i write js for example. It's infected my brain.

Here's the thing - that workflow statement by the OP is a fair chunk of the attraction to lisp for me. Before lisp i "knew" what a REPL was - i've been using python since i switched wholesale from Perl in the 00's, i thought you couldn't teach me anything new about REPLs, even quasi-REPLs like Jupyter were entirely familiar and "basic" to me. If you're a lisper, you know i was wrong about that, if you're not a lisper you're probably wondering what's the big deal.

The other chunk - and i'm only really scratching the surface so far - has been macros. I think so far i prefer the non-hygenic type, but i haven't gone through Let over Lambda yet (although the book is on my bookshelf and is only 2 titles away from being started)

For me, lisp opened my eyes to a way of working that i just wish i could fully replicate in other languages. Lisp has made me re-appreciate python and js in different ways now.

If you want a really short version - Brett Victor's famous inventing on principle talk sums it up for me (he was using JS there, so there is hope...)


> it's totally changed how i write js for example

This may be a silly question. But could you give a couple examples of how you implement something in JS now vs. how you would have implemented it 18 months ago?


Learning a little haskell a million years ago was a revelation. In my next bit of JS (a large, non-web project) I did lots of FP in it - and for the better, not just because i could. I really cut down on boilerplate, made it more readable and reusable, and... it was just great.


i would wager that after having used LISP, you start thinking bottoms up, rather than top down.

i.e, instead of thinking about a program as a large one, and break it down into smaller, and smaller components or pieces, you'd start with the most bottom-est, most small piece first. Even if that piece isn't useful on its own - or it's only of limited use. Then you'd build another small, may be even orthogonal piece.

Once you have these small pieces of individual functions, you might end up building a glue (sort of a language or custom macro?) to bind them together.


Great write-up. Indeed, the journey with Lisp is an interesting one and what you have witnessed seems to parallel many others experiences.

What I found on my journey with Lisp:

- I learnt Emacs well and learnt the beauty of its codebase and the thoughtfulness put into the program. Its' levels of documentation and customisation and being a lisp machine in its own rights was an enlightening experience for me when designing my own software (i.e. document well, document in a standardised way, document thoughtfully (think of what the users would want to know) and providing customisability to end users (which to be honest, dynamic langauges like Lisp excel at because they are much better suited for runtime manipulation and optimisation

- The power of macros to change one's language. Now admittedly, this took some time to get a handle on. You kinda need to be proficient in Lisp before you start doing some super funky stuff and mould your language and program together into one (Ala PG's On Lisp). But having the ability to change the language at _compile_ time is simply, utterly, most imaginably amazing!

- Writing clearer code: Because Lisp has very little punctuation syntax (not to be confused with the syntax of certain special forms), you cannot hide behind boilerplate code as much and you end up having to face your own thoughts more directly in the code you write. It is tough going at the start (who would have thought a _simple_ language would actually be hard to program in well - basically the more complexity in a language, the more restriction to your expressiveness, the more simple a language, the more power to you to fully express yourself by combining orthogonal blocks...speaking generally here as sometimes complexity does help in other ways (e.g. see CLOS))

- Learning from the greats in Computer Science - there is so many good ideas and thoughts embedded into the lisps, that you inevitably learn a lot of good things by immersing yourself in the language and its history (of course over time)

And finally, 'Life Beyond Lisp'. I no longer program in Common Lisp as my main language because I do need some libraries that I cannot do without. But as you alluded in your post on writing better JS after Lisp, I too have significantly improved my programming and code clarity in these other languages, after learning Lisp. Of course, one day I hope to program exclusively in CLOS, but until that day, Swift remains my near friend (at least it has generic functions).

I can wholeheartedly recommend Common Lisp to anyone who has some spare time and curiosity to improve their skills in our profession of computer science. Also (and this is something Lispers don't seem to do that well), try other languages! Learn from them all in my humble opinion. Computer Science is a wonderful thing :)

p.s. Congrats to Kandria on their progress. Truly a wonderful team and their main author is a wonderful open-source lisper.


Thanks for that. Are you able to tell us what books got you to where you are now? I've just started Emacs Lisp An Introduction as I'm wanting to dive right into Emacs (coming from a life-long Vimer), but I'd also want to expand to other Lisps.


Could you expand on the REPL bit? Having worked with JS, Python and Elixir, they feel more or less the same. What’s Lisps have that these don’t?


So, I would say the idea of dynamic redefinition is probably the big thing: In cl I can redefine a class and have all instances updated while the program is running.

I would also say that the error management is also beyond fantastic. Say you run a function, and deep inside the call stack there is an error. In CL you can actually update the faulty function and just continue executing at the point of the error.

For someone like me without any intellectual rigour at all, this lends itself to a kind explorative programming I really like.


The Lisp instinct is to make a change in a file and immediately hit C-c C-c in Emacs, at which point the change goes live in the system - no matter if it's a variable or a function that you have changed or something else.

You don't need to reload the full module like in BEAM languages like Erlang or Elixir, you can modify a single function or a form without the need to make a version upgrade to the processes running your code - the next time any thread, including e.g. an already running game loop, ends up calling the new function, it's the new version of the function that will end up being called.


Version upgrades are not needed on BEAM anymore than they are in Lisp, you can just reload a module and code will start using the new version.


TIL, that's a big thing! When was that change made?


I don't think it's recent, as far as I know. Versioning, appup / relup, etc. are really for upgrading production deployments live, when you need special instructions to pause processes or update their internal state before loading new code. In development, you can ignore all that and just reload modules directly. e.g. with "r" in the Elixir REPL.

Once that's done any external call (`MyModule.add(1,2)`) will go to the new version of the code, while local calls (`add(1,2)`) stay on the same version.

The Phoenix web framework will also auto-reload any module that has changed on disk when handling a web request in dev mode.


I personally prefer SLIME over any IEX client I've ever seen, but that's not a limitation of Elixir. Since Erlang has a Smalltalk-esq image just like CL does, the Elixir REPL is in principle as powerful as a Lisp's. In practice CL has lots of additional little things which are nice, like the ability to update instances in place after changing a class or DEFVAR forms which don't clobber their contents on reloads, but for me most of the advantage comes from just how well designed SLIME is.


I'd really like to understand how live reloading works a bit better, especially when editing variables that some running thread depends upon. Is there an overall strategy here? Do I implement locking around variables that I intend to play with at runtime?


All of the contention issues that we've had for ages across all platforms exist in CL. Loading and compiling new code isn't any different than two separate threads calling setq on the same element at the same time.

The reality, though, is that the vast majority of the time when this is done, the system is idle. If you live update your server that's serving 1000 connections and isn't specifically coded to handle this potential event, then, for sure, there can be trouble.

But if you're hot reloading your dev server, with an idle connection, then you're likely to have fewer problems.

However, there's the potential impact of captured state that is not properly refreshed. For example, if you have some long running state that, say, captured a function via #'my-func, then that routine has, indeed, captured the pointer to the routine. But when you redefined it, the symbol (i.e. my-func) is now pointing to the new code, while your existing state is pointing to the old code. Any code that uses the symbol to call the routine will update automatically, but anything that does not, is stuck with stale data.

And that rock potentially sends different ripples through the still pond of your running image. Just something to be aware of.

It's a great feature, just need to be aware of what's going on when you do it. Most of the time, it's a non-issue.


I don’t believe the Common Lisp specification says anything about threads. For example here[1] is the relevant section of the SBCL manual. As you can see it’s quite low level. If you’re using those primitives then yes you’ll have to manage barriers, synchronization, use atomic operations, or otherwise maintain sanity. The REPL (or SWANK) thread is just another thread.

Edit: I should add that the usual pattern is Bordeaux threads or some other higher level thing.

[1] http://www.sbcl.org/manual/index.html#Threading


With the threading model of most CL implementations, you really only need locking if you need to change two (or more) places and have the update appear atomic, or if you are doing a read/modify/write. But you would already need such infrastructure in place if you were going to modify them in you program as well.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: