Technical overview of Kandria, a game and game engine developed in Common Lisp
reader.tymoon.eu
reader.tymoon.eu
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!
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.
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!
> 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?
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.
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.
No UI but the listener.
The point is that you might want to keep the state.
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.
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...)
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.
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?
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.
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.
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.
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.
Edit: I should add that the usual pattern is Bordeaux threads or some other higher level thing.
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.