Context

I’ve wanted to develop video games ever since I was a kid. You might say my first foray into gamedev/modding was probably editing game graphics in ms paint. I think the first game where I discovered you could do that was Cow Tipper (I think that was the name, unfortunately it seems to be one of those games lost to time, I simply can’t find it at all). Later I moved on to creating custom levels for Half-Life and Counter Strike using valve’s Hammer editor. I’ve met one of my best friends on a dedicated half-life mapping forum. I think it was called The Whole Half Life. I also got into 3D modelling by trying to find out how I could modify the 3d models of the characters in the game. Apparently you needed a thing called MilkShape 3D, unfortunately you could only do texture swaps without a paid license, you needed to pay money to actually edit the model’s vertices. Something that was at the time impossible for a broke kid in eastern europe.

Programming was still a rather fuzzy subject for me back then. I knew some pascal from school but that was it. The aforementioned friend introduced me to C++ programming, that opened a whole new world of possibilities. The first graphics library I dabbled with was called Allegro, discovered via the youtube tutorials of a woman kind enough to share her knowledge with the world (I can’t believe I was able to still find this on youtube). It might seem quaint now but getting bitmaps to move on the screen with arrow keys as a 14 year old was a peak experience. I’ve been chasing that high ever since.

About that time I also discovered GameMaker, I’ve built many fun prototypes during summer break with that one, it’s amazing what one can do with a shit-ton of time and no internet connection. But GameMaker had some issues, you need to pay real money to rotate a damn sprite, and it could not really do 3D graphics at all back then.

After a while, not getting very far with allegro or game maker I moved on to SDL, which felt a lot more powerful. I programmed my first raycaster shooter using SDL, it was the first time I saw the utility of trigonometric functions via a real life application.

I also looked into OpenGL, which was all the rage back then for 3D graphics, but for a kid barely entering highschool all those matrices seemed really scary, so I moved on from that.

About that time I learned about the concept of a game engine. Half Life had a cool game engine, based off Quake, but back then you’d have to pay good money to get your hands on the code for that. I don’t remember from what indie game I found out about the existence of Torque3D, but back then that also cost money.

Eventually I stumbled upon the Unity game engine. Unity was great, it has a user friendly interface, it was free and the internet was full of tutorials about how to do stuff in Unity. Unity is also how I took my C# skills to the next level. I knew some C# from doing WinForms projects for school, but Unity was what motivated me to learn more C#.

I have developed many prototypes in Unity, I have this terrible habit of starting side projects and never finishing any because there’s always a more shiny idea over the horizon, so nothing much ever came out of that.

Unity is what eventually lead me to get a Web Dev job in .NET and through a series of events I landed where I am now: Working as a game developer writing C++ for an Unreal Engine game. Although I’d love to advertise it I’d rather not doxx myself.

Being a professional game developer in Unreal is probably how I got to learn about the existence of Godot, it’s always good to keep up to date with the industry. Godot got a massive boost when Unity made that terrible move regarding the per install fee. Of course the announcement has by now been replaced with a retraction but the internet has a habit of not forgetting.

When that announcement was made I also had a look at Godot as an alternative for pet projects, as Unreal was way too heavy and Unity was looking less and less attractive.

Godot seemed like a reasonable option given that it supported scripting via C#. Unfortunately, back then the C# documentation was rather sparse compared to the GDScript documentation, and for collections you have to use specific Godot containers instead of C#’s native containers (i’m using the term native here meaning the containers that come out of the box with the C# runtime, I am not implying C#’s containers are native, because they are not they are managed). And the problem with GDScript was that it was yet another language I needed to learn to do anything useful in Godot (as pythonic as it may be at first glance, it is not exactly python, and Godot’s script editor is… not that great to be honest). That short experiment left a bit of a sour taste in my mouth so I just moved on.

But lately I’ve felt the creative juices flowing so I thought, what the hell, let’s give it another go. This time I had AI available for support, and surprisingly, AI is not terrible at writing GDScript (at least to the untrained eye). And while setting up some basic test scenes for the thing I was prototyping I had a sort of epiphany…

Godot Enlightenment

If you consume programming related content, you might have heard of the concept of Lisp “Enlightenment”, the sort of epiphany one gets after hacking away at problems using LISP for a long enough amount of time. Eric Steven Raymond mentioned it in 2001 in his FAQ document “How To Become a Hacker” where he states:

