On Evaluating Godot
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. ???
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:
- 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!
- 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.
- 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.
- 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.
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.
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.
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ð¢
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.
- 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.
- 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.
- 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.
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
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.