Here are some of my favorite blog entries related to functional game development:
https://prog21.dadgum.com/228.html
https://prog21.dadgum.com/23.html
https://prog21.dadgum.com/24.html
Functional is all about transformation of data structures, and a game is nothing more than a function that takes {state, mouse, keyboard} as input and returns {new_state, output} where output is a set of Vulkan commands, or pixels that you bitblt to screen. Rinse and repeat in an vsynced infinite loop.
I think the linked articles above betray the fact that they were written 15 years ago when functional programming was a niche quasi-academic idea and no one was used to building stateful programs with it. 15 years ago it was all about Java and C++. Lisp was as forgotten as it is today. In 2024 most above average programmers have experience in functional logic, and it is not that arcane of an idea. The reason that it is not widely adopted is that functional programming is not as performant as imperative semantics, and you need all the speed you can get. On the other hand, imperative often means buggy mess as game complexity grows and painful debugging session trying to understand who and why changed this state variable.
But not all games are AAA FPS that require ingenious optimized codepaths to perform well, not in this day and age, so this is why you see commercial games running on "slower" abstractions such as C# (AOT compilation doesn't make it less of an abstraction layer), so you can very well design a commercial game in Haskell or Scheme or Lisp or Clojure if you wish. Nothing stops you, apart from the lack of serious game dev frameworks built in those languages.
It all depends on the compiler, really, and it is asymptotically hard to compile functional languages so they perform as fast as C for example. There is no market for "high performance Haskell or Clojure", so there is no compiler that good either.
JVM languages have a huge gap* in low-level capabilities where-as C# sits next to C and Rust in many of the features that it offers, even if the syntax is different. JVM implementations also come with significantly higher FFI cost. This makes them an overall poor choice for game development. Your experience of writing a game engine in pure C#, calling out to rendering and other device APIs will be massively better than doing so in Java/Kotlin/Clojure/etc, because of both runtime capabilities and ecosystem of interop libraries.
Also, C# has zero-cost abstractions in the form of struct generics which are monomorphized like in Rust, and performance-sensitive code relies on this where applicable in "modern" codebases.
* Projects like https://github.com/bepu/bepuphysics2 are impossible to implement on top of JVM without calling out to native components. This might change once the incubating Panama vectors improve upon their API and what they compile to.
The "sufficiently smart compilers" turn out to either not exist, or be beyond the ability of even the smartest humans.
This is not celebration of that, or condemnation that anyone tried. It's a major bummer, actually, and I am suitably bummed. I'd love to have the Sufficiently Smart Compiler. But wanting doesn't count for much. At this point if someone wants to argue that something at a Haskell level of "functional programming" can run at C speed routinely, they need to produce the compiler; we ran the gamut on mere theories.
(I have to qualify it that way because we do have a lot of evidence that you can have "functional flavored" languages that run much more quickly, like O'Caml (at least in single thread) and Rust, if you consider that "functional flavored". But straight-up Haskell does not appear to be able to "just" get transformed to C-speed code reliably.)
A lot of effort has gone into optimizing C compilers, Haskell is easier to optimize in theory, but in practice it isn't enough easier as to overcome the massively larger amount of effort put in C.
In the best case for both the code will be roughly the same speed. However the best case for both can look very different and any comparison is suspect as it is likely that whoever wrote the code wasn't as good at one as the other and so wrote bad code.
High-level languages like Haskell can and do approach the performance of handwritten C if you encode enough information to get the optimizations for free:
https://stackoverflow.com/questions/35027952/why-is-haskell-...
but that's a completely convoluted approach to identity in a model for a videogame. The reason Rich Hickey took that approach in Clojure to state and identity is because in the domains he cared about he wanted to prevent the destruction of past state. (he wanted to avoid what he called "place based programming" IIRC).
If you want to implement a git like versioning history say, you really care about any change to state as a collection of individually immutable snapshots. But in a videogame that's completely artificial. Nobody thinks about a persistent entity in a game as a collection of states at a million points in time. It's much more natural and performant to think of your entities as persistent and mutable and you basically only care about where they are now.
You care about what they are doing now, and what they should do on next render based on the rules of the game and the player’s input. To me, that looks a lot like (state, actions) => newState.
I’m not sure how much of a difference there really is between having objects track their own state vs having them pull their state from somewhere in a single State tree. The advantage to the latter approach IMO is that it simplifies cross-cutting concerns (“player deals bonus damage when all enemies are affected by poison”), and it helps immensely with debugging to be able to query the exact, full game state whenever it’s needed.
My brain has been rewired to find FP easier to reason about than OOP. This won’t be true for everyone, and it isn’t always the right tool. I think it works well for game dev but can be held back by performance concerns.
It's a costly business mistake to build your tech stack using any of the above languages. The trade off simply is not significant enough to matter vs using mainstream languages with 10,000x the number of mindshare.
Rust is starting to look like it will make the jump to likely to be around for the foreseeable future, but only time will tell.
Of course just because you can build it doesn't mean it will work. I know of an embedded system where the 16 bit CPU went obsolete and they discovered that the C code was not 32 bit safe and worse it relied on the timing of the one CPU it was written for (though not so bad as to have to turn off compiler optimizations). I know of a product written for X11 that is porting to wayland. Keeping code running for decades is a hard problem - and one that many fail to understand. (nor do they need to - the web site you do today will need a major rewrite to fit the latest UI fad no matter what you do)
Clojure won’t necessarily live as long as the JVM does, in that the language could someday be abandoned, but support for its hypothetical last language version won’t be somehow removed from the JVM.
Why not? Python2 is a good example - if they make a JVM2 without the cruft and then a transition they will leave behind those that don't transition. Of course that is the worst case and I'll admit unlikely.
(also the JVM isn't very relevant to me because I work in embedded systems without a JVM)
> If you write in something like C++ or Java running on Linux or Windows I'm confident that in 20 years you will find a tool that can build your code for the latest computers
What I wrote was based on your confidence in Java (the language) being around in 20 years. One of Java’s core features is backwards compatibility. Clojure’s implementation will continue to work as long as Java (the language) itself exists. There’s too much business depending on old Java software for the language to break like Python3 did.
Clojure is also a hosted language by design, and has been ported to JS, .NET, and BEAM (though maybe not completely on that one). If JVM2 were to come out, and if it supported garbage collection, it’s likely that porting it will be easy enough for someone to handle the task of transitioning the JVM1 build to JVM2. Implementing a lisp is not a rare hobby, and IMO Clojure’s language design makes it a particularly tasty flavor of lisp.
you won't be able to find them in enough numbers and it would be tough to gauge their proficiency too
unless you are purely interested in it for academic purposes, its best to avoid clojure and any sort of esoteric languages. Even Rust development is riddled with false roads and mirages.
i just want to save anyone reading this 5 years of their time. You don't get better when you are constantly having to re-invent the wheel for essentially shaving off roughly 20~30% lines of code you'd write in python, php, ts. It's hardly a fair trade off
But there is also a peak performance achieved by using tools for decades that is not transferable. C experts didn’t switch to Java, they found different jobs.
For your core technology you want a few of those 5-10+ year guys.
Combined with clojures way of handling immutable datastructures so well & the excellent protocol system (https://www.freshcodeit.com/blog/clojure-protocols-and-the-e...) I could separate the whole game easily into separate components.
Of course, the Clojure approach is great for a lot of use cases, but video games traditionally do a lot of mutable changes to components. That doesn't mean, of course, that it cannot be done any other way, but it would certainly be a different approach.
In my experience, it slightly favors wide rather than deep game state because wide game state implies fewer nodes that need to be re-created when creating a new state. Since the author is using ECS, it's probably wide enough already.
A bit more in the weeds and if one was using a deep game state, careful structuring of assoc-in and update-in calls might be warranted. Ideally each node would only be copied once to arrive at the next game state. Sometimes it can be more efficient to build a list of changes and then property sequence the application of the changes to the state.
At the end of the day, people don't have to use a single atom like the author either. A game state can consist of multiple mutable data structures. It's not like the Clojure-police are going to arrest anyone for it.
Also Clojure has better performance prospects than Ruby and Python, both have seen "real" usage in commercial indie games.
Personally, I think that FP could be a great fit, but we first need to come up with architectures that solve real game development problems. We have to do this with small scale experiments first (game jams are perfect for this) and then scale up only if they succeed.
This project is exactly that - kudos to the author.
They aren't using them because they aren't a good choice for delivering games that need to run fast and consistently in real time.
Immutable data structures and garbage collection might be nice for people writing something, but that isn't what someone buying software wants, they want smooth and fast.
Also most of the sprite rendering is the main problem, which an atlas texture could also improve even more. (Right now all creature animations are separate texture files).
And you can always step down a level to java if the need arises!
C# tends to be a bit more forgiving about when it triggers GC and how. The generational garbage collector in C# will tend to be more reliable or at least I never ran into super huge issues with the places I've used C# for game dev.
The JVM GC has this unfortunate effect of having very bad pauses occasionally. And appears to do so regardless of the type of GC you are using.
There are some techniques you can use to get around this -> re-using entities. Using C# structs. Not doing allocations/deallocations inside the main game loop if you can.
For small enough games it is irrelevant but as soon as you start to get a larger game with lots of memory allocations/deallocations it really crushes performance.
Notch surely isn't sorry for having coded it in Java.
Too much is wasted on what language, or engine to use, instead of what actually make as a game.
And even when everything is done right, it is a drop in the ocean of daily released titles.