Dastardly Postmortem: Reflections on My First Digital Game Using the Godot EngineProject Update, Godot Engine, Postmortem ·
Well, it only took a couple years, but I finally finished a digital game!
I’m about to release Dastardly on Itch.io after about a year of part-time development. Dastardly is a fast-paced and chaotic local multiplayer arena combat game for 1 to 4 players. Travel across the Milky Way to face down other bandits. Jump, dash, and shoot your way to victory, making use of an array of powerups like shields, cloaks, and jetpacks. Inspired by classic couch multiplayer games like Super Smash Bros. and more recent hits like Towerfall: Ascension and Samurai Gunn 1 and 2, Dastardly has a focus on tight controls and fast matches. Load it up and shoot your friends from the comfort of your own home.
In this post I’ll reflect on the challenges and what I learned through the process. If you are thinking about developing an indie game, particularly using the Godot game engine, I hope this is useful to you! It’s certainly comprehensive. 😅
Inspiration and Initial Goals
I had a pretty clear goal with Dastardly: create a digital game with a scope small enough that I could create it relatively fast in my spare time and learn more about game development using the Godot engine. I had already tried to create a different game, The Sword Will Decide, in 2019 and ended up bashing my head against it and not making much progress. In this second try, I wanted to focus on my coding skills and understanding Godot, as that is where I fell flat the first time. I had a bit of coding background in R and Python, but nothing that serious.
Upon reflection my first try was not successful because:
- I had a scope that was far too large.
- I chose to develop a GUI-heavy game but didn’t put enough time into learning how Godot’s Control nodes function.
- I could write simple code but struggled without understanding architecture, inheritance, and design patterns.
- Trying to both design a game and learn Godot at the same time. I could have used paper prototyping to iterate on the design before coding. I hope to take that route in the future with this project.
So, for my next project I had a few goals:
- Improve my programming skills.
- Become more comfortable using Godot.
- Reduce the scope so I can finish a game.
- Not spend much/any money on the project, just time.
- Not worry too much about making money on the project.
Regarding the choices to reduce the scope and make it more manageable. Those included:
- Drawing heavily from an existing game design so I could focus more on the nuts-and-bolts development and less on design.
- Trying to use public domain/open assets as much as possible. I’m not much of an artist and would rather improve my programming skills than art skills.
- Thinking carefully about what features to build, including creating a prioritized list of features with a defined “minimal viable product.”
- Choosing a less GUI-heavy design that would let me use existing tutorials to learn rather than trying to learn just through the documentation.
I’ll reflect on each of these decisions in a bit more detail.
Using Existing Design Ideas
It’s common to hear advice to focus on a basic mechanic (or mechanics) when creating your first few games. A common strategy is to “clone” or copy an existing game to try to understand how it works. While the lack of creativity involved in that process didn’t thrill me, I knew to be successful I’d have to constrain my scope a bit. Reducing the design work was one way to do that. I’d feel like I was cheating if I fully cloned a game, but I was happy to let one be a central inspiration to constrain the design space.
I also knew that one of my goals was to improve my programming skills specifically. Those skills are more transferable to my current career and would build a solid foundation for the future. Reducing design work helped there as well.
With that in mind, I started to think about what genre or specific game to emulate. The obvious first choice was a 2D pixel-art Metroidvania platformer; that still seems to be a go-to approach for indie games nowadays. However, because the market is so inundated with those games, I was less interested in creating one. I did want to stick to 2D, though. It’s just a simpler system for a first game, and I’m scared of quaternions!
The next piece of inspiration I had was to think about simple games I enjoyed playing over the years. One that immediately stuck out was Towerfall: Ascension. Although I actually had not played that much of it, I was really attracted to the simplicity of the design and graphics, the great game feel, and the fun and frantic joy that comes from local multiplayer games.
Although I definitely could have picked a simpler genre or game to emulate (endless runners, flappy birds, or simple word puzzle games are other popular choices), I was inspired by this idea and decided to run with it. I knew that local multiplayer was a pretty categorically bad choice, since those games historically do not perform well. I forget where I saw that data, but it was probably produced by Chris Zukowski. It makes sense: they instantly create a barrier to entry by requiring the use of controllers and having multiple people available to play at once. However, I had such fond memories of playing local multiplayer games that I decided to push ahead anyway. I also figured it would be a good experience to learn more about input management and multiplayer without needing to deal with netcode. More about that decision below…
This restriction felt like it would potentially be the most impactful. Developers rightly take flack from players for “programmer art” and “asset flip” games. I knew I wanted to avoid either of those issues. After doing some digging around the web for art with open or permissive licenses, I found that OpenGameArt and Itch.io seemed to be the best places to look. That said, both of them definitely require sifting the wheat from the chaff. There are a lot of low-quality assets in both places, though, perhaps because it’s been around longer, I felt like Open Game Art was worse in this respect. There’s some great stuff out there, but you have to search for it.
Additionally, I knew from looking at other no-budget indie games that a common mistake was using mismatched art from various sources. So, it was important to me that I try to source my art from a small number of artists with a similar style.
After a few days of searching I found myself drawn to the (mostly) free assets produced by penusbmic on Itch. His art is really clean and fun. Because he had several tilesets available using a similar style, I knew I could create different biomes or environments that would still feel cohesive. I saw he had a few characters available as well. After seeing this art and thinking about the Towerfall game mechanics, an idea began to emerge in my head: a local multiplayer arena combat game with a focus on quick rounds and fast aiming. Basically a cowboy shootout-type situation. Because Penusbmic offered sci-fi-themed art as well, why not go for a space Western theme? I had recently watched Cowboy Bebop for the first time, and in hindsight that probably played a role as well!
For my day job I manage a backlog of work as part of a company-wide kanban system. I enjoy the structure kanban provides, so I thought that even though I’m just a team of one, I’d like to use a similar approach for developing my game. So one of the first things I did was create a small backlog of tasks on a Trello board.
Then I could commit to just tackling one piece of work at a time. If I spent too long on something I could break it down into smaller tasks or ditch it and look for a way around the problem.
At first I wanted to validate that my idea was even possible, so I used a 2D Platformer template and loaded in some of Penusbmic’s assets to create a basic platformer. That didn’t end up taking as long as I thought, so after validating I could probably make this work, I filled in the backlog with more details.
My initial feature selection was driven by trying to create a simplified version of many of the features present in Towerfall. That included:
- Finding open character art
- Finding open level tileset art
- Finding open chiptune music
- Finding open retro sound effects
- Creating a 2D platformer character who could:
- Be idle
- Grab a ledge
- Slide on a wall
- Draw a gun
- Aim with the gun
- Shoot the gun
- Hit other players
- Creating level assets
- Creating menus:
- Title screen
- Player select screen
- Level select screen
- Pause menu with options
- Post-match summary screen
I created a rather large backlog and tried to figure out what I’d consider the MVP. In the end. I still set the scope way too large for my first game. I ended up including a lot of the basic features I wanted. I could have made a much simpler first version, but I felt I would not be satisfied with it. So, instead, over the course of development I did move the goalposts on what I considered an MVP. However, I also tried to make the game playable relatively quickly, and I started playtesting with others as soon as a single local multiplayer match could actually happen. But, that was still month seven of development! I could have playtested even earlier just focusing on moving the player, but I’m glad I at least started relatively quickly.
My first attempt using Godot was a GUI-heavy game. I struggled to understand how to use Control nodes properly and my progress was slow. Now I can say I have a better understanding of Control nodes, but I did try to avoid implementing too much GUI for Dastardly. I’d say this is one of the initial goals I didn’t do a great job on. I still had to implement quite a lot of user interface to make the game work.
In the end I am not 100% satisfied with the solutions I came up with. I think the UI looks OK, but under the hood I am doing a lot of hacky things and often not using Control nodes at all. That decision was driven mostly by the challenges I faced with multiple user inputs, because Control nodes are designed to work by default with a single user input. In hindsight I could have spent more time extending the default Control nodes to work better with multiple inputs.
That said, choosing to create a platformer rather than a card game made my life easier because there are far more Godot platformer tutorials available than card game ones. This meant I could find answers to common questions (how to do ledge grabs, for example) much faster.
Here are some of the issues I ran into during development and how I addressed (or avoided) them.
Choosing to create a local multiplayer game meant I’d have to figure out how to setup Godot input for multiple controllers. The primary challenges there included:
- Setting up Input actions for multiple controllers
- “Registering” a controller and associating it with a player
- Creating GUI that accepted either one or multiple player inputs
For #1 I found it easiest to setup a default InputMap in the Project Settings, then duplicate it for any devices that joined the game. The default InputMap would have actions like “jump” and “shoot” while the duplicated ones would append a device suffix, “jump_1”, “shoot_1”, etc. Then my Player node would just have a
device variable and append that to all Input action names when doing Input checks, e.g.:
if Input.is_action_just_pressed("jump" + str(device)): # do stuff here
For #2 I created a PlayerSelect screen that had individual PlayerSelectBoxes. PlayerSelect would monitor for unregistered input, record the input event’s device number, and then create a new player associated with the next unused PlayerSelectBox and let them join the game. That PlayerSelectBox would then also only accept input actions from that device. It would also add their information to a global singleton
players dictionary storing player information.
For #3 I used the same approach of forcing GUI nodes to only accept input from certain devices. This method was made a bit more challenging when I decided to only let Player 1 control certain sections, such as level select. However, that was solved with a simple call to the global singleton to retrieve the device number of Player 1. This decision also meant I couldn’t really use the default UI input actions, because those were all associated with device 0.
Overall this setup was challenging, but I found as long as I was consistent about storing player and device data it was never too difficult.
Related to the issue above, I had challenges using the default Control nodes and input actions with multiple devices. I experimented with changing the default UI actions to use the P1 device, but in the end I usually controlled all of this manually. This was a pain, as it meant I couldn’t take advantage of focus and focus change options built into the Control nodes. I still think there was probably a higher-level way I could have setup my code to take advantage of that, but in the end I went the dumb route just to get it to work. That meant having to manually code some focus change actions, which was definitely inefficient. I’m happy with the results for the most part. Playtesting was important here, as I realized early on I made some pretty unintuitive choices, particularly with the player select screen.
Not Using A State Machine
I remember early in the development of Dastardly I was talking to a friend with extensive software development experience. I had read that finite state machines were really helpful for organizing platformer character code and making it more modular. He thought it sounded like a good idea to use one, but also reflected the advice below about making it work before worrying too much about making it clean. I made a conscious decision to not bother with a state machine and instead controlled my player code largely though booleans like,
is_aiming, etc. This approach worked fine at first, but, surprising no one, it didn’t scale well. 😂
Now my player code is a bit of a mess, with a huge number of booleans controlling player behavior. It can be quite tricky to debug. I learned my lesson: I’m definitely using a state machine next time. I might also explore using an AnimationTree node, as that sounds super useful. Right now I’m just using an AnimationPlayer.
A pattern I definitely noticed was running into challenges with 2D transforms, angles, and other math problems. These issues raised their head primarily with the player aiming and shooting code. In early versions of the game, I made the problem simpler by using a less-ideal aiming system where up input increased the angle of the aiming line and down input decreased it. That made the math easier, but players found it frustrating to aim.
Another choice I made to simplify things (less math-related) was to force the player to stop moving while aiming. That made the game a bit more tactical as you were very vulnerable while aiming, but it also slowed down the pace in a way no one enjoyed. So, in later versions I changed the input so you could aim while moving, either using one stick to both aim and move at once, or using an alternative twin-stick system where you could aim and move independently.
As much as I hate to admit it, a lot of the math issues I solved through brute force. I would change signs around or pause the game and check values to see where I was going wrong. Later in the development cycle I started adding GUI elements to the player to help debug math problems. This technique was really helpful because it meant I could observe the issue in real time. Here’s a simple example:
func _draw(): if is_sliding: color = Color(0,1,0) else: color = Color(1,0,0) draw_circle(Vector2(0,-16), 8, color) func _process(delta): update()
I also did something similar with a simple Label node over the player’s head that could report values of particular variable so I could observe how they were changing in real-time. This method is much better than just using
print() to the Output console and digging through it later.
Here are some things that I think went well during Dastardly’s development.
As I mentioned above, I’m glad I started playtesting relatively early. Playtesting could have began even earlier, but at least I did it! In past game design projects, the effort of getting playtesters together often meant I wouldn’t playtest that often. In the future I’d like to think more about how to address that problem, either through finding local game design groups to work with or releasing much earlier alpha builds for people to try out.
I conducted five playtests for Dastardly with friends who graciously offered their time to try out the game. These were immensely useful. I had tweaked enough things each time that I usually just let them try to play without any specific instructions other than to speak their mind as they played. I took extensive notes on player behaviors, things that confused them, and of course, the many humorous bugs that emerged.
An obvious challenge for developing a multiplayer game without AI is that you can’t really test everything yourself. I could test player actions in isolation, but the combination of actions was often where bugs emerged. At some points I was even trying to use two controllers at once to test combined actions!
Obviously in the future I could address this issue by:
- Not developing a multiplayer game
- Adding AI early in development, including designing the player code for easy adaptation
- Conducting more regular playtests
Some of the big changes I made during playtesting included:
- Revamping the aiming system from aiming up and down to aiming via analog stick direction
- Improving the player select and match setup UI to make it easier to use
- Adding aiming while moving
- Removing head stomps
- This was a tough one! I quite liked the idea of having a more melee attack that rewarded careful platforming and jumping, as well as taking advantage of the “screen wraparound”. That feature was a pretty key gameplay element in Towerfall I wanted to keep. However, players continually complained - rightly so - that stomping was far more powerful than shooting. Players were hard to shoot and easy to stomp on. Additionally, shooting was more “cowboy-like” and satisfying due to the difficulty. I resisted removing stomping for a long time. Eventually I saw it had to be removed as a default behavior, but one of the players had a good idea after I introduced powerups: make stomping a powerup. So, I created some iron Boots inspired by Metal Mario and gave stomping a cost: slower speed. I’m pretty happy with this compromise - though I’m just realizing now these should really be Spurs, not Boots! Maybe in a future update.
- Setting the default gravity
- My original implementation was quite fast, as I thought that would make the game more fast-paced and frenetic. But I ended up greatly reducing player speed and setting a floaty fall speed to make mid-air aiming and shooting easier. That led to more dramatic mid-air gun fights and trick shots that the players enjoyed.
- Setting the default match length and ammunition amount
- My original defaults were 10 kills to win and 5 shots (inspired by the “cowboy load”). But it turned out that 10 kills made matches far too long and 5 shots lowered the drama of having to reload and being vulnerable. So I changed the defaults to 5 kills (at least 2 rounds with 4 players) and 3 bullets (enough to kill one player per reload).
Becoming Comfortable With Godot
One of the key successes from Dastardly is that I am now far more comfortable using Godot. This includes learning the Editor interface, learning the differences between GDScript and Python, and, most importantly, understanding which nodes are useful in which contexts. Godot has such a great variety of built-in nodes, but at first I struggled to know which ones to use where, and of course what their basic methods could do.
After spending time with the engine, much of that is now second nature, especially for Control nodes and basic 2D game nodes. I still have a ton to learn, but I saw a very clear non-linear curve in my comfort and speed adding features over time as I both built my own infrastructure in the code and became more comfortable using Godot’s built-in features.
All of that to say - if you are just getting started with Godot I’d recommend spending lots of time reading the docs and looking at examples to better understand what the built-in nodes can do. Often the challenges you are facing will have a built-in solution somewhere; it’s just a matter of finding it. In my opinion, it might take a bit longer to read and understand the nodes compared to the instant results of starting to build your own solution, but in the long run it will save you a lot of time.
A List of Things To Learn
Throughout development I often would run into a challenge and have to ask myself, “Is it worth learning how to do this new feature/new design pattern/new Godot node?” For this project, I often answered, “No, let’s do it a hacky way and take a note to look into the ‘right’ way later.” This choice was driven by the advice I’d seen to make a game playable and fun before worrying about writing ‘clean’ code. This idea reflects a larger mantra in the world of software development:
Make it work, make it right, make it fast - Kent Beck
Here is my large list of things to look into in the future. I’m going to reference this for my next project and consider doing a bit of extra research before deciding to keep using my hacky inefficient approaches.
- Static typing with GDScript is an easy one. I avoided doing it until near the end of development, and, surprise, surprise, a lot of my bugs were typing related.
- Thinking more about using design patterns generally, e.g. from http://gameprogrammingpatterns.com/.
- Using finite state machines with enums, see e.g., https://gdscript.com/solutions/godot-state-machine/.
- Using principles to make every function only need to know about its inputs and better encapsulating functions. Right now my default is to write functions with no inputs that act on scene variables. Better off making most things local to keep functions separate.
- I still struggle with encapsulated design generally. I find it quite hard to wrap my head around passing data between nodes without duplication and data becoming stale.
- Setting all colors in a global color definition singleton, or ideally doing it all in Themes.
- Can add favorite colors now, but it’s only in the editor, not in code.
- Think more about using inheritance or composition, especially for GUI.
- I used almost no inheritance in this game.
- One interesting option is to use standalone scenes as composition nodes in a parent scene. There are challenges around decoupling them adequately so they work, but I think it’s a promising solution. It’s also more flexible than inheritance. But it depends on the problem you are trying to solve. This Reddit discussion is helpful.
- Using constants and avoiding string literals, using methods instead of literal comparisons, etc. See, e.g., this article.
- Better planning and executing GUI work to make it less painful and repetitive.
- For example, I ended up using custom
disappear()functions to animate Control nodes quite a lot. But, I never really made them generic and reusable, so there is a lot of duplication in that area. I also never really decided if code or AnimationPlayer was a better path forward for those. I was super inconsistent.
- Another big GUI issue is the “flashing” of GUI elements before they animate. The solution I found was to set the modulate to transparent when instancing, then use
yield(get_tree().create_timer(time), "timeout"), then turn it visible. The same with instancing a bunch of Control nodes in a grid or whatever; if you don’t have a pause their placement can get messed up. However, I feel like I’m still missing something here about instancing Control nodes while avoiding this issue. It was common enough I want to investigate further in the future.
- For example, I ended up using custom
- I somehow avoided using constructors at all. I think these would help a lot, as I was inconsistent in how I was generating nodes from data.
- I did use a Theme, but I found it a bit hard to work with. Thankfully it sounds like the Theme experience has improved for Godot 4.0.
- I definitely need better debugging methods, including things that only run when in debug mode.
- One thing I didn’t really do is create a system to quickly spawn things in-game that I need to test. For traditional character-controlled games, that’s easy with a test level. For card games or tactical games, it feels harder. I would need a special test scene that has parameters you can tweak in-game or in the editor to test what you want.
- I also think having a
Globals.debugboolean you can turn on and off to make testing easier is smart. I have a few separate ones now, but it would be better to do one big one.
- I’m using git, but I haven’t tried using the git integration in the Godot Editor itself.
- I’d like to work on prototyping core mechanics earlier.
- I considered looking into unit testing, but reading more, it doesn’t sound worth my time.
- This is a super basic one but: proper use of variable versus constant! I actually was not consistent about this.
When I was writing my dissertation I printed out a few inspirational quotations. One of them I found on Twitter was:
A good dissertation is a done dissertation. A great dissertation is a published dissertation. A perfect dissertation is neither.
I tried to bring that attitude to my game design practice, but without clear deadlines and impacts from delay (like running out of funding!), it’s a bit harder to put that idea into practice. However, I’m proud I finally have finished something enough to release it. Sure, there are things I’d still like to add (more levels, mainly), but for now I stand behind Dastardly and am ready to put it out into the world.
I’m going to have to think about what’s next. I loved working on a digital game, and with my new Godot skills I think my next one would be faster and better. But, I also love analog game design and there’s something to be said for the speed at which you can iterate on analog games! For now I’m taking a short break and plan to pick up a new project (or finish an old one) soon.