“LISP is worth learning for a different reason — the profound enlightenment experience you will have when you finally get it. That experience will make you a better programmer for the rest of your days, even if you never actually use LISP itself a lot. (You can get some beginning experience with LISP fairly easily by writing and modifying editing modes for the Emacs text editor, or Script-Fu plugins for the GIMP.)”

Sadly I have only dabbled with LISP and LISP like languages such as clojure and racket, so I have yet to reach programming Nirvana, but I caught a few glimpses of it, like seeing a pattern formed by things that are missing, as opposed to seeing a pattern in things that are there.

The gist of LISP Enlightenment is its homoiconicity. Code is data and data is code. And I think the same sort of enlightenment can be had with Godot.

The thing Godot does so well that other engines don’t do as well, is that everything in the game is a tree of Nodes. Nodes can be of many types, but at the end of the day, everything is a hierarchy of Nodes. You can attach code to those nodes and that’s it. If you want to save a collection of nodes as a thing you can instantiate multiple times you save it as a scene. Everything that lives in the 3D world (or 2D) is a scene. The “level” is a scene. What Unity calls a prefab and Unreal calls a Blueprint, in Godot is a scene. It’s perfectly composable. Yes that’s not all there is to it, Godot has the concept of resources and reference counted objects that don’t live in a scene. But I think to best illustrate why I find Godot’s approach so profound is by comparing it to what Unreal and Unity do.

The main problem I want to focus on, as it is a problem that I am working at as part of my job, is procedural dungeon generation based on hand-crafted rooms. Think dungeon generation based on pre-made, artist edited rooms.

Unity’s approach

Unity is not so egregious in its approach. It has scenes and it has prefabs. In earlier versions prefabs were a bit of a PITA to edit but in later versions prefabs can be opened up in a separate editor tab and worked with as if they were in their own individual scene. The problem with unity was that there could only be 1 active scene at a time and that was it. You could switch between scenes but you couldn’t instantiate a scene. Meaning that if you wanted to do any sort of procedural content generation based on hand-crafted content you have to stuff everything in prefabs and work with prefabs and forget about scenes. Obviously you’d still want a main scene and a menu scene etc, but “level” pieces would have needed stored as prefabs.

I initially wrote the preceding paragraph in the present tense based on my outdated knowledge about Unity. It seems unity has added the ability to additively load scenes 10 years ago (how time flies). However as far as I can tell in the editor you can still only edit 1 scene at a time, so this is only a runtime feature, meaning you can put stuff in scenes for dynamic loading at runtime but you can’t easily reuse them during editing (please correct me if I’m wrong)

With the later versions of the engine providing a much better user experience for prefab editing, it’s not too bad at the end of the day. Unity’s taxonomy is pretty simple at the end of the day: you have scenes, scenes contain game objects and game objects can have components attached to them, as well as other game objects, and some of these components can be scripts. That’s mostly it, not too shabby.

Unreal Engine’s approach

Unreal is where things get… complicated. I get it though, at the end of the day Unreal Engine 5 is an iterative product built on top of years and years of bolted on features starting way back in 1998.

I will preface this by saying that Unreal Engine is pretty great, it offers multi platform support, its multiplayer server-client architecture is really good and performant. It has a ton of great features and code that just works out of the box, it’s a very batteries included kind of engine. If you’re making triple A first person shooters that is… but at least it’s free and open-source so you don’t have to pay thousands of dollars for access to the source code if you ever need to fix an engine bug (and believe me we had to do a few edits here and there)

I probably have enough complaints and materials to write tens of posts about Unreal’s pain points (and maybe I will), but for now I’ll just focus on the problem at hand: composability and procedural generation. Now, we are aware that PCG has been a thing for some versions now, but the kind of procedural content generation we need is not exactly covered by PCG and the feature was developed before PCG was even announced.

Taxonomy

Regarding the problem at hand, and composability, the first pain point of Unreal is that it loves and encourages inheritance hierarchies. Of course, back in 1998 C++’s shiny OOP features were all the rage. Unfortunately, this inheritance scheme has been inherited (see what I did there) all the way until the present.

