On Evaluating Godot

Casey Yano
13 min readOct 17, 2023

I just wrapped up a 3-week internal game jam we did at Mega Crit to evaluate the Godot engine.

Why change engines?

For those from the future, on September 12, 2023 Unity announced a runtime fee and partially walked back on it 10 days later. Chaos ensued during the interim as developers didn’t know if this applied to already released games, the definition of a runtime fee was vague, there were concerns over how this was detected, and they got rid of their github Terms of Service (TOS) page.

Unity later said they removed this method of keeping their TOS transparent because it didn’t see enough traffic to this github page. ???

We and a LOT of other developers made a public statement in hopes that Unity would do better

Needless to say, Unity dug deeper and deeper with strange follow-up statements and minor clarifications, while their employees, just as confused as everyone else, tried their best to keep everyone calm. It was a disaster.

You can read more about its initial problems here and the follow up here.

It was a stupid thing Unity was doing, I don’t even like their engine that much, and I’m an indie dev who wasn’t feeling very independent when told that this “for the community” engine was tacking on fees and never fixing their buggier components after they IPO’d.

So the Mega Crit team and myself evaluated Godot for about a week, then decided it would be best to make a two-week game to examine any pitfalls. Pitfalls like, “Godot simply cannot do that” or architectural pitfalls; maybe the creator is overzealous about ECS, who knows?

But why Godot?

There are a few requirements I have when choosing a game engine. The main ones that come to mind are:

  1. More than a Game Library: Having worked in SDL and LWJGL I’d like a bit more handholding. A few in-house APIs for loading/unloading resources, font stuff, and display handling please. I don’t want to write those; I want to make games!
  2. Statically-Typed Language: Certainly no visual scripting, drag and drop-only editors, and none of this dynamic scripting thing please. I know C++, C, #Objective-C, Java, and while I know Python and javascript it’s just icky to me. Not the code I write in these languages, but there’s no world where I can enforce statically typing everything within a company. I can’t even enforce where my bunny poops.
  3. Community: Godot feels like a popular alternative in terms of enthusiasm. More popular means more common issues are solved/discussed. I’m not a smart guy. If I can’t solve something fast enough I just hack something in and move on. Still, I would prefer finding and implementing a solution that feels high quality for the project. We run lean so I don’t want to bother reinventing wheels. You think I’m going to implement volumetric clouds on my own? No thanks.
  4. Porting: I want to do my own ports! The reason we chose LibGDX for Slay the Spire was because it could do PC, Mac, and Linux. Yes, it runs in a JavaVM and it has all sorts of problems but it’s write once, run anywhere amirite? No. It don’t run on consoles and Mac and Windows updates constantly break it. Anyways, porting is hard and Godot, well W4Games looks to be working on porting tools and there are also other companies which are doing ports for consoles as well! Very cool. Brotato and Cassette Beasts on Switch 👀!?

There are other reasons to choose a game engine as well. Things like general engine speed, how regularly it’s updated, how pleasant the interface is, and whether or not the owners/founders are displaying signs of John Riccitiello-itis.

So Godot met these requirements and it supposedly supports C#. Cool. Let’s make a game.

What is Dancing Duelists?

Well, it’s the jam game we talked about earlier! We ended up spending an extra week on the jam as we had one of our programmers start working on the “big port” and we didn’t want too many cooks on the core framework.

Spoiler Alert: We finished the game!

So we worked on a deckbuilding autobattler because it’s familiar and it has a lot of bits and bobs which tend to be the types of games that we like making (content-heavy, deterministic).

Game Content Pipeline

I think unlike most companies, we’re a design-first company AND both of our game designers (myself and Anthony) are technically proficient so we’re able to circumvent the making of several tools.

Even when working within Unity, we went against the advice of creating Prefabs as content or using Scriptable Objects as a means of storing content data because we just don’t want to be clicking about and using an IDE is considerably faster for us. Jumping around definitions/parameters, using powerful search functions, and bugs are easier to troubleshoot when the engine just points to a line of code.

This content-as-code architecture is exactly how we do it for Slay the Spire and so we jammed out a little card game and there we go, it worked flawlessly in Godot as well.

If you are a non-technical designer and the word programming scares you, take a look at the following. It’s how we implement a card called Backflip. I personally find this to be more pure than using an inspector or special data format but your mileage may vary.

public sealed class Backflip : CardModel
{
public override string Title => "Backflip";
public override string Description => "Deal 2 damage.\nGain 2 HP.";
protected override string PortraitPath => "backflip.png";

public override async Task OnPlay(FighterClashState owner, FighterClashState target)
{
await FighterCmd.Damage(target, 2, owner, this);
await FighterCmd.GainHp(owner, 2, owner);
}
}

Before we implemented any of this game content we needed to setup a framework for screen-to-screen logistics, ui, animations, vfx, audio, fonts, etc. This took up most of the time in the jam because like in most engines there’s an optimal way to setup managers, factories, and race condition proofing your game while making it as pleasant as possible to incorporate and troubleshoot game content.

Screens and Objects