Unreal presents a very strict taxonomy. You have FStructs, UObjects which is the base of all other objects in unreal, you have a UWorld which is both the engine’s logical world but a map in unreal is also a UWorld that is serialized as part of a UPackage. Then there are Actors, AActors and any other class inheriting from AActors. Actors are the backbone of Unreal, if it exists in the World it must be an Actor. Actors are also the means by which network stuff replicates (via ActorChannels but that’s an implementation detail). Unreal has a Garbage Collection system. All UObjects are garbage collected, but, not Actors, actors must be destroyed by calling the Destroy function and if you have any Actor pointer stored anywhere that points to that actor that the GC system is not aware of (because it’s not under a UPROPERTY or it can’t be) tough luck you now have a dangling pointer and a crash waiting to happen. That is not the case with UObjects, which are kept alive as long as there’s any UPROPERTY that points to it. And actors contain UActorComponents which are UObjects, but a UActorComponent does not have a position, if your component needs to have a position then it is a USceneComponent. The level also has a special actor called ALevelScriptActor which is an Actor but not really an Actor because it’s very special and doesn’t really exist in the world. The player controller is also an Actor and each player’s online state is also an Actor. Are you dizzy yet?

Structs are structs and are meant to be used as plain old data. While you can define methods on them they can never be exposed to the blueprint system (which by the way is a noob trap - it makes it way too easy to load the entire game in memory via a series of ill thought casts - we are spending a lot of effort to undo that whenever we have the time). Then you have UObjects. They are the bread and butter of unreal, anything that matters in unreal is inherited from UObjects. UObjects are managed, garbage collected and can expose methods to the editor and blueprints. The problem with UObjects is that, they can not be created on other threads, creating a UObject on a thread other than the main thread is a surefire way to trigger a crash in the garbage collector and good luck figuring out what caused the crash. So if you need to generate something like a graph on a background worker thread you can only use FStructs or plain C++ classes, but not UObject derived classes. And if you need any kind of polymorphism you’re gonna have to work with pointers, preferably smart pointers, but unreal’s smart pointers can not be declared as UPROPERTY so unreal’s reflection system can not know that you have a smart pointer holding a struct that might be holding a reference to an Actor or UObject.

So what’s the big problem with this? It sucks for composability! A level is a very special thing, that until some years ago could not even be instantiated as part of another level. And in a level you have actors, but attaching actors to other actors is very janky, each actor is meant to be its own thing so you can’t for example have a tank turret by an actor that you attach to a tank body. No, actors can only contain components, and components can not really contain other components. Unreal’s “prefabs” are called blueprints. A blueprint can be any UObject derived class, so it could just be some data, it could be a component or it can be an actor with many components. But you CAN NOT have a blueprint that contains a collection of components unless it is an ACTOR and an Actor can not have Actors as part of it.

Well it can, actually, but it’s via a very specific component called a ChildActor component, which can store a reference to the blueprint of the actor that should be spawned by the ChildActorComponent. The problem is the ChildActorComponent is a very janky piece of shit. It’s riddled with bugs since time immemorial and every good Unreal Engine developer will tell you to avoid it like the plague. It’s especially egregious in multiplayer games. So yeah, composability and reuse in Unreal Engine is an afterthought.

Level Instancing

At least we can instantiate level instances right? Yes, that fortunately works, more or less. Of course there are a slew of problems with this system as well. As far as I know there’s no out of the box system to get some metadata about the levels you want to instantiate such as for example, how large is this level?. So we had to create our own level metadata management system, whatever, that’s not necessarily a problem of Unreal. What is a problem of Unreal however is the piss poor API. When you instantiate an asynchronous loaded level (of course you want it to be asynchronous because it might take a few seconds, you can’t just freeze) it just returns a special UObject to which you can bind a delegate letting you know when streaming has completed. The problem? The fucking delegate is a void (void) delegate, meaning, if you’re streaming multiple levels at the same time, the delegate will not tell you which damned instance has finished loading, so then you have to iterate through the streaming state objects to figure out which level just finished streaming. Alternatively you need to wrap the returned ULevelStreamingDynamic object in your own UObject that also handles the specific delegate for that instance, but WHY, why do I have to do that when the API could have provided this information.

Back to Godot

In Godot, each hand-crafted room is just a scene. I give the root node an exported Resource with metadata about dimensions and possible connection points. At generation time, I instantiate scenes, position them, done. No ChildActorComponent jank, no streaming delegates. Godot even supports creating node trees on different threads!

So yeah, I hope you can see how by comparison Godot has taken a very wise approach to this problem. Yes, I know Godot has the benefit of many years of learned lessons that the industry has paid for with white hairs and lost follicles, but it does feel like quite an epiphany when things are treated in a very uniform and composable manner.