I hate loading and unloading screens because it’s unnecessarily slow, things are “cleaned up” in esoteric ways, and we need to invent exciting ways to pass all sorts of different data from one place to another. When the thing you need to unload and then load are tiny (like in a 2D game), you can unload a handful of assets and then load in the new ones. You can even asynchronously load it ahead of time if you’re confident. Both Unity and Godot are flexible here, allowing you to visualize objects with parent/child relationships and avoid using scene reloading. Honestly, one of the nice things about modern game engines. Sometimes you leave some objects behind and don’t realize it without a visualization of a scene’s tree. Speaking of a scene’s tree there are scenes and there are prefabs in Unity.

In Godot, they are just combined into one thing called scenes and are in the tscn format and it is git compatible. Holy shit. Much better than the Unity YAML stuff which made us contemplate if perforce was actually useful (it is not, fight me). Gone are the days of hellish prefab merges. Still, if there are a lot of changes in a scene you can get into a bit of trouble. Just a bit though.

Objects within these scenes are called Nodes. So GameObject -> Nodes. You can’t attach a lot of components but there are many extensions of Nodes which handle most of your needs. If not, then you’ll have to write your own on top of the base Node class. This is fine by me because I’m nitpicky as hell anyways. The Frankenstein component-riddled GameObjects in Unity were always upsetting to me. Having to scroll through the inspector all day is painful. This might be controversial, but I want to use my whole monitor to make video games and I prefer it to be snappy, beautiful, and rather not alt-tab so much. Still, I need to fiddle with various knobs on Nodes sometimes as well but I find that each Nodes’ limited parameters kept us in check.

UI Layouts

Speaking of Nodes. Parenting nodes and anchoring them to various points are a contemporary way to layout game UI and improve compatibility for various aspect ratios and screen sizes. If you wonder why developers who roll their own engines hate UI work, it’s because they don’t want to implement this kind of system. It’s annoying and it doesn’t feel like you accomplish anything when the game supports yet another obscure aspect ratio.

Both Unity and Godot has pivots, anchors, and various containers to help you organize things into lists or grids. Sometimes it can be quite confusing, I think that when certain variables become locked, it should be very very clear, but it never is. The worst possible case brings up a popup in the vein of, “You can’t do that because your parents are mad at you.”

So there’s a learning curve here and Unity decided to redo the whole thing via UI Toolkit. Basically, making UI in video games remain a disaster from now unto forever because there’s only like 6 of us who enjoy making UI and whoever is in charge at Unity is probably frontend webdev and now all indie devs have to be forced to learn their ways. My condolences.

Anyways, in both Godot and Unity, all the special Nodes for UI are bad. That’s right. They have always been bad and they will always remain bad. Games have lots of complex UI edge cases and borrowing generic templates will make your game shit. It’s short for user interface, it’s how your users (players) interact with the interface (game).

All new developers will use the default. When a button changes its color imperceptibly. “Ah yes, that’s a Unity game with a lazy UI dev.” When you hover-over letters but your mouse cursor has to literally be touching the vector shape of the text? Ah, that’s a Flash game. How are you even playing a Flash game in 2023?

Don’t even get me started on Unity’s ugly “Font” component default buttons and UI controls.

Do I look familiar?

TextMeshPro is phenomenal though. I miss it. Please, just let me vertically center my my auto-sizing min/max set font text into a clean rectangle. We had to write our own stuff to make text nice. I care a lot about fonts and text.

We have a bot in our server to warn me if I talk about fonts too much

Still, I’m happy to see MSDFs in Godot. It’s cool technology! The corners are so sharp and they scale so well. Except when they don’t work whatsoever😢

Yes, I’ll report the issue with an example project. Give me some time; I just finished a jam

Does it actually support C#?

Maybe you can’t access certain APIs in C#. Perhaps it’s a pain to find objects in your scene. Maybe it’s always updating or unstable when compiling. Maybe the workflow is bad, forcing us to compile scripts for 10 seconds each time we alt-tab from IDE to the Godot editor. These were some of my irrational fears when the GDScript evangelists started appearing during the C# talk.

I mentioned it above and I’ll mention it again. I like statically typed languages. I don’t want any of this loosey goosey dynamically typed var stuff and nobody can change this part of me. I like things verbose and obvious. Sort of related, we use JSON over CSV. Same principle.

Moving on, I was pleasantly surprised by the C# integration. The setup to use my IDE of choice (JetBrains Rider btw) was easy. Autocomplete works great and the game could be run from the IDE by default. Also, switching to Godot and running the game was fast. I mean really really fast. The 5–10 second “Compiling Scripts…” popup in Unity always haunted my dreams.

Is it fast? Yes

So the compiling scripts thing is gone and Godot is considerably lighter weight than Unity. If you’re looking to mess with high-end graphics or fancy lighting tech I don’t really have a response for you. But you can mess with shaders, a particle system, materials, and all that jazz. Maybe not the most cutting edge stuff but I wouldn’t know — my focus isn’t on the shiniest toys; I’m an indie dev after all.

Godot is small and it feels nice to just run the game, change some parameters, and rerun the game rather than making/using debug tools all the time.

You can also make changes to Nodes during runtime which are reflected in the running game! This was a “oh this feels very modern.” moment when I was learning Unity so I’m happy to see this working well in Godot. It has a few limitations compared to Unity but overall really great stuff.

Back on the topic of speed, opening a project is faster and doesn’t require some bullshit authentication. Running a project is faster. The scripting workflow is faster and exporting builds are faster. It’s fast!

Sidenote: There were some complaints about raycast2d performance on the Internet. If you’re planning to use a lot of those then I suggest reading up a bit on it.

TexturePacking/TextureAtlas/SpriteAtlas

Kind of sucks, that’s all there is to it. At least it can read files created using TexturePacker (the name of a third party texture packing tool). It’s just a tad annoying. Then again, Unity would pack them every time I ran the game. That was stupid too. Maybe this workflow has never been great anywhere?

NinePatch textures are annoying to setup with TextureAtlases. It doesn’t seem like that complex of a problem so perhaps I’ll submit a feature request.

Dancing Duelists design misses

Okay, back to Dancing Duelists. I figured I would go over some limitations which were introduced to reduce the scope of the project and areas of the game’s design we didn’t fully explore due to time constraints.

  1. Decks are too random: The initial design allowed the player to setup their decks to play in the order they wished. The decks would play automatically from top to bottom. This requires serious UX work to make it intuitive and introduces a lot of complexity we need to balance. We didn’t expect to get the game playtest ready quickly so we couldn’t rely on running several playtest sessions to iterate through problematic content. This randomness keeps it simpler for a jam.
  2. The turn order problem: Who goes first? How do ties work? If it’s multiplayer who would go first? A speed stat? A coin toss? You go first and then they go first? It’s honestly a complex issue to solve for autobattlers to solve fairness so we just opted for a PvE game where the player always goes first. Just like above, this simplifies things but the game is shallower due to it.
  3. Samey Builds: In an ideal world we’ll make bespoke card pools for each fighter so they can build towards 2–4 common archetypes and we can mess with multi-pool, shared pool, and maybe something similar for the trinkets-system as well. Making this much content, balancing, and exploring these archetypes would be a massive undertaking. Sorry, but the card pool is littered with Spin cards for this jam.

There are more limitations but I think that we were able to release a game in such a short amount of time due to these cuts. While it’s sad to not have a game realize its full potential, it’s good to be pragmatic and not become overly attached to a jam we did to explore a game engine.

Multiplayer?! Alas

We went a bit back and forth on whether this jam game should be multiplayer as autobattlers tend to be. However, we never solved a few design problems so we didn’t get around to it in the end. This meant that we needed to play the game, save the decks, and then fight against these decks.

Getting this data from playtesting didn’t go as smoothly as expected so I spun up some automation. You’re probably asking “machine language?”, “AI?”, “neural something something?” and the answer is no not really. It’s brute force. Which was fine by me because the game is asymmetrical due to the “The turn order problem” and the decks being randomly shuffled at the start of each combat introduced a lot of variance.

We talked about Godot being fast to work in, but it also runs lean.

Godot’s pretty performant, I ran ~15 instances at a time with full audio/animations

So we brute force card selection and make them fight our small player-made decks and when they win, we save that deck and incorporated them back into the game. We wanted roughly 20 unique decks per round per fighter and ended up with ~1,200 unique opponents which meet a minimum strength requirement of defeating a player-made deck. It sounds like a lot of information but it’s 2mb. These decks are saved as JSON files. Here’s an example file: jazzy_jasper_R2_1697423308.json

{
“name”: “player”,
“round”: 2,
“strength”: 3,
“autogenerated”: true,
“character”: “CHARACTER_MODEL.JAZZY_PACIFIST”,
“trinkets”: [
“TRINKET_MODEL.CIRCULAR_BREATHING”,
“TRINKET_MODEL.GOTHIC_WARDROBE”
],
“cards”: [
“CARD_MODEL.GROOVE”,
“CARD_MODEL.GROOVE”,
“CARD_MODEL.GROOVE”,
“CARD_MODEL.SMOOTH_SOLO”,
“CARD_MODEL.POLYRHYTHM”,
“CARD_MODEL.HEADSHOT”,
“CARD_MODEL.LEG_DAY”,
“CARD_MODEL.ASTEROID”,
“CARD_MODEL.FIRE_BLAST”,
“CARD_MODEL.MOONWALK”,
“CARD_MODEL.ALACRITY”
]
}

In the end we didn’t utilize the strength variable, which was meant to introduce ramping difficulty as you won more with your current fighter.

Wrapping Up

We did it! In just 3 weeks we got a real video game you can download and play right here right now: https://megacrit.itch.io/dancing-duelists

Soon (tm)

There are more topics to cover like input, SDK integration, aspect ratio work, localization, hardware compatibility, third party plugins (we use Spine2D and FMOD), and moddability but making good video games are a marathon, not a sprint. Alright, I’m going to call it for this write up. I’m by no means a Godot expert but the tools to make a real 2D video game seem to be there.

--

--