I recently presented a postmortem for the game I worked on for Global Game Jam 2024, Wally’s Wacky Walk. You can watch it here (along with other presentations from the IGDA Twin Cities community), and you can download the game here!
Posts Tagged with godot
Postmortem: Black Hole Battle
This year, I participated in Global Game Jam. In preparation for it, I decided to make a game in 16 hours, or one week’s worth of my game development time. What I wanted to focus on, in particular, was scoping the game accurately; in other words, I wanted the game’s scope to only encompass what I thought would be feasible within a week.
16 hours is the amount of time I’ve estimated spending doing game development during a typical week.
How did I do? Read on to find out!
Planning Phase
Day 1 (Saturday)
I spent a couple of hours on Saturday roughing out the game idea. I wanted to make a top-down space shooter, and I wanted the player to fight against a black hole’s pull and escape its gravity well. This theme was meant to be both literal and figurative, with game elements hinting an allegory of a fight against depression. To save myself the trouble of coming up with a good name right away, I used the working title Black Hole Game.
There would be two kinds of movement: rotational, where the player’s ship rotates and moves forward; and slide, which emulates classic top-down space shooter movement, such as Space Invaders or Galaga. There would also be shooting combat, with multiple guns for the player to collect, obstacles to shoot down, and enemies to dogfight and defeat.
A direct influence was a game called Laser Age, but I doubt that one is familiar to most people; I played it a lot when I was a kid.
This was a solo project, so I decided that there would be no custom art made for the game; everything had to be found in pre-existing art packs. I spent some time searching, and eventually purchased a few packs from an artist in a style that I liked (the Void packs from FoozleCC on Itch.io). For sound effects, I wouldn’t do any recording myself; either I’d find the effects online or I’d generate them with BFXR.
I chose to compose and mix the music. I had an idea of mashing the themes from two songs together: Rush’s Cygnus X-1 (Book 1: The Voyage) and Orden Ogan’s Black Hole. Both of these songs are themed around black holes, representing the dual literal/figurative theme I sought to represent. Although it would have been simpler to relegate this to external sourcing (as I did the art), I enjoy making music, so wanted to keep this part for myself.
I’d use Godot 4 to make the game. My long-term project, Dice Tower, is being built with Godot 3.5, so I wanted to get more experience using the latest version of Godot, particularly since that’s what I’d be using for Global Game Jam.
Finally, I set a deadline for the entire endeavour: Saturday, January 20th, 2023 at 5:00pm CST. In the spirit of a game jam, I wanted a clear, strict end time to force me to complete the project.
With my ideas set, I spent Sunday and Monday engaged in other activities. Come Tuesday, I was raring to go.
Day 2 (Tuesday)
My work period was in the evening. My only task this day was to create a game design document and a timeline for when I was going to work on certain tasks. I gave myself a one-hour time limit for doing all of it; the idea was that limiting how much time I had to plan would keep me from adding scope creep.
I used a modified version of the Pomodoro technique for consuming that hour of time. I’d do fifteen minutes of work, then take five minutes of break time. For the first block of work time, I spilled out as many ideas about Black Hole Game as I could. After the five-minute break, I spent the next fifteen minutes going through six randomly-drawn cards from my Deck of Lenses, writing down answers to how Black Hole Game might be seen through each lens. The final fifteen minute block was spent furiously typing out the actual game design document, or what was actually going to make it into the game. That brought me to 55 minutes of work; the final five minutes were spent taking my game design document and scheduling out which tasks would be worked on when.
I think timeboxing the game design period proved useful. Only a small amount of the many ideas I’d come up with made it into the design doc, and that was solely because I didn’t have enough time to write them all down. Since there were fewer things, it limited the scope of what Black Hole Game was going to be. All that remained was to see whether or not that small amount of scope was achievable.
Some of the ideas that were left on the planning room floor included the guns and shooting-oriented mechanics; the gameplay would solely focus on movement.
Development Phase
Day 3 (Wednesday)
My time period was both morning and evening. Accordingly, I focused on roughing out the core of the game: movement mechanics and endgame triggers. My goal for the end of the day was to have a playable minimum viable product.
After creating the Godot project, I started implementing the player spaceship. I added the rotation-based movement first, spending a bit of time trudging through trigonometry (and the CharacterBody documentation) to figure out a simple implementation. Once this was mostly functioning as desired, I added the alternative sliding movement style, as well as the ability for the game to switch between the two movement styles seamlessly. Finally, I added constant downward velocity, simulating the pull of a black hole.
I added a debug key to let me freely toggle back and forth between the two styles of movement, even though my ultimate goal was to trigger this change through a pickup. I didn’t remember to take it out of the final build, so any player that figures out what the bind is can cheat the game. ;P
Once the player movement was working, I fleshed out my test game world into a fuller experience. I created some simple asteroid platforms for the player to rest their ship upon. I also made a couple of debug endgame zones: a red one for the black hole bottom, signifying defeat; and a green one to indicate where victory would be given to the player. In both cases, I showed a rudimentary test popup to communicate this endgame state. At this stage, the loss and victory zones were not too far away, for ease of testing.
With a game world in place, I worked on creating the Fuel mechanic. This limited how long the player was able to use their forward thrust, and with it the ability to drive against the black hole’s pull, and would create the game loop of moving from resting place to resting place without running out of fuel along the way. The implementation itself was simple: I made a custom resource that tracked how much fuel was in a given fuel tank, how long it took for the fuel to recharge, and signals to indicate when fuel was depleted or replenished. I then gated the player’s forward movement behind whether they had fuel in the fuel tank; if the player ran out of fuel, they couldn’t thrust forward until the tank recharged to full.
The last thing I tried was an experiment to render a black hole through Godot’s shader system. I tried a few shaders I found online, but none of them worked with initial implementation. Eventually, I decided that this wasn’t worth continued pursuit, and I axed it from my todo list.
By the end of the day, I had my MVP working: the player ship moved as designed, they had a fuel resource to manage, and places where they would trigger defeat or victory upon touching. I was feeling good about my chances meeting the planned scope.
Day 4 (Thursday)
Today, I only had a couple of hours in the morning to do game development work. Knowing this ahead of time, the only work I scheduled for that day was implementing a UI display for the fuel gauge and a Pickup system with three implementations: Fuel Tanks (instant refuel), Tank Expansions (refuel and expand the player’s tank capacity) and Stabilize (temporarily activate the Slide movement style). The day went as planned, and I implemented all of those thing by the end of my game development time. Once again, my confidence in the game scope increased.
Day 5 (Friday)
Once again, I only had a morning’s couple hours to work. Originally, my plan was to implement sound effects, but I changed my schedule to work on music, instead; I figured the timeboxing would be more useful for roughing out a composition than figuring out sound effects and how to implement them.
This time, I encountered difficulty. I had specific ideas for how I wanted to make the music, and I spent an hour messing around with various virtual instruments to try and get better sounds. Ultimately, most of that time was wasted, as I reverted to using the sounds I’d had in the first place.
Because of that wasted time, I rushed my way through a composition, and at the end of my gamedev work period I still didn’t have a fully composed piece of music, let alone the victory and defeat variants and the actual in-game implementation.
At this point, I became worried about whether I could still meet my planned scope. Were it not for my strict limit on when I would be allowed to work, I probably would have forced myself to finish the music on Friday night, against my work-life balance needs; instead, I forced myself to stick to the plan, and resolved to finish things as soon as I could on Saturday.
The Final Push
Day 6 (Saturday)
This was the final day for developing Black Hole Battle (the final title of Black Hole Game). I had ten hours, from 7am to 5pm, to finish development. By my self-imposed standards, this included publishing the game and making it available for people to play.
First, I had to catch myself up from where I’d gone off-plan. I spent about an hour finishing the music composition; ultimately, I was very pleased with it, and it decently accomplished my composition goal of merging Rush and Orden Ogan. I also threw together some short themes to play during defeat and victory.
With the music composed, I started work on implementing the music into the game. I thought I’d save some time by stealing some code from Dice Tower’s sound management system and converting it to work in Godot 4; the reality was that this system was built on top of a number of internal systems which I also had to port over to make the entire system work. Altogether, that was another two hours spent. Once the foundational systems were in place, it didn’t take too long to wire up the logic for when each piece of music should play.
Next, I jumped into creating and integrating sound effects. With time being compressed, I opted for using BFXR to create almost all of the needed sound effects (the lone exception being an ambient background noise, which I wound up stealing from Dice Tower). I worked my way down the list of planned effects, crossing out any which I felt I could do without. By early afternoon, all the necessary sound effects were created and implemented.
At this point, I needed to add the bare minimum requirements for UI, menus and game restart logic. I spent 20 minutes finding and adding two fonts: one for stylistic display, like headings, and one for button and paragraph text. Next, I created a Theme resource and added just enough customization to reduce the cost of duplication (like consistently styled buttons). With that theme, I created and styled my main menu, pause menu, and endgame menu popups. Once the menus were made, I added logic for when they would appear. Finally, I created proper game start and restart logic and integrated that with my menus. Once these things were finished, I had a fully functional and minimally polished game. To confirm I had a functional build, I did a test export of the game and proved that it still worked.
Fortunately, I only had one screen resolution to worry about; my experience supporting multiple resolutions in Dice Tower cautioned me against making the effort to do more than that.
By this time, I had about an hour left to add whatever content and polish I could muster. I threw together some simple game objects (based around the asteroids and planet from the purchased art packs) and tossed them into an expanded game world, along with generous placement of player pickups. I also adjusted the player movement mechanics slightly, to make them feel more responsive. Finally, I hid the debug graphics for the endgame zones, and, for the black hole bottom, I added a particle effect to indicate some kind of churn and swirl; hardly a realistic representation of what a black hole would actually look like, but it felt cool and only took a minute to spin up.
With minutes to spare, I created the final export and uploaded Black Hole Battle to a hosting service. The project was finished, and precisely at 5pm! I shared the project with a few friends, then went upstairs to have supper with my family.
Takeaways
The primary goal for Black Hole Battle was to practice scoping for a specific amount of time. Given this, I was successful: I accomplished all of the features I’d set out in the game design document.
Did I complete every single task? No, but that was never the goal; it’s impossible to complete a project exactly as drawn up, and there must be room alotted for adjustments. What I was expecting of myself was to implement all the planned game features and to release them in a polished state; in this effort, I succeeded.
Was the game itself perfect? No; the audio balance between SFX and Music was off, I didn’t really nail the planned thematic duality of black holes and crippling depression, and the small amount of gameplay means it doesn’t take long to fully explore what the game offers. My focus wasn’t on making Black Hole Game the best game it could be, but on making it good enough to be releasable. The game isn’t perfect, but it’s “good enough” to feel like a complete game.
At no point did I force myself to work longer than the hours I’d planned. I resisted the urge to crunch when I felt like I was falling behind, and I still found a way to deliver a completed project. This was a rare success, as previous projects have either ran horrifically over scope or had significant cuts to features and quality to release them on time. Hopefully, I can use this as a standard to plan other projects by.
Conclusion
I wanted to prepare for Global Game Jam by making a one-week, precisely-scoped project. With Black Hole Battle, I successfully achieved this goal, and it left me feeling confident going into Global Game Jam.
How did the jam itself turn out? You’ll find out soon, when the IGDA Twin Cities Global Game Jam postmortem meeting is uploaded to YouTube! That said, I consider the work I did on Black Hole Battle an important factor in how my time at Global Game Jam went.
Here is the final result for Black Hole Battle, for those who wish to try it out. I have no plans to make further changes for it, but feel free to leave feedback so I can apply it to future projects.
Stop Waiting for Godot (Presentation)
Tonight, I gave a presentation for my local IGDA chapter (Twin Cities) called Stop Waiting for Godot: Introducing You to the Godot Game Engine. This is a high-level overview of what Godot is and what you can do with it, intended to give you a taste of what working with Godot is like. You can watch the recording here, on IGDATC’s YouTube channel!
Anchors and Margins and Containers, Godot My!
Note that the title’s joke only works if you use the correct pronunciation of Godot. 😉
As much as I’ve loved using Godot over the years, one of my biggest pain points had been grappling with positioning UI nodes. I work as a web developer for my day job, so I work regularly with CSS. Compared to that, Godot’s UI didn’t feel intuitive, and anything more complex than full-screen positioning frequently resulted in nodes placing themselves in ways I didn’t expect, and it felt like I spent hours duct-taping an interface together. I scoured whatever documentation I could get my hands on, but nothing seemed to help much in the end.
Recently, however, I’ve had some tremendous breakthroughs in my understanding of how Godot’s UI positioning system works. The result is that I now understand that Godot’s UI is actually very simple to use, almost brilliantly so. Suddenly, I was able to put together complex UI scenes easily, and comprehend why my UI looked the way it did. Given all the frustration I’ve felt over the years, these realizations have felt almost miraculous.
I wanted to codify my newfound knowledge in this blog post. Not only will this be a reference I can look back on to refresh my memory, it will help people struggling with Godot’s UI to gain the perspective I have, and thus make UI work a breeze instead of a hurricane.
This is not a tutorial on how to make good UI, but an explanation of how Godot positions UI nodes. That said, I made a Godot project to help illustrate my points, and I’ve made it available for you to download and reference for your own edification.
The project was originally created in Godot 3.5, but I tested opening it in 4.0 and nothing seems to have changed for the worse. The UI around anchors/margins is different, as called out on Twitter by Jaysen Huculak, but the underlying principles are still the same.
Anchors
Let’s start with the first of the foundational elements of UI positioning: Anchors.
Anchors control the boundaries of where a node and its content are drawn on the screen. The unit of measurement is literally, that: a unit, from 0 to 1. What is the unit’s reference? The parent node’s size, which starts from the pivot (which is the 2D coordinate where the node is “located” in UI coordinate space) and extends across horizontal and vertical axes to the full size of the parent node. 0 means you are at the start of the axis (horizontal for left/right, vertical for top/bottom) and 1 means you are at the end of the axis.
That’s tricky to parse with words alone, so let’s look at some images to illustrate how this positioning system works.
The above is a simple Control node that is the child of another node that fills the entire viewport. Note that all the anchor values have been set to 0. Correspondingly, the node’s size is also zeroed out, so you can’t see anything of that node’s contents.
If you create a Control node with Godot’s editor interface, it will not look like this despite also having 0’s set for the anchors. I’ll explain why later in the article.
Let’s see what happens when we set the right and bottom values to 1 (or 100% of the parent’s size horizontally and vertically).
Suddenly, our node stretches fully across the available viewport space. By setting right
to 1, we told Godot to expand the right edge of the node all the way across the parent node’s bounding rectangle, across the horizontal axis; it’s the same story with setting bottom
to 1, but on the vertical axis of the bounding rectangle, instead.
In Godot 4, the anchors are abstracted behind a helper menu interface. To change the values manually, you have to set the anchors preset to “custom”, which exposes the raw numbers. Thanks to Rob for the comment pointing this out!
Just for fun, let’s change the left
and top
values to 0.5 and see what happens.
Now our Control node looks like it’s in the lower-right corner of the viewport. Essentially, we told Godot to move the left and top edges of our node 50% away from the parent’s pivot origin.
I’m not showing this in the screenshots, but I placed a
ColorRect
within theControl
to make it more obvious how much space it’s taking up. It’s essentially just filling whatever space ourControl
node is, and isn’t needed in any functional way.
Anchors are just one part of Godot’s UI placement equation. Another critical part of that equation is Margins.
Margins
Margin controls the amount of spacing used from the specified edge. Where Anchors are placed using percentile units, Margins use pixel units.
In Godot 4, margins are called “offsets”. Same concept, different name. Thanks again to Rob for pointing that out!
Let’s take another example Control node, with anchor values right
and bottom
set to 1.
Currently, the viewport is completely filled. Let’s try adding some margin around the Control so that there’s 16px of space around it.
…wait, that doesn’t look right. There’s 16px of space around the top and left of the Control, as we expected, but the bottom and right sides got pushed outside of the viewport’s visible area.
Why did that happen? It’s simple: Godot doesn’t treat margin as distance into the bounding rectangle. Instead, Margin gets applied along an axis of direction; positive margin is to the right/bottom of the specified edge, while negative margin is to the left/top of that edge.
This is different from how margins work in CSS, and is a big reason why I misunderstood Godot’s UI for so long.
To get the spacing effect we want, we need to apply negative margin values to the bottom and right margins.
That’s more like it.
Just for fun, though, what if we wanted to have our Control’s width exceed the bounding rectangle of the containing node? Simple, we just make the left
and top
margins negative, and the right
and bottom
margins positive.
Earlier, I glossed over the fact that creating a new Control node in Godot’s editor doesn’t actually create a node with no size, despite anchors being set to 0. That’s because Godot defaults the new Control’s right and bottom margins to 40px, which results in giving them a default rect_size
of Vector2(40, 40)
. I don’t know of official documentation explaining why, but my guess is that this is to try and minimize confusion around why new controls have no size.
Wait, what’s rect_size
? How is that related to margin values? Good questions!
How Anchors and Margins Impact Other Control Properties
While Anchors and Margins are the core aspects that determine a node’s position and size, they do so in coordination with other node properties. Changing the anchors and margins of a node will usually automatically adjust these tangential properties, and vice versa.
Let’s go over what these properties are.
Rect Size
The rect_size
property is the actual size of the control node. This can be set directly, but often this gets readjusted based on the settings of the anchors and margins. The important thing to remember is that this value always represents the node’s actual size in-game.
Rect Position
rect_position
is the point where the control “is” in the game’s UI space (aka it’s point of origin). Like rect_size
, this can be set manually, and is also automatically adjusted based on interactions with anchors and margins.
Rect Min Size
The rect_min_size
property forces Godot to never shrink this particular node below the specified size. Unlike rect_size
and rect_position
, this is never adjusted automatically by Godot. It’s useful for when you absolutely need to have a control not shrink below a certain size, but be careful: it’s easy to abuse this property to hack around badly-set UI properties. (I certainly used it this way!)
Layout Presets
At this point, if you’ve worked with Godot’s UI before, you may have realized something: “This feels awfully similar to what happens when I use the Layouts button!”
That’s because those Layouts are nothing more than common presets for the anchor and margin values. For example, “Full Rect” is the exact same thing as setting top and left anchors to 0, right and bottom anchors to 1, and all margins to 0. Meanwhile, the “Center” preset sets all anchors to 0.5 (aka 50%) and then automatically calculates the margin values such that they are half of the node’s minimum size, resulting in a centered node.
The presets specified were common enough that Godot’s developers decided to make a convenient way to set them, but it can be confusing when you try to use them without understanding how the underlying system works. I’ve definitely had confusion about why a previously “centered” control wasn’t updated automatically when I did something which changed the node’s size. The reason why is because the presets don’t automatically update in response to changes; they just act on whatever you have architected at the time they get used. Thus, if I change something which affects the node’s size, I need to reapply the “Center” preset to get the node to look centered again.
Child Control Nodes
What happens if you change anchors and margins for a child node? Exactly the same thing as changing those values for its parent! All the examples I’ve used to this point have had nodes be children to a root node that matches the size of the viewport, but the viewport itself is completely irrelevant to the sizing of nodes. If you have a sized control, and adjust the anchor and margin values of its children, they will fit within that parent control’s space.
That’s incredibly powerful and predictable, to have a UI system which functions the same for every Control-based node in Godot.
Well, almost all nodes…
Container Nodes are Exceptions
There is a class of node in Godot called a Container. Container nodes themselves can be sized with anchors and margins, just like any other node. However, the children of container nodes get automatically sized and positioned by that container’s internal logic, ignoring (and overwriting) any manually-set size and position values.
There are multiple kinds of Container nodes, each with their own internal logic for how children size and position are handled. To give a few examples:
HBoxContainer
aligns its children horizontally.VBoxContainer
aligns its children vertically.GridContainer
aligns its children within a set grid of columns.CenterContainer
centers all direct children to its own center.
Godot’s documentation does call out this behavior, and the layout presets are disabled when working within container nodes (the latter behavior to prevent developers from using them in places where they simply won’t work). If you hadn’t understand how the UI system works overall, like me, then these explanations and behaviors may have felt more like descriptions rather than elucidations.
Size Flags
You do still have some control over the placement and sizing of nodes within a Container, through size flags. Size flags have four types of behavior, for both horizontal and vertical axes (all of which can individually be turned on or off).
fill
causes the node to fill the space given to it. That space is governed by both the parent container’s size and the node’s own calculated size.
expand
causes the node to fill any space within the parent container not already used by another node. If neighboring nodes are not set to expand, they get pushed by ones that do. If adjacent nodes both expand, they split the space between them.
shrink center
causes the node to occupy only its minimum possible size, while also centering its own position relative to its neighbors and its parent container.
shrink end
is the same as shrink center
, but with the position at the end of the available space instead of the center.
Not setting any of the above flags makes the node act as though a shrink begin
property existed.
The important thing to remember with positioning nodes within containers is to not worry about getting the nodes in a specific position or size, but to get them aligned according to whatever ratio you’re trying to achieve. The advantage of this approach is that you can place container nodes anywhere within the UI, and they will automatically take care of placing and sizing their children to match that same ratio.
Why Won’t My Control Node Behave in a Container
Have you ever tried to put a Control node in a Container node and it behaved like this?
At first glance, it seems like the Container isn’t size-managing the Control, but that’s not actually the case. The truth is that Control nodes, by default, do not adjust themselves based on the size of their own children. (In other words, Control nodes are not Containers!) In fact, the Control node is resized by the Container, but since the Control isn’t expanding to the size of its children its own size is getting set to 0.
There are two ways to get more expected behavior. One is to put a rect_min_size
on the Control node so there is something for the Container to resize.
The other way is to use the Control node’s size flags.
Which one should be used? It will depend on the effect you’re trying to achieve. If you just need the node to occupy space, a rect_min_size
should do the trick. For more dynamic size adjustment, changing the size flags works best.
Conclusion
This is how Godot’s UI sizing and positioning system works: Anchors, Margins, and Containers. Now that I understand this, I’ve had a much easier time crafting UI that is placed exactly how I want it to be. The system is simple, but until I grokked how it works it felt confusing and unintuitive.
Hopefully, this post helps you better understand Godot’s UI as well!
Postmortem: Belle Bubb’s Sand-Witch Shop
Rebecca and I have spent the last half year working on the same game: Dice Tower (the full release). We hit a milestone of getting the game ready enough to show at a local playtest livestream, and I really wanted to take a break from Dice Tower development. The two of us decided to participate in another game jam; it would let us try making something different, and possibly something that would be worth continuing after Dice Tower was finished.
Participating in more game jams was something we’d intended to do after GMTK Game Jam last year, but we spent so much time focused on Dice Tower that we never made time for it.
After some research into game jams happening at the time, we chose to enter Godot Wild Jam 54. It’s a game jam where only Godot engine-made games were allowed, so we’d get a chance to see other games made in that engine. It was a smaller size game jam than others we’d entered in the past, so the odds that people would play our game (and leave feedback) was higher. Finally, the length of the jam was nine days, and we thought that would allow us to better fit jam work in alongside our normal day job and family responsibilities without taking too much time off.
The game we created was Belle Bubb’s Sand-Witch Shop, and it was about working in a sandwich shop making “cursed sandwiches” (fitting the jam’s theme of “curses”) through 2D drag-and-drop gameplay. Sadly, we were unable to finish the game due to a convergence of multiple major issues; that said, those issues provided great insight into how to get better at making games, so I don’t think our efforts were wasted.
I’ll identify what those issues were, explain why they were problematic, and explain how I’m going to learn from them to become a better game developer.
Issue: Isolated Tests
The gameplay for Belle Bubb’s Sand-Witch Shop involved doing new kinds of things that I was less familiar with, like drag and drop mechanics, evaluating what constituted a “sandwich”, and having ingredients that could interact with one another both during sandwich assembly and during sandwich consumption (aka “eating”). I chose to try and build each system in isolation and test that each worked well enough before moving on to others.
Creating all the systems in isolation meant there was no gameplay loop for the majority of the game’s development, so there couldn’t be any testing around whether the game was actually fun. Sure, dragging ingredients around and having them interact with one another was cool, but without evaluating them it was just an aimless sandbox experience. Testing the systems independently also meant I couldn’t see how well they’d interface with one another. This had catastrophic consequences when I encountered fatal issues with those systems once they were brought together.
Lesson Learned: No More Isolated Tests
I like making isolated tests because it’s simpler for me to wrap my head around them; I also don’t have to immediately worry about making them work as part of a whole. However, this is twice now that waiting to integrate systems until later in the project has resulted in slower progress. The jam version of Dice Tower was able to recover from this; Belle Bubb’s Sand-Witch Shop was not. Therefore, for games I’m working on the future, I need to build all the systems together at once. That should help me identify integration problems much sooner, and come up with better fixes for them.
Issue: Badly-Designed Resource System
The foundation of evaluating the sandwich structure was a system that made heavy use of Godot’s customizable Resource
nodes to store data in complex ways. This was somewhat similar to resource-based work that I’d created for Dice Tower, so I thought it wouldn’t be too much work to do the same kind of thing here. The architecture I came up with involved having the Ingredient resource store references to its UI model, and then the UI model also referencing its own Ingredient resource.
This dual-reference approach led to horrible issues with resource uniqueness. I was treating each Ingredient resource as a unique entity, but Godot treats resources as shared entities; anything holding a reference to a resource refers to the same instance of that resource. Since each ingredient, in design, could have its own unique set of curses and effects, I had to do a lot of duplication when creating new Ingredients and assigning them to models. This created all kinds of bugs where the stored data on a model or resource didn’t match what the game systems expected, and the massive amount of nesting took hours for me to debug.
All that stemmed from one complex resource system. I built multiple such architectures, and then had them all reference each other… The bugs stemming from that hell were the reason we wound up not being able to fully complete the game.
Lesson Learned: Don’t Abuse Resources
Working with Godot’s resources allows for creating powerful customizable systems, but it is incredibly easy to abuse that power to the point of making things insanely difficult to troubleshoot. I need to not use resources so much for the data solutions. At the very least, I need to minimize how much nested resource storage I’m doing, and make sure the architectures I’m coming up with aren’t fatally flawed before building on top of them.
Issue: Too Much Undetected Complexity
I had no idea how complex the game we were trying to make actually was. To illustrate, these are the features which we managed to get into the game. At the time of planning, I thought they were simple:
- Sandwiches have ingredients
- Ingredients have curses
- Curses have effects
- Ingredients can also have effects
- The same effects can be assigned to both curses and ingredients
- Ingredients have individual tastes
- Sandwich ingredients are combined together to form a recipe
- They also come together to form the actual sandwich
- Both of these have a “taste profile” that is the amalgamation of all ingredient tastes
- They also have an “effects profile” that is the amalgamation of all curse and ingredient effects
- Recipe and food need to be compared to each other to determine if they are “close enough”
- All of the above needs to be represented in-game and in UI
That wasn’t even the full scope of what we had originally planned to put into the game. Other ideas include:
- Randomizing which ingredients have what curses
- Getting paid for making sandwiches
- Using earned money to choose what ingredients to buy
- Having a story about how you sold your soul to keep your sandwich shop alive
- Post-sandwich order review that included descriptions of what effects your curses had
That was a lot of complexity. It didn’t feel that way at the beginning because I didn’t think things through. The further we got into making the game, the more I realized the amount of effort needed to make all these systems and interactions work was insane.
And, because I waited so long to bring systems together, I didn’t experience this revelation until it became too late to pivot to something simpler.
Lesson Learned: Work Through Systems Implications in Advance
I need to have much better insight ahead of time where potential complexities will arise. The best way I can think about doing that is diagramming them somewhere, on paper or in a whitescreen board app, so I can visualize what systems need to interact with one another. If there’s too much complexity, then it’s a clear sign that I need to scale things back. I’ve been worried in the past that this will make the process take longer, but the consequence of not trying to do so is having things take longer anyway and not realize it until it’s too late.
Issue: There Was No Planned Endgame
We came up with an idea for interesting gameplay, but had no concept of what the goal of the game was going to be. I kept putting a pin in it to “come back to” after the other systems were developed. Since none of the other systems were ever fully completed, we never got a chance to figure out what happens at the end.
Lacking this conceptualization of the player’s goal contributed to the underestimation of how much work was needed. There was no way to test the player could play through a core gameplay loop because we didn’t know what said loop was beyond “make a sandwich”. Without that guiding star, it was too easy to fall into a pattern of perfecting the systems with no plan for whether those systems would work well together to create a satisfying resolution.
Lesson Learned: Define the Goal at the Outset
Knowing what the player’s in-game goal is from the beginning not only gives a development target to shoot for in terms of minimum viability, but it also makes it clear what systems really need to be kept to make a fun gameplay experience. That doesn’t mean the goal has to be immutable; in fact, the goal will likely need to be simplified further as development roadblocks cut into available development time. If the goal is known in advance, though, then figuring out how to juggle all the necessary concerns should become much simpler.
Issue: I Was Overworked
As mentioned previously, we’d spent a long time before this working on Dice Tower. In that entire time, I hadn’t really taken a break, and this caught up with me during the jam.
I’m no stranger to pushing through burnout—I don’t think I’ve ever fully recovered from burning myself out nearly a decade ago—but I underestimated how much easier it is to work through burnout when on a consistent, slow grind. When I needed to work fast and come up with good ideas quickly, my brain refused to cooperate. That’s one of the reasons I didn’t have a good grasp on how complex the ideas were: I couldn’t think far enough ahead to evaluate the consequences of those ideas, so I instead ignored that part of the process to work on the next thing necessary. The consequence of working through major burnout was significantly slowed progress, much too slow for a game jam. Subsequently, since I fell behind pace, I didn’t give myself time to rest during the jam, which only further exacerbated the mental problems I was dealing with. It was a nasty negative feedback loop.
Lesson Learned: Take a Break!
I’m bad at taking time off to actually take time off. I also know that it’s easier for me to push through tiredness and make progress if I do it consistently. But there’s a limit to how much grind I can endure, and I need to respect that. At the very least, I should take some time off from game development and other project work prior to starting a new game jam, to give myself time to recharge properly. That should hopefully make it easier for me to work and plan properly, and thus also make it easier to not feel like I’m falling behind, resulting in pacing my work schedule appropriately.
Conclusion
I entered a game jam with the intent of working on a different project which might become future inspiration. Instead, I endured an experience that revealed multiple flaws with how I approach game development. Though the experience was painful, the lessons I learned from it are valuable, and that makes me glad for having gone through it.
Lest this postmortem post make it seem like the entire project was a disaster, let me highlight some of the things that did go right:
- People liked messing around with making the sandwiches, so there is an idea there which might be worth future exploration.
- While the resource system was buggy, it did allow for relatively easy creation of ingredients, curses, and effects.
- Of all the things that got cut, at least sound and music weren’t among them this time!
Our focus is going back to Dice Tower, but we definitely intend to enter game jams more frequently in the future. Even when the projects don’t succeed in being good games, they succeed in being great teachers.
Postmortem: Dice Tower
This July, Rebecca and I took part in Game Maker’s Toolkit Game Jam 2022 (an annual game jam hosted by the creator of YouTube channel Game Maker’s Toolkit, Mark Brown), making a game called Dice Tower in only fifty hours. It was our first game jam in a long time, having spent the last three years focusing (and failing) on making our “first big release”. Those fifty hours proved to be an intense, fulfilling experience, and I want to examine how things went. I’ll start with the goals that Rebecca and I made, continue into what happened during the jam, and conclude with whether I felt our goals were met and what we’ll do in the future.
Our Goals
First and foremost, Rebecca and I wanted to actually release the game! Our last jam effort, Rabbit Trails, never saw the light of day because we didn’t get the build submitted in time, and we were determined to avoid making that mistake again.
I wrote briefly about this experience as part of my retrospective on 2019.
To that end, we explicitly wanted to scope our game small enough that it had a realistic chance of being completed by jam’s end. Further supporting that goal, we also decided that we would avoid trying to come up with something “clever”; we would be fine with coming up with feasible and fun ideas, even if they might be ideas that other people were likely to come up with. Finally, we determined that we would specifically make a 2D side-scrolling platformer, because that was the genre we were most familiar with; we didn’t want to waste time figuring out how to do a genre that we lacked experience in making.
Based on what had happened in previous jams, we had a few other goals we wanted to meet. Given that our previous games felt bland, we wanted to make sure whatever we made felt more polished and juicy, thereby increasing how good it felt to play. Previous jams had left us feeling burnt out and exhausted, and, while that is a traditional hallmark of game jams, we strongly wanted to avoid feeling that way at this jam’s end, so we made plans to limit how many hours we’d work per day so that we’d have time to relax and get our normal amount of sleep.
Separate from the in-jam goals, we had one ulterior objective: we wanted to see if the idea we came up with was good enough to develop further into a small release. The game we’d been working on, code-named “Squirrel Project”, was still very much in the early prototyping stage; we’d built and tried a lot of things, but made little progress in actually putting together a full game experience. Both of us felt concerned with how much time we were taking in making a “small game”, so we were interested in seeing if doing game jams might give us smaller concepts that would be easier to develop and release quickly. This jam would serve as a proof of concept for this theory.
Day One (Friday, 7/15/22)
The day of the jam arrived. Our son was off spending the weekend with relatives, we’d prepared our meal plan for the next couple of days, and I’d taken time off from my day job. We sat around my laptop, awaiting Mark Brown’s announcement of the theme. At noon, the video was released, and we saw the theme emblazoned on the screen:
Initial Planning
I hated the theme at first impression. One of my takeaways from working on Sanity Wars Reimagined (our first-ever full release) was that working with randomness in game design was hard, and now we had a game theme which strongly implied designing a game focusing around randomness. In my mind, it would be harder to design a game around dice that wasn’t random in some way. Concerned now with whether we would have enough time to make a good game, I started brainstorming ideas with Rebecca.
We spent that first hour working out an idea; the first idea we hit upon wound up dominating the rest of our brainstorming session, to the point that we didn’t seriously entertain anything else. The game would be a rogue-lite platformer, where you moved to various stations and rolled dice to determine which ability you got to use for the next section of gameplay. Throughout the level would be enemies that you could defeat to collect more dice, which would then give you a better chance to roll higher at the ability checkpoints, thereby increasing your odds of getting the “good” abilities.
I intentionally wanted to minimize the randomness of our game so that players felt they had some level of control over the outcomes of dice rolls, and I liked the idea of using dice quantity to achieve this outcome. The more dice you add to a roll, the more likely you are to get certain number totals. This is known as a probability distribution.
At the end of that hour, we formally determined that this was the idea we wanted to work with. Rebecca started crunching out pixel art, and I got to work making a prototype for our envisioned mechanics.
That Old, Familiar Foe
The next few hours of my life were spent working on the various elements that would serve as a foundation for our mechanics. I threw together some simple dice code, along with dice containers that could roll all the dice they were given. I also pilfered my player character and enemy AI code from Squirrel Project, to jumpstart development in those areas.
But something happened as I started trying to work out how I was going to make the player’s abilities work: my brain began to freeze up. This was a frighteningly familiar sensation: I’d felt this way near the end of developing the original Sanity Wars (for Ludum Dare 43). Dark thoughts started clouding my mind:
There’s not enough time to make this game.
You’re going to fail to finish it, just like your last game jam.
Is what you’re feeling proof that you’re not really good enough to be a game developer?
Slowly, I forced myself to work through this debilitating state of mind. After several more hours, I came up with a janky prototype for firing projectiles, and a broken prototype for imparting status effects on the player (like a shield). It occurred to me that if I was having this much trouble making something as simple (cough cough) as player abilities, then how was I going to have time to fix my glitchy enemy AI, and develop the core mechanic of rolling dice to gain one of multiple abilities, and make enough content to make this game feel adequate, let alone good. Oh, and then there was still bugfixing, sound and music creation, playtesting…
It finally hit me: This idea wasn’t going to work. There was simply too much complexity that was not yet done, and I was struggling with the foundational aspects that needed to be built just to try out our idea. There was no way I would be able to finish this vision of our game on time.
My mind in shambles and my body exhausted, I shared my feelings and concerns with Rebecca, and she agreed that we needed to pivot to a new idea. We tried to come up with something, but we were both too tired to think clearly. Therefore, we decided to be done for the evening, get some supper, and head to bed.
Note the lack of planned relaxation time.
As I lay in bed, I fretted about whether we could actually come up with a new idea. That first idea was by far the best of what few ideas we’d been able to come up with during our brainstorming session; how were we going to suddenly come up with an idea that was good enough and simpler? With these worries exhausting my mind, I fell asleep.
Day Two (Saturday, 7/16/22)
The next morning, at 6am, I woke up, took a shower, and played a video game briefly. This was my normal morning routine during the workday, and I was determined to keep to it. At 7am, I grabbed a cup of coffee and sprawled out on the couch, pad of paper and pen resting upon an adjacent TV tray, prepared to try and come up with a new idea.
Rebecca joined me at 7:30am, after she did her own waking routines.
The New Idea
I wrote down the elements we already had: Rebecca’s character art and tileset from her previous day’s work, some dice and dice containers, and a player entity that was decently functional as a platformer character. If our new idea could incorporate those elements, then at least not all of yesterday’s work would go to waste.
After thinking about it for a long while, I hit upon an idea: what if you rolled dice to determine how much time you had to complete a level? As you moved through each level, you could collect dice and spend them at the end-of-level checkpoints to increase your odds of getting more time to complete the next level. If that were combined with a scoring system involving finding treasure collectibles that were also scattered throughout each level, then there’d be potentially interesting gameplay around gambling how many dice you’d need to collect to guarantee that you had enough time in each level to collect enough treasure to get a high score.
I pitched the idea to Rebecca, and after some discussion about that and a few other ideas, we decided this was the simplest idea, and therefore had the best chance of being completed before the deadline. Fortunately, this idea also did successfully incorporate most of the elements that we’d worked on yesterday, so we didn’t have to waste time recreating assets. On the other hand, this idea needed to work; there was likely not going to be any time to come up with another plan if we spent time on this one and it failed.
Our idea and stakes in mind, Rebecca and I once more commenced our work.
Slogging Through the Day
I created various test scenes in Godot. Slowly, I began to amass the individual systems and entities that I’d eventually put together to form the gameplay. Throughout the day, I felt very sluggish and lethargic mentally; this was likely a side effect of the burnout I’d put myself through yesterday. I kept reminding myself that some amount of progress was better than none at all, but it still didn’t feel great.
By the start of that evening, I had a bunch of systems, but nothing that fully integrated them. Taking a break, I went outside for a walk and some breaths of fresh air. As I trod the trails in our neighborhood, I worked out the game’s next steps. I reasoned that if I continued to put each system and entity together in isolation and test them, I stood a realistic chance of running out of time to put everything together into a cohesive game. Therefore, I decided to forgo this approach, and simply put everything together now, and build the remaining systems as I went along. This flew in the face of how I traditionally prefer to program things, but I resolved to set aside my clean code concerns and focus simply on getting the game working.
The Late Evening Dash
Not long after I returned from my walk, it became 7pm. This was the twelve-hour mark, and in our pre-jam plans I’d determined that I wouldn’t work more than twelve hours on Saturday. Yet I still didn’t have much that was actually put together. I was now faced with a conundrum: should I commit to stopping work now, and risk not having enough time tomorrow to finish the minimum viable product?
Ultimately, I decided that I would try to get an MVP done tonight, or at least keep working until I felt ready to stop. Three hours later, while that MVP was still incomplete, I had put together the vast majority of what was needed to make the game playable: a level loading system, the dice-rolling checkpoints, dice collection (though said dice weren’t yet connected to the checkpoints), the level timer and having it set from rolling the checkpoint dice, rudimentary menu UI, and player death. At this point, I felt that continuing to work would just cut into my sleeping time, and I was still determined to get a proper amount of sleep. At the least, those few hours had produced enough progress that I felt confident that I could finish things up tomorrow morning.
I quickly prepared for bed, and after watching some YouTube to wind down I fell asleep.
Day Three (Sunday, 7/17/22)
That morning, I woke up at 6am, as usual, but this time I skipped all of my morning routine save the shower. By 6:30am, I was at my computer and raring to go.
Blazing Speed and Fury
The first thing I did was export the game as it currently was. I’d been burned enough times by last-minute exports that I was determined to make sure that didn’t bite me in the butt this time. Fortunately, this time there were no export-specific crashes or bugs, so I resumed work on the game proper.
For the next several hours, my mind singularly focused on getting this game done. I finally made the checkpoints accept the dice you collected, thus completing the core loop of rolling dice to determine the time you had to make it through the level. I added game restart logic, added transition logic for when the player was moving between levels, added endgame conditions and win/loss logic, and fix various bugs encountered along the way. I also realized that I wouldn’t have time to implement a scoring system, so I unceremoniously cut it from the MVP features.
Around noon, I finally had the game fully working. I could boot the application, start a new game from the main menu, play through all the designated levels, and successfully reach the victory level to win the game (or run out of time and lose). If nothing else, we at least had a playable game!
Final Push
The game jam started at noon on Friday, and as it was now noon on Sunday that meant 48 hours had passed. Thankfully, the jam had a two-hour grace period for uploading and submitting games to Itch.io. That meant I had less than two hours to jam as much content and polish into the game as I could before release!
I blazed my way through creating six levels, spending less than an hour to do so. Of course, that meant I had little time to balance the levels properly, beyond ensuring they could be completed in an average amount of time. One thing I did spend time on was adding the various pieces of decorative art Rebecca made to each level. It might seem frivolous to add decorations, but I think having a nicely-decorated level goes a long way towards breaking up level monotony and sameness, and honestly it didn’t take that long for me to add those things.
After a few trials, I hit upon a solution for balancing the randomness for the dice checkpoints: I’d give the player six free seconds at the start of each level, and a single die at each checkpoint (in addition to the ones collected through gameplay). As I playtested the level, that at least felt long enough that, using probability curves, the average player could have a decent chance of finishing each level.
Intentionally, I chose to not focus at all on adding sound effects and music. My reasoning went thusly: players would probably prefer to have more content to play through than have sound and music with very little content. It made me remorseful, because sound can make an okay game feel great, but there just wasn’t enough time to do a good job of it, so I decided it was better to just cut audio entirely.
Finally, at 1:30pm, I looked at what we had and determined it was good enough. Rebecca had been working on setting up our Itch.io page, and I gave her the final game build export to upload to the store page. We submitted the game just in the nick of time; shortly after our submission was processed, Itch.io crashed under the weight of thousands of last-minute uploads. While this server strain prevented us from adding images for our game page (we got them in later), at least we already had the game uploaded and submitted, so we weren’t in danger of missing the deadline.
Rebecca and I looked at each other. We’d done it! We’d successfully made a game and submitted it! We spent the rest of the afternoon out and about on a date, taking advantage of our last bits of free time before returning to parenthood the next day.
Feedback
Over the next week, our game was played and rated by people. We received multiple comments about how people enjoyed the core idea, which was encouraging.
A couple of our friends from the IGDA Twin Cities community took it upon themselves to speedrun our game. This surprised me, because I thought the inherently random nature of our game would be a discouragement from speedrunning. They told me they enjoyed it a lot, however, and over the course of that week they posted videos of ridiculously speedy runs and provided good feedback.
By happenstance, the Twin Cities Playtest session for July was that same week, so Rebecca and I submitted Dice Tower to be played as part of that stream. Mark LaCroix, Lane Davis, and Patrick Yang all enjoyed the game’s core concept, and gave us tremendous feedback on where they felt improvements could be made, as well as different directions we could go to further expand and elaborate on the core mechanic. It was a great session, and we are greatly indebted to them for their awesome feedback.
After the play-and-review week had passed, Mark Brown made his video announcing the winners of the jam, and we got to see our final results online (we did not make it into the video, which only featured the top one-hundred games).
For a game jam with over six thousand entries, we did surprisingly well, placing in the top 50% in overall score. Our creativity score was in the top third, which pleased me greatly; it felt like further validation that our core concept was good enough to build on.
Reflection
I want to circle back to those goals Rebecca and I set before the jam, and see how we did in meeting them. There were a couple of other takeaways I had as well, which I’ll present after looking at the goals.
Did We Meet Our Goals?
First and foremost, we wanted to release a game, and we did! That was huge for us, given our last effort died in development. In particular, to release this game after the mental fracturing I endured on Friday night, and having to pivot to a new idea, shows that, perhaps, we’re decent game developers, after all.
Did we scope our game properly? At the beginning, no, definitely not. Fortunately, we recognized this (albeit after spending a day on it), and decided to change to a more feasible idea. Even though we had to cut scope from the new idea as well, the core was small enough that we were able to make it in the time allotted. It gives us a new baseline for how much work we can fit in a given amount of time, which should serve us well in future endeavors.
It’s a similar story for our theme interpretation. Our first idea seemed simple, but turned out to be complex under the hood, as it’s a lot of work to not only add lots of different player abilities, but game entities (such as enemies) to use those abilities on. In hindsight, I laugh at how we thought that kind of game was feasible in 48 hours, with not nearly enough foundation in place beforehand. That said, once again, we pivoted from the complex to the simple, and the new theme interpretation was simple enough to be doable.
Ironically, I thought this would be a common enough idea that plenty of other people would do it, but I actually never encountered a mechanic similar to this during my plays of other jam games, and multiple people commented on the novelty of the idea. Perhaps our focus on coming up with a good idea instead of a unique one managed to get us the best of both worlds!
We stuck to our guns and made a 2D side-scrolling platformer, even though at times I felt like that made it more difficult to find a good theme interpretation. Because we knew how to work in that genre, it made our mid-development pivot possible; I don’t think we could have been successful in doing that if we’d tried to make something unfamiliar to us. Additionally, I think using a genre where dice are less commonly used led to coming up with something more unique than we might’ve if we’d done an RPG or top-down game (two genres I thought would be easier fits for the theme).
One area we did miss on was making this a polished release. The missing audio was sorely felt. That said, I really like the art that Rebecca came up with, especially the decorations, and I don’t regret the decision to cut sound in favor of making levels and placing her doodads. Honestly, despite the missing audio, the fact that this game was fun and playable made this jam release feel more polished than our previous jam efforts.
A big success we had was in self-care management. Even though we didn’t stick to the maximum hours per day we set before the jam, we took care to make sure we got enough sleep each night. The end result is that this is the first jam we’ve done that both of us didn’t feel utterly spent at the end of it. That allowed us to enjoy a lovely afternoon and evening together as a couple, and we didn’t feel totally shot for days thereafter. I think that also helped us have the energy needed to push us through to the finish line.
One final goal remains to be evaluated: was our game good enough to base a future release upon? The answer is “yes”. Based on the enthusiastic feedback we received, plus our own thoughts about where the concept could go, we’ve decided to put Squirrel Project aside and focus on making a full, but small, release out of Dice Tower. With some more features and polish, and maybe a dollop of background story to tie things together, we think Dice Tower could make a good starting point for our first paid release.
Additionally, we plan to take part in future game jams, and further explore the concept of making quick games to find small ideas with good potential.
Other Takeaways
There were some additional things we learned from the jam. These emerged as byproducts of the things we tried during the jam.
Our initial thoughts were that we’d spend our brainstorming session coming up with multiple ideas, and then make small prototypes for each of them and decide which of them was the one we wanted to make. It didn’t turn out that way; both our first and second attempts settled early on a single idea, to the point where we didn’t really have many other ideas that we seriously considered. I think that perhaps that’s just how Rebecca and I work; it’s easier for us to jump into an idea and try it rather than piece a bunch of ideas together and flesh all of them out on paper. Perhaps in our next jam, then, we’ll deliberately settle on an idea right away and just go for it, making explicit allowance for pivoting if it starts feeling like too big an idea to work with.
Personally, I also learned that I need to let go of trying to test every little thing in isolation. That may be a good approach for building long-lived, stable systems, but for something as volatile as game prototyping it just slows the process down too much, with no game to show for it. By putting things together as early as possible, it makes things feel more real because it is the real game! I’ll keep this in mind for future projects, to just make a go of it right off the bat, and save my efforts to make things nice and tidy for when the ideas are codified and made official.
Conclusion
Ultimately, I’m glad Rebecca and I decided to participate in GMTK Game Jam 2022. It feels exhilarating to not only finish a game in that short amount of time, but to defeat some of my internal demons from previous jams in the process. We feel more confident in our ability to develop and release games, and we’re excited to tackle making the full release of Dice Tower.
It’ll be interesting to compare this postmortem with the postmortem for whatever that game becomes!
In the meantime, though, feel free to try out our jam release of Dice Tower and let us know what you think!
Postmortem: Sanity Wars Reimagined
After seven months of development, Rebecca (PixelLunatic) and I have released Sanity Wars Reimagined! Along the way, we learned a lot, lessons we hope to apply to future games we develop. In this article, I want to dig a little bit into what we learned, from what we were initially trying to do to where we wound up, and the various lessons learned and mistakes made along the way.
You can check out Sanity Wars Reimagined on Itch.io!
This project started with a discussion Rebecca and I had about where things were from a long-term standpoint. For the past few years, we’d attempted, and abandoned, various prototypes, and it seemed like we were still far off from actually releasing a product. At the time, I had just come off of a month-long project exploring the creation of game AI; my intent was to start another project aimed at making improvements to the dialogue system. During our discussion, however, Rebecca pointed out that, while our ultimate goal was to make games and sell them, we had a bad habit of not committing to anything long enough to get it released. What’s more, she was concerned that this pattern of starting and abandoning projects wasn’t good for our morale.
She was right to be concerned about this; I’d felt that making an actual release was still a point far off in the distance, and this was making it easier for me to accept the idea of working on non-release projects, to “gain experience”. How would we ever learn how to improve, though, if we never got our projects to a releasable, playable state? Only two of our game jam games and one of our prototypes had ever been given to other people to play, so we had very little feedback on where we needed to improve. I realized that we couldn’t keep waiting to release something until “we got better”; we needed to make something and release it, however bad it may be.
We decided that we would start with a small project, so that it would take less time to get it to a releasable state. To that end, we determined that the project should be a remake of our first jam game, Sanity Wars. By remaking a game that was already released, we thought, we could focus on actually building the parts needed to make the game work; since I’d made those things work previously, we would hopefully avoid the pitfall of trying to create mechanics that were beyond our current skill to implement, or would take too much time to build. Why Sanity Wars? Out of all the previous jam games we’d made, it seemed like the most successful one, so we thought we could just add some polish, redo the art (since Rebecca did not do the original’s art), and it would be fine.
With that, our next project was set: Sanity Wars Reimagined. We would stay faithful to the mechanics of the original, aiming only to remake them in Godot, as this would be quicker than trying to iterate and make new mechanics. I would also take the opportunity to try and make systems that would be reusable in future games; ideally, we would treat Sanity Wars Reimagined as a foundation that we would directly expand upon for the next game. Since the original Sanity Wars was done in three full days, I thought this project wouldn’t take long to complete. Accounting for our busy adult schedules, I estimated the work would take two weeks to complete; at most, a month.
It didn’t take two weeks. It didn’t take a month. It took seven months before we finally released Sanity Wars Reimagined on Itch.io. Along the way, we made significant modifications to the core mechanics, removing multiple parts that were considered essential to the original Sanity Wars; even with those changes, the end result was still not that fun (in our minds). There were many times during the development period where it felt like it was going to drag on and on, with no end in sight. All that said, I still think Sanity Wars Reimagined was a successful release.
Why do I think that? To answer that question, I want to examine what technologies we developed during the project, what mistakes we made, and what we plan to do to improve things for our next project.
Technologies Developed
A lot of what I made from a code standpoint was able to be imported back into my boilerplate Godot project, which will then be available from the start when I clone the Genesis boilerplate to make any future game project. In doing so, I’ve hopefully decreased the amount of development time needed for those projects.
Genesis is name of a tool I created in NodeJS that lets me keep a centralized Godot boilerplate template and create new projects from that boilerplate using simple commands in a command line interface. To give a non-technical summary, it allows me to quickly create and update new Godot projects that include common code that I want to reuse from project to project.
Here are some of the things that I’ll be able to make use of as a result of the work done for Sanity Wars Reimagined:
Resolution Management
There is a lot to consider when supporting multiple resolutions for a game, especially one that uses a pixel art aesthetic. I was already aware of this before committing to figuring out a solution for Sanity Wars Reimagined, but I underestimated just how much more there was to learn. The good news is that I created a solution that not only works for Sanity Wars Reimagined, but is generalized enough that I can use it as the starting point for future games.
I’ll talk in brief about some of the struggles I had to contend with and what I did to solve them.
For starters, when working with pixel art, scaling is a non-trivial issue. Because you are literally placing pixels in precise positions to render your art aesthetic, your scaling must always keep the same aspect ratio as your initial render; on top of that, it must specifically be increased in whole integer factors. This means you can only support a limited number of window sizes without messing up the pixel art. That plays a huge factor in determining what your base size is going to be; since most monitors use a 1920px by 1080px resolution size, your base needs to be a whole integer scale of 1920×1080, or else the game view is not going to actually fill the whole screen when it is maximized to fill the screen (aka fullscreen).
The way fullscreen modes are typically handled for pixel art games, when they attempt to handle it at all, is to set the game view to one of those specific ratios, and letterbox the surrounding area that doesn’t fit cleanly into that ratio. That is the approach I chose for my fullscreen management as well.
Godot does give you the means to scale your game window such that it renders the pixel art cleanly, and you can also write logic to limit supported resolution sizes to only those that scale in whole integers from the base resolution. However, there is a catch with the native way Godot handles this: any UI text that isn’t pixel-perfect becomes blurry, which isn’t a great look to have. I could have switched to only using pixel-perfect fonts, but that wasn’t the look I wanted the game UI to have. After spending a lot of time experimenting with ways to handle this in Godot’s settings, I determined that I would have to create a custom solution to achieve the effect that I wanted.
I wound up talking to Noel Berry (of Extremely OK Games) about how Celeste handled this problem, as its crisp UI over gameplay pixel art was similar to what I was hoping to achieve. He told me that they actually made the UI render at 1920×1080 at default, and then scaled the UI up or down depending on what the game’s current resolution was. This inspired me to create a version of that solution for Sanity Wars Reimagined. I created a UI scaling node that accepts a base resolution, and then changes its scale (and subsequently the scale of its child and grandchild nodes) in response to what the game’s current resolution is. It took a lot of effort, but in the end I was able to get this working correctly, with some minor caveats*.
* I’ll be coming back to these caveats later on in the article, when I discuss mistakes that were made.
Overall, I’m very pleased with the solution I developed for resolution management in Sanity Wars Reimagined, and ideally this is something that should just work for future pixel art-based games.
Screen Management
Another system I developed for Sanity Wars Reimagined is a screen management system that supports using shaders for screen transitions. Although my boilerplate code already included a basic screen manager that was responsible for changing what screens were being currently portrayed (MainMenuScreen
, GameScreen
, etc.), a significant flaw it had was that it didn’t provide support for doing screen transitions. This was going to be a problem for Sanity Wars Reimagined, as the original game had fade transitions between the different screen elements. I thus set out to refactor my screen management to support doing transitions.
In the original Sanity Wars, the way I accomplished the fade was through manipulating the drawn image in the browser’s canvas
element (as the original game was built using HTML/JavaScript, the technologies I was most familiar with at the time). It was hardcoded to the custom engine I’d built, however, and there wasn’t a direct way to achieve the same effect in Godot. It’s possible I could have made the fade transition, specifically, work by manipulating the screen node’s modulation (visibility), but I didn’t feel comfortable making direct changes to node properties for the sake of screen effects, especially if I wanted to have the ability to do other kinds of transitions in the future, such as screen slides or flips; anything more complex than that would be outright impossible through mere node property manipulation.
My focus turned towards experimenting with a different approach, one based on using Godot’s Viewport
node to get the actual render textures, and then manipulating the raw pixels by applying shaders to the render images of the outgoing screen and the incoming screen. Viewports were something I hadn’t had much experience with, however, so I wasn’t certain if the idea I had would actually work. To prove the concept, I spent a weekend creating a prototype specifically to test manipulating viewport and their render textures. The approach did, in fact, work as I envisioned (after a lot of research, trial, and error), so I proceeded to refactor the screen management system in Sanity Wars Reimagined to use this new approach.
When referring to screens here, I’m not talking about physical monitor screens; it’s a term I use to refer to a whole collection of elements comprising a section of the game experience. For instance, the Main Menu Screen is what you see on booting up the game, and Game Screen is where the gameplay takes place.
Overall, the refactor was an immense success. The fade effect worked precisely the way it did for the original Sanity Wars, and the system is flexible enough that I feel it should be easy enough to design new screen transition effects (in fact, I did create one as part of making the LoadingScreen
, transitioning to an in-between screen that handled providing feedback to the user while the incoming GameScreen
prepared the gameplay). Should I want to create different visual effects for future transitions, it should be as simple as writing a shader to handle it. (Not that shaders are simple, but it is far easier to do complex visual effects with shaders than with node property manipulation!)
Automated Export Script
After realizing that I needed to export game builds frequently (more on that later), I quickly found that it was tedious to have to work through Godot’s export interface every time I wanted to make a build. On top of that, Godot doesn’t have native build versioning (at least, not that I’ve found), so I have to manually name each exported build, and keep track of what the version number is. Needless to say, I wondered if there was a way I could automate this process, possibly through augmenting the Genesis scripting tools to include a simple command to export a project.
I took a few days to work through this, and in the end I managed to create functionality in my Genesis scripting tool that did what I wanted. With a simple command, godot-genesis export-project "Sanity Wars Reimagined" "Name Of Export Template"
, Genesis would handle grabbing a Godot executable and running a shell command to make Godot export the project using the provided export template, and then create a ZIP archive of the resulting export. The name of the export was the project name, followed by a build number using the semantics I chose (major.minor.patch.build). By providing a -b
flag, I could also specify what kind of build this was (and thus which build number to increment). It works really well, and now that exports are so easy to do I am more willing to make them frequently, which allows me to quickly make sure my development changes work in the release builds.
Other Features and Improvements
There are many other features that were created for Sanity Wars Reimagined; to save time, I’ll simply give brief summaries of these. Some of these were not generalized enough to be ported back into the Genesis boilerplate, but the experience gained from creating them remains valuable nonetheless.
Generators
These nodes handle spawning entities, and I made the system flexible enough that I can pretty much generate any kind of object I want (though, in this case, I only used it to spawn Eyeballs and Tomes).
RectZone
This is a node which let me specify a rectangular area that other nodes (like Generators) could use to make spatial calculations against (aka figure out where to spawn things).
PixelPerfectCamera
This is a Camera node that was customized to support smoothing behavior rounded to pixel-perfect values. This helps reduce the amount of visual jitter that results from when a camera is positioned between whole integer values.
The reason this happens is because pixels can’t be rendered at fractional, non-integer values, so when a pixel art game asset is placed such that the underlying pixels don’t line up to a whole integer, the game engine’s renderer “guesses” what the actual pixel color values should be. This is barely noticeable for high-resolution assets because they consist of a huge number of pixels, but for something as low-resolution as pixel art, this results in visual artifacts that look terrible.
UI Theming
I finally took a dive into trying to understand how Godot’s theme system works, and as a result I was able to create themes for my UI that made it much simpler to create new UI elements that already worked with the visual design of the interface. I plan to build on my experience with UI themes for future projects, and ultimately want to make a base theme for the Genesis boilerplate, so I don’t have to create new themes from scratch.
State Machine Movement
I converted my previous Player
character movement code to be based on a state machine instead of part of the node’s script, and this resulted in movement logic that was far simpler to control and modify.
As you can see, there were a lot of features I developed for Sanity Wars Reimagined, independent of gameplay aspects. A large part of what I created was generalized and reusable, so I can put the code in future projects without having to make modifications to remove Sanity Wars-specific functionality.
Complications
No human endeavor is perfect, and that is certainly true for Sanity Wars Reimagined. In fact, I made a lot of mistakes on this project. Fortunately, all of these mistakes are things I can learn from to make future projects better. I’ll highlight some of these issues and mistakes now.
Both Rebecca and I learned a lot from the mistakes we made developing this project, but I’m specifically focusing on my mistakes in this article.
New Systems Introduced New Complexities
The big systems I added, like Resolution Management and Screen Management, added lots of functionality to the game. With that, however, came gameplay issues that arose as the result of the requirements integrating with these systems introduced.
Take ScreenManager
, for example. The system included the ability to have screens running visual updates during the transition, so the screen’s render texture didn’t look like it was frozen while fading from one screen to the next. By creating this capability, however, I needed to modify the existing game logic to take into account the idea that it could be running as part of a screen transition; for instance, the player character needed to be visible on the screen during the transition, but with input disabled so the player couldn’t move while the transition was running.
Another issue the ScreenManager
refactor created had to do with resetting the game when the player chose to restart. Before, screens were loaded from file when being switched to, and being unloaded when switched from, so restart logic was as simple as using node _ready()
methods to set up the game logic. After the refactor, this was no longer true; to avoid the loading penalty (and subsequent screen freeze) of dealing with loading scenes from file, ScreenManager
instead kept inactive screens around in memory, adding them to the scene tree when being transitioned to and removing them from the scene tree when being transitioned from. Since _ready()
only runs once, the first time a node enters the scene tree, it was no longer usable as a way to reset game logic. I had to fix this by explicitly creating reset functions that could be called in response to a reset
signal emitted by the controlling screen, and throughout the remaining development I encountered bugs stemming from this change in game reset logic.
ResolutionManager
, while allowing for crisp-looking UI, created its own problems as well. While the UI could be scaled down as much as I wanted, at smaller resolutions elements would render slightly differently from how they looked at 1920×1080. The reason for this was, ironically, similar to the issues with scaling pixel art: by scaling the UI down, any UI element whose size dimensions did not result in whole-number integers would force Godot’s renderer to have to guess what to render for a particular pixel location on the monitor. Subsequently, some of the UI looked bad at smaller resolutions (such as the outlines around the spell selection indicators). I suspect I could have addressed this issue by tweaking the UI design sizes to scale cleanly, but my attempts to change those values resulted in breaking the UI in ways I couldn’t figure out how to fix (largely due to my continued troubles understanding how to create good UI in Godot). In the end, I decided that, with my limited amount of time, trying to fix all the UI issues was less important than fixing the other issues I was working on, and ultimately the task was cut during the finishing stages of development.
I’m guessing most people won’t notice, anyway, since most people likely have the game at fullscreen, anyway.
Complexities arising from implementing new systems happened in other ways throughout the project as well, although the ones stemming from ScreenManager
and ResolutionManager
caused some of the bigger headaches. Fixing said issues contributed to extending development time.
Designing for Randomization
One of the core mechanics of Sanity Wars (original and Reimagined) is that all the entities in the game spawn at random locations on an unchanging set of maps. At the time I created the mechanic, my thought was that this was a way to achieve some amount of replayability, by having each run create different placements for the tomes, eyeballs, portals, and player.
Playtesters, however, pointed out that the fully random nature of where things spawned resulted in wide swings of gameplay experience. Sometimes, you got spawns that made runs trivially easy to complete; other times, the spawns made runs much more difficult. This had a negative impact on gameplay experience.
The way to solve this is through creating the means of controlling just how random the processes are. For example, I could add specific locations where portals were allowed to spawn, or add logic to ensure tomes didn’t spawn too close to portals. Adding controlled randomness isn’t easy, however, because by definition it means having to add special conditions to the spawning logic beyond simply picking a location within the map.
The biggest impact of controlled randomness wasn’t directly felt with Sanity Wars Reimagined, however; it was felt in our plans to expand directly off of this project for our next game. Given that random generation was a core element of gameplay, that meant adding additional elements would also need to employ controlled randomness, and that would likely result in a lot of work. On top of that, designing maps with randomness in mind is hard. It would likely take months just to prototype ideas, let alone flesh them out into complete mechanics.
This aspect, more than anything else, was a huge influence in our decision to not expand on Sanity Wars Reimagined for the next project, but to concentrate on a more linear experience. (More on that later.)
Clean Code
If you’re a programmer, you might be surprised at seeing “clean code” as a heading under complications. If you’re not a programmer, let me explain, very roughly, what clean code is: a mindset for writing code in such a way that it is easy to understand, has specific responsibilities, and avoids creating the same lines of code in different files; through these principles one’s code should be easier to comprehend and use throughout your codebase.
Under most circumstances, writing clean code is essential for making code not only easier to work with, but faster to develop. So how did writing clean code make Sanity Wars Reimagined more complex?
Simply put, the issue wasn’t strictly with adhering to clean code principles in and of themselves; the issue was when I spent a lot of time and effort coming up with clean code for flawed systems. Clean code doesn’t mean the things you create with it are perfect. In fact, in Sanity Wars Reimagined, some of the things I created wound up being harmful to the resulting gameplay.
A prime example of how this impacted development is the way I implemented movement for the Eyeball entity. I had the thought of creating a steering behavior-based movement system; rather than giving the entity a point to navigate to and allowing it to move straight there, I wanted to have the entity behave more like real things might move (to put it in very simple terms). I then spent a long time creating a locomotion system that used steering behaviors, trying to make it as clean as possible.
In the end, my efforts to integrate steering behavior movement were successful. There was a huge flaw, however; the movement hampered gameplay. Steering behaviors, by design, are intended to give less-predictable behavior to the human eye, which makes it harder to predict how the eyeball is going to move when it isn’t going in a straight line. This style of movement also meant the Eyeballs could easily get in a position where it was difficult for the player to hit them with the straight-line spirit bullet projectile, which was specifically intended for destroying Eyeballs. Since steering behaviors work by applying forces, rather than explicitly providing movement direction, there wasn’t an easy, feasible way for me to tweak the movement to make it easier for the player to engage with Eyeballs.
In addition to making Eyeballs less fun to play against, the steering behaviors also made it hard to make Eyeballs move in very specific ways. When I was trying to create a dive attack for the Eyeball, I literally had to hack in movement behavior that circumvented the steering behaviors to try and get the attack to visually look the way I wanted to; even then, I still had a lot of trouble getting the movement to look how I felt it should.
How did clean code contribute to this, precisely? Well, I’d spent a lot of time creating the locomotor system and making it as clean an implementation as I possibly could, before throwing it into the gameplay arena to be tested out and refined. If there had been time to do so, I likely would have needed to go back and refactor the Eyeball movement to not use steering behaviors; all that work I’d spent making the steering behavior implementation nice and clean would’ve gone to waste.
Don’t get me wrong; writing clean code is very important, and there is definitely a place for it in game development. The time for that, however, is not while figuring out if the gameplay works; there’s no sense in making something clean if you’re going to end up throwing it out shortly thereafter.
Playtesting
I didn’t let other people playtest the game until way too late in development. Not only did that result in not detecting crashes in release builds, it also meant I ran out of time to properly take feedback from the playtests and incorporate it back into the game.
For the first three months of development, I never created a single release build of Sanity Wars Reimagined. I kept plugging away in development builds. The first time I attempted exporting the project was the day, no, the evening I was scheduled to bring the game to a live-streamed playtest session with IGDA Twin Cities. As a result, it wasn’t until two hours before showtime that I found out that my release builds crashed on load. I spent a frantic hour hack-fixing the things causing the crashes, but even with that, the release build still had a major, game-breaking bug in it: the testers couldn’t complete the game because no portals spawned. Without being able to complete the game, the testers couldn’t give me good feedback on how the game felt to them. From that point onward, I made a point of testing exports regularly so that something like that wouldn’t catch me off-guard again.
The next time I brought the game out for playtesting was in the middle of January 2022, three months after the first playtest. At that point, I’d resigned myself to the fact that Sanity Wars Reimagined didn’t feel fun, and was likely going to be released that way; my intent with attending the playtest was to have people play the game and help me make sure I didn’t have any showstopping bugs I’d need to fix before release. What I wasn’t expecting (and, in retrospect, that was silly of me) was that people had a lot to say about the game design and ways it could be made more fun.
To be honest, I think I’d been stuck so long on the idea that I wanted to faithfully recreate the original game’s mechanics that I didn’t even think about making changes to them. After hearing the feedback, however, I decided that it would be more important to make the game as fun as I could before release, rather than sticking to the original mechanics.
That playtest, however, was one and a half weeks before the planned release date, meaning that I had very little time to attempt making changes of any significance. I did what I could, however. Some of the things I changed included:
- Removing the sacrifice aspect of using spells. Players could now access spells right away, without having to sacrifice their maximum sanity.
- Giving both the spells dedicated hotkeys, to make them easier (and thus more likely) to be used.
- Adding a countdown timer, in the form of reducing the player’s maximum sanity every so often, until the amount was reduced to zero, killing the player. This gave players a sense of urgency that they needed to resolve, which was more interesting than simply exploring the maps with no time constraints.
- Changing the Eyeball’s attack to something that clearly telegraphed it was attacking the player, which also made them more fun to interact with.
- Adding a scoring system, to give the player something more interesting to do than simply collecting tomes and finding the exit portal.
- Various small elements to add juice to the game and make it feel more fun.
Although we did wind up extending the release date by a week (because of dealing with being sick on the intended release week), I was surprised with just how much positive change I was able to introduce in essentially two and a half weeks’ worth of time. I had to sacrifice a lot of clean code principles to do it (feeding into my observation about how doing clean code too early was a problem), but the end result was an experience that was far more fun than it was prior to that play test.
I can only imagine how much more fun the game could’ve been if I’d had people involved with playtesting in the early stages of development, when it would’ve been easier to change core mechanics in response to suggestions.
Thanks to the IGDA Twin Cities playtest group, and specifically Mark LaCroix, Dale LaCroix, and Lane Davis, for offering many of the suggested changes that made it into the final game. Thanks also to Mark, Lane, Patrick Grout, and Peter Shimeall for offering their time to playtest these changes prior to the game’s release.
Lack of a Schedule
I mentioned previously that I’d thought the entire Sanity Wars Reimagined project wouldn’t take more than a month, but I hadn’t actually established a firm deadline for when the project needed to be done. I tried to implement an approach where we’d work on the project “until it felt ready”. I knew deadlines were a way that crunch could be introduced, and I wanted to avoid putting ourselves in a situation where we felt we needed to crunch to make a deadline.
The downside, however, was that there wasn’t any target to shoot for. Frequently, while working on mind-numbing, boring sections of code, I had the dread fear that we could wind up spending many more months on this project before it would be finished. This fear grew significantly the longer I spent working on the project, my initial month-long estimate flying by the wayside like mile markers on a highway.
Finally, out of exasperation, I made the decision to set a release date. Originally, the target was the middle of December 2021, but the game wasn’t anywhere near bug-free enough by that point, so we pivoted to the end of January 2022, instead. As that deadline approached, there were still dozens upon dozens of tasks that had yet to be started. Instead of pushing the deadline out again, however, I went through the list to determine what was truly essential for the game’s release, cancelling every task that failed to meet that criteria.
Things that hit the cutting room floor include:
- Adding a second enemy to the game, which would’ve been some form of ground unit.
- Refactoring the player’s jump to feel better.
- Fixing a bug that caused the jump sound to sometimes not play when it should.
- Adding keyboard navigation to menus (meaning you had to use the mouse to click on buttons and such).
- Create maps specifically for the release (the ones in the final build are the same as the ones made for the second playtest).
It’s not that these things wouldn’t have improved the game experience; it’s just that they weren’t essential to the game experience, or at least not enough to make it worth extending the release date to incorporate them. By this point, my goal was to finish the game and move on to the next project, where, hopefully, I could do a better job and learn from my mistakes.
These are far from the only complexities that we had to deal with during Sanity Wars Reimagined, but they should serve to prove that a lot of issues were encountered, and a lot of mistakes were made. All of these things, however, are learning opportunities, and we’re excited to improve on the next project.
Improvements for Next Time
There’s a lot of things that I want to try for the next project; many of them serve as attempts to address issues that arose during the development of Sanity Wars Reimagined.
Have A Planned Release Date
I don’t want to feel like there’s no end in sight to the next project, so I fully intend to set a release date target. Will we hit that target? Probably not; I’m not a great estimator, and life tends to throw plenty of curveballs that wreak havoc on plans. By setting an end goal, however, I expect that it will force us to more carefully plan what features we want to try and make for the next game.
In tandem with that, I want to try and establish something closer to a traditional game development pipeline (or, at least, what I understand of one), with multiple clearly-defined phases: prototyping, MVP, alpha, beta, and release. This will hopefully result in lots of experimentation up front that settles into a set of core mechanics, upon which we build lots of content that is rigorously tested prior to release.
Prototype Quickly Instead of Cleanly
Admittedly, the idea of not focusing on making my code clean rankles me a bit, as a developer, but it’s clear that development moves faster when I spend less time being picky about how my code is written. Plus, if I’m going to write something, find out it doesn’t work, and throw it away, I want to figure that out as quickly as possible so I can move on to trying the next idea.
Thus, during the prototyping phase of the next project, I’ll try to not put an emphasis on making the code clean. I won’t try to write messy code, of course, but I’m not going to spend hours figuring out the most ideal way to structure something. That can wait until the core mechanics have been settled on, having been playtested to confirm that said mechanics are fun.
Playtest Sooner Rather Than Later
The feedback I received from the final big playtesting session of Sanity Wars Reimagined was crucial in determining how to make the game more fun before release. For the next project, I don’t want to wait that long to find out what’s working, what’s not, and what I could add to make things even more fun.
I don’t think I’ll take it to public playtesting right away, but I’ll for sure reach out to friends and interested parties and ask them to try out prototype and MVP builds. It should hopefully be much easier to make suggested changes during those early stages, versus the week before release. With more frequent feedback, I can also iterate on things more often, and get the mechanics to be fun before locking them down and creating content for them.
Make a Linear Experience
After realizing how much work it would be to try and craft a good random experience, I’ve decided that I’m going to purposely make the next game a linear experience. In other words, each playthrough of the game won’t have randomness factoring into the gameplay experience. This may be a little more “boring”, but I think doing it this way will make it easier for me to not only practice making good game design, but make good code and good content for as well.
Will it be significantly less fun than something that introduces random elements to the design? Maybe, maybe not. We’ll find out after I attempt it!
Those are just a few of the things I intend to try on the next project. I don’t know if all of the ideas will prove useful in the long run, but they at least make sense to me in the moment. That’s good enough, for now. Whatever we get wrong, we can always iterate on!
Conclusion
That’s the story of Sanity Wars Reimagined. We started the project as an attempt to make a quick release to gain experience creating games, and despite taking significantly longer than planned, and the numerous mistakes made along the way, we still wound up releasing the game. Along the way, we developed numerous technologies, and learned lots of lessons, that should prove immensely useful for our next project. Because of that, despite the resulting game not being as fun as I wish it could’ve been, I consider Sanity Wars Reimagined a success.
What’s next for Rebecca and I? It’ll for sure be another platformer, as that will allow us to make good use of the technologies and processes we’ve already developed for making such games. I fully expect there will be new challenges and complications to tackle over the course of this next project, and I can’t wait to create solutions for them, and learn from whatever mistakes we make!
Implementing the Messenger Pattern in Godot
Note: If you implemented this pattern and are now experiencing issues with
get_tree()
calls, see my Issues with get_tree() section at the end of the article.
Oftentimes, in code, you need a way to have different parts of the codebase communicate with each other. One way to do this is have those components directly call methods from another component. While that works, it means you directly couple those components together. If you want to reuse one component in another project, you either have to take all the directly-coupled components with it or you have to refactor the direct couplings out of the component you want to reuse, neither of which is desirable from a clean code standpoint.
A way to solve this problem is to use the signal pattern. This is where each component can emit a named signal, and other components can then be connected to that signal. From that point on, whenever that signal is emitted by the component, anything that is listening for that signal can run code in response to that emission. It’s generally a great pattern, allowing for code to indicate when some event, or signal, happens, and for other parts of code to respond to that event accordingly (without code directly relying on calling methods from one another).
There is a third way to have decoupled components communicate to one another: the messenger pattern. At surface level, it’s very similar to the signal pattern: a part of your code dispatches a named message, and any code that is listening for that particular message can respond to it. Those different parts of your code aren’t connected to one another, however; instead, they interact through a Messenger node. Code that wants to listen for a message registers a message listener to the Messenger, and when another part of code dispatches a message with that name, the Messenger loops through all the registered listeners for that message name and invokes their callback functions.
Both the signal pattern and the messenger pattern can be considered subsets of the Observer pattern. The key difference is that the signal pattern has many objects connecting to one (the object emitting the signal), while the messenger pattern has a mediator object through which messages are dispatched and listened for by other objects. Which is better? It depends on what you are trying to accomplish architecturally, and there’s no reason you can’t use both.
Let’s discuss specifics, with relation to what Godot uses. Godot has the signal pattern baked into it at the core. Nodes can define signals through use of the signal
keyword. Any node that wants to listen for another node’s signal can connect()
to that node’s signal and associate a callback function to it. It looks like this, at a simplified level:
# OrdinaryNode
extends Node
signal some_cool_thing
# DifferentNode
extends Node
func _ready():
# Assuming both OrdinaryNode and DifferentNode are children of a hypothetical parent node.
get_parent().get_node('OrdinaryNode').connect('some_cool_thing', self, '_do_something_awesome')
func _do_something_awesome():
print("This is awesome!")
From then on, whenever OrdinaryNode
emits the some_cool_thing
signal, the _do_something_awesome()
function in DifferentNode
will run, printing “This is awesome!”
While this is a good implementation of signals, the nature of how the signal pattern works implies some shortcomings. For instance, all signals must be explicitly defined in code. You can’t have OrdinaryNode
, as written above, emit a coffee_break
signal because the code didn’t explicitly define that such a signal exists. This is by design, as it means you have to plan what your node can and can’t emit. Sometimes, though, you do want to have a more flexible way to communicate with other nodes, and at that point signals can’t help you. This is one thing the messenger pattern can help with, by not requiring you to explicitly define what messages can or can’t be sent.
Another aspect of the signal pattern is that it requires you to have nodes define a connection to the node emitting the signal if you want those nodes to react to the signal. That means those nodes must, by definition, couple themselves to the node emitting the signal (though the emitter node doesn’t know, or care, about those couplings). This isn’t necessarily bad, but it limits how you can architect your code; you have to make sure nodes that need to listen for a specific signal are able to connect to the node emitting said signal. Conversely, using the messenger pattern, you can have nodes connect only to a single Messenger node, which can be simpler to implement.
Godot does not natively implement such a messenger node, however. If we want to use this messenger pattern, we’ll need to make something ourselves. That’s what this tutorial will be about.
Note: What I’m calling the Messenger Pattern is more commonly known as the Mediator Pattern. I came up with the name Messenger before I learned what it is called, and I’ll continue to use it in this tutorial because I think it communicates more clearly what I’m using it for.
Setting Up
There is a sample project, if you want to refer to the finished product.
If you want to code alongside the tutorial, start by creating a new Godot project, then create a GDScript file named Messenger.gd
. We’ll make this as the base file that other implementations of messengers can extend to provide their own functionality.
The original project was created in Godot 3. Here is a branch that is configured for Godot 4. (Thanks to valVk for assisting the Godot 4 conversion!)
Adding and Removing Listeners
The first thing we want to do is provide a way to add and remove message listeners. Let’s begin with adding listeners.
var _message_listeners := {} # Stores nodes that are listening for messages.
# Add object as a listener for the specified message.
func add_listener(message_name: String, object: Object, method_name: String) -> void:
var listener = { 'object': object, 'object_id': object.get_instance_id(), 'method_name': method_name }
if _message_listeners.has(message_name) == false:
_message_listeners[message_name] = {}
_message_listeners[message_name][object.get_instance_id()] = listener
This is fairly straightforward. We take the name of the message, the object that has the callback function, and the name of the callback. We store all that in a listener
dictionary (defined as a class property outside of the function) and store it in _message_listeners
in the dictionary stored at the key matching the message name (creating a dictionary for that key if it doesn’t already exist). We key this listener in the message_name
dictionary to the object’s instance id, which is guaranteed to be unique.
Since Godot implements signals at the object level (Node inherits from Object), I’ll be typing these as Objects rather than Nodes, which allows for any node inheriting from Object to be used as a listener (including Resources).
Next, the ability to remove a registered listener.
# Remove object from listening for the specified message.
func remove_listener(message_name: String, object: Object) -> void:
if not _message_listeners.has(message_name):
return
if _message_listeners[message_name].has(object.get_instance_id()):
_message_listeners[message_name].erase(object.get_instance_id())
if _message_listeners[message_name].empty():
_message_listeners.erase(message_name)
Again, fairly straightforward. We run existence checks to see if a listener exists at that message_name
key, and erase it from the dictionary if so. Additionally, if no more listeners exist for that message_name
, we erase the dictionary for listeners of that message name.
Sending Messages
Now that we can add and remove message listeners, it’s time to add the ability to send those messages.
# Sends a message and triggers _callbacks on its listeners.
func dispatch_message(message_name: String, data := {}) -> void:
var message = { 'name': message_name, 'data': data }
_process_message_listeners(message)
We take a message_name
string and a data dictionary (which defaults to be an empty dictionary), store it to a message variable, and pass that variable into _process_message_listeners
.
# Invoke all listener callbacks for specified message.
func _process_message_listeners(message: Dictionary) -> void:
var message_name = message.name
# If there aren't any listeners for this message name, we can return early.
if not _message_listeners.has(message_name):
return
# Loop through all listeners of the message and invoke their callback.
var listeners = _message_listeners[message_name]
for listener in listeners.values():
# Invoke the callback.
listener.object.call(listener.method_name, message.data)
This is where we handle triggering the callbacks for a message listener. If there aren’t any listeners for that message name, we return early to avoid doing further processing. If there are listeners for that message name, then we loop through each one and trigger the stored method
callback, passing in the message’s data dictionary.
That’s it, as far as the basic implementation goes. But there are a couple of caveats that need to be dealt with.
Dealing with Nonexistent Listeners
One such case happens when a listener’s object is freed, making the stored reference in the listener dictionary invalid. If you try to operate on it, Godot will crash, so we need to provide a way to scan for dead listeners and remove them from storage.
Let’s start with a function to perform both the check and the purge:
# Removes a listener if it no longer exists, and returns whether the listener was removed.
func _purge_listener(listeners: Dictionary, listener: Dictionary) -> bool:
var object_exists = !!weakref(listener.node).get_ref() and is_instance_valid(listener.node)
if !object_exists or listener.node.get_instance_id() != listener.node_id:
listeners.erase(listener.node_id)
return true
return false
Multiple checks are used to see if the object exists (I’ve found in practice that I’ve needed both of these, not just one or the other). We also check to see if the instance id of the stored listener matches the id of the listener object we passed in; honestly, I can’t recall when or why that particular scenario occurs (I sadly forgot to write a comment about it in my code), but I know I’ve encountered it in the past, so I continue to include it as part of my check. If the object doesn’t exist, or the ids don’t match, we conclude the listener’s object no longer exists, and thus remove the listener from storage. Finally, we return a boolean value indicating whether the purge was performed or not.
Now we need to modify our existing code to use this function.
func _process_message_listeners(message: Dictionary) -> void:
# ...existing logic
for listener in listeners.values():
# If the listener has been freed, remove it
if _purge_listener(listeners, listener):
# Check if there are any remaining listeners, and erase the message_name from listeners if so.
if not _message_listeners.has(message_name):
_message_listeners.erase(message_name)
return
else:
continue
# ...existing logic
The difference is we call _purge_listener
before we try to invoke the callback. If the listener was purged, we perform an additional check to see if there are any other listeners of message_name
, and erase the dictionary keyed to message_name
if there aren’t; otherwise, we proceed to the next listener in the for loop.
That takes care of dead listeners. There’s one more problem we need to address.
Dispatching Messages Too Early
Right now, if we try to send and listen for messages during the ready
process (when Godot’s nodes all run their _ready
callbacks), then we’ll likely run into issues where messages are dispatched before the listeners of those messages are registered (because their ready callbacks run later than when the messages are sent). To solve this, we’re going to add a message queue. If a message is being dispatched before the root node of the scene tree is ready, we’ll add the message onto this queue, and once the root node emits its ready
signal we’ll process all the messages in the queue.
Let’s start with setting up the message queue, and modifying our dispatch_message
function.
var _message_queue := [] # Stores messages that are being deferred until the next physics process tick.
var _messenger_ready := false # Is set to true once the root node is ready, indicating the messenger is ready to process messages.
# Sends a message and triggers _callbacks on its listeners.
func dispatch_message(message_name: String, data := {}) -> void:
var message = { 'name': message_name, 'data': data }
if _messenger_ready:
_process_message_listeners(message)
else:
_message_queue.push_back(message)
We’ve added two new class properties, one to house the message queue and the other to mark when the messenger node considers itself ready. dispatch_message
has been modified to first check _messenger_ready
, and if so it runs the code the same as before. If the messenger node is not ready, then the message is pushed onto the message queue.
Now let’s set up the ability to process the message queue.
func _ready() -> void:
get_tree().get_root().connect('ready', self, '_on_Root_ready')
# Is called when the root node of the main scene tree emits the ready signal.
func _on_Root_ready() -> void:
_messenger_ready = true
_process_message_queue()
# Process all messages in the message queue and reset the queue to an empty array.
func _process_message_queue() -> void:
for message in _message_queue:
_process_message_listeners(message)
_message_queue = []
In Messenger’s own _ready
callback, we register a listener to the scene tree root’s ready
signal. The callback then sets _messenger_ready
to true and calls a function, _process_message_queue()
, which loops through each message in the queue and calls _process_message_listeners()
on them. At the send, we clear the message queue, since we don’t need (or want) to process these messages again.
Creating a GlobalMessenger
At this point, we have a base Messenger class that can be used anytime we want to implement the messenger pattern in our code. Let’s demonstrate this by creating a global singleton, GlobalMessenger
, that we can interact with from anywhere in our codebase.
Start by creating a new file, global_messenger.gd
, and have it extend our Messenger
class. If Godot claims the Messenger
class doesn’t exist, then you’ll need to reload the project to force Godot to update itself and recognize the Messenger
class we added in Messenger.gd
.
# Creates a global messenger that can be accessed from anywhere in the program.
extends Messenger
The reason I made this file name snake_case is because my personal convention is to name files that are solely used as singletons with this format, to distinguish them from files containing extensible classes. This is my personal preference only, and is not required to make this code work.
That’s all that needs to be done from a code standpoint. To make this a globally-available singleton, we need to go to Project -> Settings
in the editor menu, navigate to the AutoLoad
tab, and add global_messenger.gd
to the list of autoloaded files.
And…that’s it! We now have a global singleton that we can use from anywhere in our codebase to dispatch messages!
Deferring Messages
Let’s add some additional functionality to our global messenger. For instance, right now, once the messenger is ready, we immediately run listener callbacks upon receipt of the message. What if we wanted to defer message dispatches until the next process tick? It might prove useful to ensure all game data is updated by the time your message callbacks are being run.
We already have a message queue that is used to make sure messages are deferred until the messenger is ready. We can build on that to add functionality to intentionally defer message dispatching until the next physics process tick.
func _ready() -> void:
set_physics_process(false)
func _physics_process(_delta) -> void:
._process_message_queue()
set_physics_process(false) # We don't need to keep updating once messages are processed.
# Queues a message to be dispatched on the next physics processing tick.
func dispatch_message_deferred(message_name: String, data := {}) -> void:
_message_queue.push_back({ 'name': message_name, 'data': data })
set_physics_process(true)
First, we use _ready()
to disable physics processing. That’s because, whenever _physics_process()
is defined in a script file, Godot automatically enables processing. We only want to process when there are messages in queue, so we just disable physics processing right off the bat.
I use
_physics_process
instead of_process
to ensure messages are processed at a consistent rate.physics_process
is run a consistent amount of times per second, whereas_process
is run as often as possible, and I’ve found that having messages processed as fast as possible can result in unexpected complexity when sent from code that is expecting a consistent frame rate.
Next, in the _physics_process()
callback, we call _process_message_queue()
, then disable physics processing again (basically, only running the update step a single time).
Finally, we create a new function, dispatch_message_deferred
, making it obvious that calling this will be different from a regular message dispatch. We add the message straight onto the message queue. Afterwards, we set the physics processing step to be true
. This way, the next time _physics_process()
callbacks are run in the code, the global messenger’s _physics_process()
callback will be run, too. And since it is a global singleton, it will be run before other nodes in the root scene.
That’s it!
Testing our Implementation
Now that we have a Messenger node, and a GlobalMessenger implementation of it, let’s set up a test scene in our project to test their functionality and make sure they work as intended.
Create a new scene, TestScene
, then structure it thusly:
LocalMessenger
is a node which is extended from Messenger
; we will use this to test that a locally-built implementation of our messenger node works.
The other two nodes, OrdinaryNode
and DifferentNode
, should contain the following code:
# OrdinaryNode
extends Node
onready var localMessenger = $"../LocalMessenger"
func _ready() -> void:
GlobalMessenger.dispatch_message('test_1', { 'fish': 'shark' })
localMessenger.add_listener('test_local', self, '_on_Test_local')
func _on_Test_local(data) -> void:
print('Do you like looking at the ', data.animal, '?')
# DifferentNode
extends Node
onready var localMessenger = $"../LocalMessenger"
func _ready() -> void:
GlobalMessenger.add_listener('test_1', self, '_on_Test_1')
localMessenger.dispatch_message('test_local', { 'animal': 'rabbit' })
func _on_Test_1(_data) -> void:
print('Test 1 received')
At this point, if you run the scene, you should see the two messages printed to console. If you do, then everything was set up correctly!
Issues with get_tree()
Recently (in May 2023), I encountered a strange bug where a message callback that invoked get_tree()
was not returning the scene tree, despite the node housing the callback function being in the scene tree. After some investigation, I realized that I was calling the add_listener()
function from the node’s _ready()
callback; when I switched to adding the listener in _enter_tree()
and removing it in _exit_tree()
the get_tree()
call worked as expected.
I admittedly am not entirely sure why this works, but my theory is that adding the listener during _ready()
is either storing the reference to the function call when the tree is not yet defined or subsequent tree exits and enters is causing the reference to be lost. In any case, I wanted to add this addendum in case anyone else chose to implement this pattern and ran into the same problem.
If you happen to know more info about why this might have happened, please let me know!
Conclusion
We now have a base Messenger
node, as well as a GlobalMessenger
singleton that extends it and adds defer functionality to it. When should it be used? Personally, I use the messenger pattern in cases where I want to enable node communication, but for whatever reason it doesn’t benefit me to define the specific signals ahead of time, which is when the messenger’s dynamism comes into play.
Of course, that dynamism leads to the risk of making messy code. One advantage to explicitly forcing signals to be defined is that it forces you to think about how you are architecting your code, by making you think clearly about how your signals are going to be used. Since Messenger
lets any node send whatever message it wants, it falls on you to make sure that power isn’t abused to send messages when the situation doesn’t call for it. For instance, if you have one node which you want other nearby nodes to listen for a specific event from, you don’t need the dynamic nature of Messenger
; signals work perfectly fine, and are a cleaner way to get the job done.
As with all things, in life and code, consider carefully how you do things, and use whatever tools and patterns best fit your needs.
Creating a Debugging Interface in Godot (Part 3)
Welcome to Part 3 of my tutorial for creating a debugging interface in Godot! In Part 1, we created the base for our debugging system, and in Part 2 we created debug widgets to show our debugging information. If you haven’t read those parts, you would be advised to do so before continuing on with this part. Alternatively, if you want to start from this part, and just need the end code from the preceding parts, you can check out the tutorial-part-2 branch from the Github repo.
At this point, we have a debugging interface that we can toggle on and off, and we have a base DebugWidget
class to build our debug widget from, as well as a DebugTextList
debug widget. We don’t quite have everything we’d ideally want in a debugging system, though. What happens if we want to display different debug widgets, and not have to see all of them at the same time? What if we have a lot of debug widgets, so much so that they take up most of the screen space, making it impossible to see the underlying game beneath the cluttered visuals?
We could try creating multiple DebugLayer
nodes, but this would quickly become brittle and clunky. As the DebugLayer
is exposed globally for our code to access, any additional DebugLayer
nodes would also need to be global, which would pollute the AutoLoad declarations. It would also mean having to remember which DebugLayer
you’re connecting to, as well as assigning different keys to show and hide each layer so that they don’t all show at the same time… Suffice it to say, doing things this way is awful.
It would be better to create a system specifically for showing different debugging interfaces, depending on whatever criteria we choose to specify. We’ll do this by creating a new type of node, the DebugContainer
, and modifying DebugLayer
to be capable of managing multiple DebugContainer
nodes.
If you want to see the final result, you can check out the tutorial-part-3 branch in the Github repo.
Ready? Let’s go!
Creating the DebugContainer
Begin by creating a new script file, DebugContainer.gd
, in the _debug
directory. Have it extend MarginContainer
. We’ll begin by adding this line of code:
# The list of widget keywords associated with the DebugContainer.
var _widget_keywords = {}
Wait a minute, you say. That looks suspiciously like the code we added to DebugLayer.gd
in the previous part of this tutorial. Well, you’re right! That’s exactly what it is. Our goal is to move management of individual DebugWidget
nodes out of DebugLayer
and into DebugContainer
nodes, so it makes sense to go ahead and store the widget keywords here.
Moving Widget Code from DebugLayer to DebugContainer
In fact, we’re going to move most of the code we added to DebugLayer
for managing debug widgets into DebugContainer.gd
. Let’s take care of that right now:
func _ready():
mouse_filter = MOUSE_FILTER_IGNORE
_register_debug_widgets(self)
Debug.register_debug_container(self)
# Adds a widget keyword to the registry.
func _add_widget_keyword(widget_keyword: String, widget_node: Node) -> void:
var widget_node_name = widget_node.name if 'name' in widget_node else str(widget_node)
if not _widget_keywords.has(widget_node_name):
_widget_keywords[widget_node_name] = {}
if not _widget_keywords[widget_node_name].has(widget_keyword):
_widget_keywords[widget_node_name][widget_keyword] = widget_node
else:
var widget = _widget_keywords[widget_node_name][widget_keyword]
var widget_name = widget.name if 'name' in widget else str(widget)
push_error('DebugContainer._add_widget_keyword(): Widget keyword "' + widget_node_name + '.' + widget_keyword + '" already exists (' + widget_name + ')')
return
# Go through all children of provided node and register any DebugWidgets found.
func _register_debug_widgets(node) -> void:
for child in node.get_children():
if child is DebugWidget:
register_debug_widget(child)
elif child.get_child_count() > 0:
_register_debug_widgets(child)
# Register a single DebugWidget to the DebugContainer.
func register_debug_widget(widgetNode) -> void:
for widget_keyword in widgetNode.get_widget_keywords():
_add_widget_keyword(widget_keyword, widgetNode)
# Sends data to the widget with widget_name, triggering the callback for widget_keyword.
func update_widget(widget_path: String, data) -> void:
var split_widget_path = widget_path.split('.')
if split_widget_path.size() == 1 or split_widget_path.size() > 2:
push_error('DebugContainer.update_widget(): widget_path formatted incorrectly. ("' + widget_path + '")')
var widget_name = split_widget_path[0]
var widget_keyword = split_widget_path[1]
if _widget_keywords.has(widget_name) and _widget_keywords[widget_name].has(widget_keyword):
_widget_keywords[widget_name][widget_keyword].handle_callback(widget_keyword, data)
else:
push_error('DebugContainer.update_widget(): Widget name and keyword "' + widget_name + '.' + widget_keyword + '" not found (' + str(_widget_keywords) + ')')
Almost all of the code above is code we worked on in Part 2 of this tutorial. If you need any refreshers on how that code works, feel free to review that part.
There are a couple of differences to the code that need to be pointed out; both are in the _ready()
function. First, the mouse_filter = MOUSE_FILTER_IGNORE
line.
By default, mouse_filter
is equal to MOUSE_FILTER_PASS
. That value means that, when you render a UI node, mouse interactions are captured by the first UI element that decides to handle it. If you have two UI nodes, and you click on that stack, the “top” node will receive the mouse event first. If it doesn’t handle the event, it gets passed to any nodes below it. If it does do something with the event, however, then the event is considered to be handled, and is no longer passed on to other nodes.
With that information, let’s think about how our debugging system is implemented. We made DebugLayer
a CanvasLayer
node that is rendered at the highest level possible. Because of this, anything in DebugLayer
will receive mouse events before anything else in the game. Since control nodes default to using the MOUSE_FILTER_PASS
setting, that means DebugLayer
will consume any mouse events while it is being shown, preventing interaction with the underlying game. That is behavior we definitely don’t want. That is why we set mouse_filter
to MOUSE_FILTER_IGNORE
for DebugContainer
, so that it will ignore any mouse events, allowing them to proceed down to the underlying game nodes.
The other thing to note about the code we’re adding is the call to Debug.register_debug_container()
. This will be how our debug container registers itself with the DebugLayer
, much like what we did with debug widgets in the previous part of the tutorial.
If you’re copying code over from your project, don’t forget to update the error messaging and code documentation to say
DebugContainer
instead ofDebugLayer
.
Modifying DebugLayer to use DebugContainers
We’re going to need to add register_debug_container()
to DebugLayer.gd
. Before we do so, however, we need to make some other changes to the DebugLayer
scene, itself:
- Remove the
TextList1
node we created in the previous tutorial; we’re no longer going to store debug widgets directly in theDebugLayer
scene. - Select the
DebugUIContainer
node, click on the Layout tab, and select “Full Screen”. - Add a
VBoxContainer
child toDebugUIContainer
. - Add a
Tabs
node and aMarginContainer
node as children of theVBoxContainer
(in that order). - Name those last two nodes
DebugTabs
andDebugContentContainer
. - Go to the
DebugTabs
node properties and set Tab Alignment toleft
.
That takes care of the scene. Let’s move on to modifying the script. If you haven’t done so already, remove the code implementing debug widgets in DebugLayer
(aka the stuff we moved into DebugContainer
). Once that’s done, add the register_debug_container()
function and the related code that is part of its implementation:
signal debug_container_registered
# The debug containers registered to the DebugLayer.
var _debug_containers = {}
# The currently active debug container.
var _debugContainer: Node
# Nodes implementing the debug container tab switching interface.
onready var debugTabs = $DebugUIContainer/VBoxContainer/DebugTabs
onready var debugContentContainer = $DebugUIContainer/VBoxContainer/DebugContentContainer
func _input(_event) -> void:
if Input.is_action_just_pressed('toggle_debug_interface'):
# ...existing code
_debugContainer.show()
func register_debug_container(containerNode) -> void:
var container_name = containerNode.name
if _debug_containers.has(container_name):
push_error('DebugLayer.register_debug_container: Debug already has registered DebugContainer with name "' + container_name + '".')
return
# Reparent the container node to the DebugLayer.
containerNode.get_parent().call_deferred('remove_child', containerNode)
debugContentContainer.call_deferred('add_child', containerNode)
debugTabs.add_tab(container_name)
_debug_containers[container_name] = containerNode
if _debug_containers.size() == 1:
_debugContainer = containerNode
# Hide this container node so we don't show debug info by default.
containerNode.hide()
emit_signal('debug_container_registered', containerNode)
That’s quite a chunk of code. Let’s unpack this and see what everything does.
First, we add a signal, debug_container_registered
, which we’ll dispatch whenever a debug container is registered. Next, we add _debug_containers
, which will be used the same way that we used _debug_widgets
, just for debug containers instead of debug widgets. We also add _debugContainer
to keep track of the currently shown debug container’s node.
We define references for two of the UI nodes we added to the DebugLayer
scene, debugTabs
and debugContentContainer
. For now, we’ll ignore these in favor of explaining other parts of the added code. Don’t worry, we’ll explain what these nodes are used for as we progress through the tutorial.
Continuing on, we modify our _input()
function to show the current debug container node whenever we toggle on the debug interface. And finally, at long last, we have the register_debug_container()
function, itself.
In register_debug_container()
, we first get the name of the passed-in containerNode
and check to see if that name is already registered; if it is, we show an error and return without doing anything else. Next, we need to reparent the containerNode
from wherever it currently is in the scene tree to become a child of debugContentContainer
. Note the use of call_deferred()
, rather than invoking the functions directly; this calls the specified functions during Godot’s idle time, which prevents issues that can occur when running code within nodes that are being reparented.
We’re going to allow
DebugContainer
nodes to be added pretty much wherever we want when creating our scenes, so we need to move them inside theDebugLayer
at runtime to ensure they get displayed as part of the debugging interface. This should make more sense once we get to the part where we start using debug containers.
After the reparenting is finished, we add a new tab to the DebugTabs
node, entitled the debug container’s name. Then we add the containerNode
to the dictionary of debug containers; if it’s the first debug container we’ve registered, we set it to be the initially-shown debug container. We want to make sure that our debug containers aren’t visible by default (otherwise, we’ll see every debug container all at once), so we call hide()
on the containerNode
. Finally, we emit the debug_container_registered
signal, so anything that wants to listen for that will know when a debug container is registered, and which one it is.
I have not needed to make use of this signal yet in my personal use of the debugging system, but it seems like a potentially useful thing to expose, so it makes sense to go ahead and do so.
Now that we’ve implemented the register_debug_container()
function, it’s time to take a closer look at the DebugTabs
node and make it work.
DebugTabs
The Tabs
node in Godot is a basic tabs implementation. It does no view switching by itself; instead, when we switch between tabs, a signal is fired indicating which tab was switched to, and it’s up to our code to listen for that signal and respond to it. We’re going to use this functionality to change which debug container is the one being shown in DebugLayer
.
Godot does provide a
TabsContainer
node, which would implement both the tabs and view switching. However, since it is a single node, if you ignore mouse events (as we mentioned needing to add forDebugContainer
), then you can’t click on the tabs. If you leave the node able to capture mouse events, it will prevent interacting with the game when the debug interface is open. Thus, I’ve opted to just use theTabs
node and implement view switching manually.
The code to implement the view switching is rather simple:
func _ready() -> void:
# ...existing code
debugTabs.connect('tab_changed', self, '_on_Tab_changed')
func _on_Tab_changed(tab_index) -> void:
var tab_name = debugTabs.get_tab_title(tab_index)
var containerNode = _debug_containers[tab_name]
_debugContainer.hide()
_debugContainer = containerNode
_debugContainer.show()
During _ready()
, we connect to the tab_changed
signal for debugTabs
and provide an _on_Tab_changed()
callback. In the callback, we get the name of the tab (based on the tab_index
provided as the callback function’s argument), and use that name to find the registered debug container with matching name. We then hide the currently-visible debug container, switch the _debugContainer
variable to be the upcoming containerNode
, and then make that debug container visible.
Updating Widgets
We’re still missing one important functionality: sending data to update our debug widgets. Since we moved our previous implementation of update_widget()
into the DebugContainer
node, we’ll need to create a new version of update_widget()
that determines which debug container the widget data should be sent to.
# Sends data to the debug container specified in widget_path.
# API: container_name:widget_name.widget_keyword
func update_widget(widget_path: String, data = null) -> void:
var split_keyword = widget_path.split(':')
if split_keyword.size() == 1:
push_error('DebugLayer.update_widget(): No container name was specified. (' + widget_path + ', ' + str(data) + ')')
return
var container_name = split_keyword[0]
if not _debug_containers.has(container_name):
push_error('DebugLayer.update_widget(): Container with name "' + container_name + '" is not registered.')
return
var containerNode = _debug_containers[container_name]
widget_path = split_keyword[1]
containerNode.update_widget(widget_path, data)
Notice that the arguments are still the same: we’re passing in a widget_path
and data
. However, we need a way to indicate which debug container has the debug widget we want to update.
To do this, we’re going to modify the widget_path
API slightly. Instead of starting the string with the name of the debug widget, we’ll start with the name of the debug container, and delimit it with a colon, :
.
We implement this in code by splitting the widget_path
string on said colon and verifying that there was indeed a debug container name passed in. If no container name was provided, then we show an error and return without doing anything further; we do the same if the provided debug container’s name doesn’t exist in our dictionary of registered debug containers. If all is valid, then we get the specified debug container and call its update_widget()
function, passing in the other half of our split string (aka the original widget_name.widget_keyword
API), as well as data
.
At this point, we’re almost ready to run the test scene to try our changes, but there’s something we need to do first: modify our test scene to support the changes we’ve made to our Debug
API.
Adding a DebugContainer to the Test Scene
Let’s go straight to our TestScene
scene and add one of our new DebugContainer
nodes; name it “TestDebugContainer”. As a child of that, add a DebugTextList
debug widget with the name “TextList1”. Finally, go to TestScene.gd
and change our call to Debug.update_widget()
to incorporate our new syntax for specifying the debug container in the widget_path
.
func _process(_delta) -> void:
# ...existing code
elif test_ct == 900:
Debug.update_widget('TestDebugContainer:TextList1.remove_label', { 'name': 'counter' })
elif test_ct < 900:
Debug.update_widget('TestDebugContainer:TextList1.add_label', { 'name': 'counter', 'value': str(test_ct) })
Now we can run the test scene and see our changes in action! If you press the debug toggle key combination we defined earlier (Shift + `
), you should be able to see the same counting text that we saw before. Additionally, you should be able to see the tab we just added, titled "TestDebugContainer".
If that's what you see, good job! If not, review the tutorial (and perhaps the repo code) to try and identify where things went wrong.
Testing with Multiple Debug Containers
That said, these are things we've seen before (aside from the tab). We made these changes to support being able to show different debugging views via multiple debug containers. Let's go ahead and add another one!
Duplicate the TestDebugContainer
node (which will create a copy of both that node and the child debug widget; the TestDebugContainer
node will be automatically named "TestDebugContainer2"), then go to TestScene.gd
and add two new calls to Debug.update_widget()
as shown below:
# ...existing code
elif test_ct == 900:
Debug.update_widget('TestDebugContainer:TextList1.remove_label', { 'name': 'counter' })
Debug.update_widget('TestDebugContainer2:TextList1.remove_label', { 'name': 'counter' })
elif test_ct < 900:
Debug.update_widget('TestDebugContainer:TextList1.add_label', { 'name': 'counter', 'value': str(test_ct) })
Debug.update_widget('TestDebugContainer2:TextList1.add_label', { 'name': 'counter', 'value': str(round(test_ct / 10)) })
As you can see, we're simply changing the widget_path
to request TestDebugContainer2
instead of TestDebugContainer
. To keep the test simple, our second call is showing the same test_ct
variable, but divided by ten and rounded to the nearest integer.
That's it! No, seriously! Go ahead and run the scene again, and everything should "just work". You'll see two tabs, one named "TestDebugContainer" and the other named "TestDebugContainer2". Switching between them will alternate between showing the original counter and the rounded version.
But wait, there's more! We can add these debug containers anywhere in our scene structure, and as long as those scenes are part of the currently-running scene tree they'll register themselves to our debugging interface.
To test this, let's create a quick child scene to add to our test scene. Create a new scene called "TestChild" (based on Node
), then add a button with text "Test Button" and place it near the top-center of the child scene. Add a DebugContainer
with DebugTextList
child to TestChild
, and make sure you rename them to "TestDebugContainer2" and "TextList1" (to match the widget_path
we've defined in the TestScene.gd
script). Instance TestChild
into TestScene
and remove the TestDebugContainer2
node that was in TestScene
.
Run the test scene, and you get exactly the same result as before. You can see both the tabs, and switch between them with ease. The only difference is that one debug container originated in TestScene
, and the other in TestChild
.
If you see the
TestDebugContainer2
tab, but not the counter, that means you forgot to make the debug node names and thewidget_key
string match, so you're not actually sending updates to the correct location.
Fixing One Last Bug
Before we get too hyped with excitement, however, there is a bug that we need to take care of. Run the test scene, open the debugging interface, and hover over the button we added to the TestChild
scene. Nothing seems to happen, right? Now close the debugging interface and hover over the button again. This time, it lights up, indicating that it's receiving mouse events. That means something in our debugging interface is intercepting mouse events.
Fortunately, this is a simple fix: we just need to go to the DebugLayer
scene and change the mouse_filter
properties for DebugUIContainer
, VBoxContainer
, and DebugContentContainer
to MOUSE_FILTER_IGNORE
(shown as just Ignore
in the editor interface). Do not, however, change the mouse_filter
property for DebugTabs
, or you may find yourself unable to click on the tabs at all!
Once you've made those changes, run the test scene again. This time, you should be able to trigger the button hover state when the debug interface is open.
Congratulations!
We now have DebugContainer
nodes, which we can add wherever we want, and add whatever debug widgets we want to them, using the tabbed interface to switch between whichever debugging views we want to see. And best of all, it's simple to add these debug containers and widget as we need them, whether for temporarily reporting data or for permanent display of debugging information.
With these things, you have the essentials needed to build on this debugging system and make it your own. Create widgets that show you the information that you need to know. As shown in this tutorial, it's easy to make a new debug widget, and just as easy to register it to the debugging system. Using this system has definitely made my game development much easier, and I hope the same will be true for you!
If you want to see the code as it was at the end of this part, check out the tutorial-part-3 branch in the Github repo.
Creating a Debugging Interface in Godot (Part 2)
Welcome to Part 2 of my tutorial for creating a debugging interface in Godot! In Part 1, we created the base for our debugging system. If you haven’t read that part, you should do so now, because the rest of the tutorial series will be building atop it. Alternatively, if you just want the code from the end of Part 1, you can check out the tutorial-part-1 branch in the Github repo.
At this point, we have the base of a debugging system, but that’s all it is: a base. We need to add things to it that will render the debugging information we want to show, as well as an API to DebugLayer
that is responsible for communicating this information.
We’ll do this through “debug widgets”. What’s a debug widget? It’s a self-contained node that accepts a set of data, then displays it in a way specific to that individual widget. We’ll make a base DebugWidget
node, to provide common functionalities, then make other debug widgets extend that base that implement their custom functionalities on top of the base node.
Alright, enough high-level architecture talk. Let’s dive in and make these changes!
Creating the Base DebugWidget
To get started, we want a place to store our debug widgets. To that end, make a new directory in _debug
, called widgets
. In this new widgets
directory, create a new script called DebugWidget.gd
, extending MarginContainer
.
# Base class for nodes that are meant to be used with the DebugLayer system.
class_name DebugWidget
extends MarginContainer
Note the custom class_name
. This is important, because later on we’ll be using it to check whether a given node is a debug widget.
You may need to reload your Godot project to ensure that the custom
class_name
gets registered.
Next, we’re going to add something called “widget keywords”:
# Abstract method which must be overridden by the inheriting debug widget.
# Returns the list of widget keywords. Responses to multiple keywords should be provided in _callback.
func get_widget_keywords() -> Array:
push_error("DebugWidget.get_widget_keywords(): No widget keywords have been defined. Did you override the base DebugWidget.get_widget_keywords() method?")
return []
This function will be responsible for returning a debug widget’s widget keywords. What are widget keywords, though?
To give a brief explanation, widget keywords are the way we’re going to expose what functionalities this debug widget provides to the debugging system. When we want to send data to a widget, the debugging system will search through a list of stored widget keywords, and if it finds one matching the one we supply in the data-sending function, it will run a callback associated with that widget keyword.
If that doesn’t make much sense right now, don’t worry. As you implement the rest of the flow, it should become clearer what widget keywords do.
One thing to note about the code is that we’re requiring inheriting classes to override the method. This is essentially an implementation of the interface pattern (since GDScript doesn’t provide an official way to do interfaces).
Let’s add a couple more functions to DebugWidget.gd
:
# Abstract method which must be overridden by the inheriting debug widget.
# Handles the widget's response when one of its keywords has been invoked.
func _callback(widget_keyword, data) -> void:
push_error('DebugWidget._callback(): No callback has been defined. (' + widget_keyword + ', ' + data + ')')
# Called by DebugContainer when one of its widget keywords has been invoked.
func handle_callback(widget_keyword: String, data) -> void:
_callback(widget_keyword, data)
handle_callback()
is responsible for calling the _callback()
function. Right now, that’s all it does. We’ll eventually also do some pre-callback validation in this function, but we won’t get into that just yet.
_callback()
is another method that we explicitly want the inheriting class to extend. Essentially, this is what will be run whenever something uses one of the debug widget’s keywords. Nothing is happening there right now; all the action is going to be in the inheriting debug widgets.
That’s it for the base DebugWidget
. Time to extend that base!
Creating the DebugTextList DebugWidget
Remember that DebugLabel
that was discussed at the beginning of the article? Having a text label that you can update as needed is a useful thing for a debugging system to have. Why stop with a single label, though? Why not create a debug widget that is a list of labels, which you can update with multiple bits of data?
That’s the debug widget we’re going to create. I call it the DebugTextList
.
I prefix debug widget nodes with
Debug
, to indicate that they are only meant to be used for debugging purposes. It also makes it easy to find them when searching for scenes to instance.
Create a directory in widgets
called TextList
, then create a DebugTextList
scene (not script). If you’ve registered the DebugWidget
class, you can extend the scene from that; otherwise, this is the point where you’ll need to reload the project in order to get access to that custom class.
Why create it as a scene, and not as another custom node? Really, it’s simply so that we can create the node tree for our debug widget using the editor’s graphical interface, making it simpler to understand. It’s possible to add the same child nodes through a script, and thereby make it possible to make the DebugTextList
a custom node. For this tutorial, however, I’m going to keep using the scene-based way, for simplicity.
Alright, let’s get back on with the tutorial.
Add a VBoxContainer
child node to the DebugTextList
root node. Afterwards, attach a new script to the DebugTextList
scene, naming it DebugTextList.gd
, and have it extend DebugWidget
. Replace the default script text with the following code:
const WIDGET_KEYWORDS = {
'ADD_LABEL': 'add_label',
'REMOVE_LABEL': 'remove_label'
}
onready var listNode = $VBoxContainer
listNode
is a reference to the VBoxContainer
. We also have defined a const, WIDGET_KEYWORDS
, which will define the widget keywords this debug widget supports. Technically, you could just use the keyword’s strings where needed, rather than define a const, but using the const is easier, as you can see below.
# Handles the widget's response when one of its keywords has been invoked.
func _callback(widget_keyword: String, data) -> void:
match widget_keyword:
WIDGET_KEYWORDS.ADD_LABEL:
add_label(data.name, str(data.value))
WIDGET_KEYWORDS.REMOVE_LABEL:
remove_label(data.name)
_:
push_error('DebugTextList._callback(): widget_keyword not found. (' + widget_keyword + '", "' + name + '", "' + str(WIDGET_KEYWORDS) + '")')
# Returns the list of widget keywords.
func get_widget_keywords() -> Array:
return [
WIDGET_KEYWORDS.ADD_LABEL,
WIDGET_KEYWORDS.REMOVE_LABEL
]
Notice that we’re overriding both _callback()
and get_widget_keywords()
. The latter returns the two widget keywords we defined in the const, while the former performs a match check against the widget_keyword
argument to see if it matches one of our two defined keywords, running a corresponding function if so. By using the const to define our widget keywords, we’ve made it easier to ensure that the same values get used in all the places needed in our code.
match
is Godot’s version of implementing theswitch/case
pattern used in other languages (well, it’s slightly different, but most of the time you can treat it as such). You can read more about it here. The underscore in the match declaration represents the default case, or what happens ifwidget_keyword
doesn’t match our widget keywords.
Let’s go ahead and add the two response functions now: add_label()
and remove_label()
. We’ll also add a helper function that is used by both, _find_child_by_name()
.
# Returns a child node named child_name, or null if no child by that name is found.
func _find_child_by_name(child_name: String) -> Node:
for child in listNode.get_children():
if 'name' in child and child.name == child_name:
return child
return null
# Adds a label to the list, or updates label text if label_name matches an existing label's name.
func add_label(label_name: String, text_content: String) -> void:
var existingLabel = _find_child_by_name(label_name)
if existingLabel:
existingLabel.text = text_content
return
var labelNode = Label.new()
labelNode.name = label_name
labelNode.text = text_content
listNode.add_child(labelNode)
func remove_label(label_name) -> void:
var labelNode = _find_child_by_name(label_name)
if labelNode:
listNode.remove_child(labelNode)
_find_child_by_name()
takes a given child_name
, loops through the children of listNode
to see if any share that name, and returns that child if so. Otherwise, it returns null
.
add_label()
uses that function to see if a label with that name already exists. If the label exists, then it is updated with text_content
. If it doesn’t exist, then a new label is created, given the name label_name
and text text_content
, and added as a child of listNode
.
remove_label()
looks for an existing child label, and removes it if found.
With this code, we now have a brand-new debug widget to use for our debugging purposes. It’s not quite ready for use to use, yet. We’re going to have to make changes to DebugLayer
in order to make use of these debug widgets.
Modifying DebugLayer
Back in Part 1 of this tutorial, we made the DebugLayer
scene a global AutoLoad, to make it accessible from any part of our code. Now, we need to add an API to allow game code to send information through DebugLayer
to the debug widgets it contains.
Let’s start by adding a dictionary for keywords that DebugLayer
will be responsible for keeping track of.
# The list of widget keywords associated with the DebugLayer.
var _widget_keywords = {}
Next, we’ll add in the ability to “register” debug widgets to the DebugLayer
.
func _ready():
# ...existing code
_register_debug_widgets(self)
# Go through all children of provided node and register any DebugWidgets found.
func _register_debug_widgets(node) -> void:
for child in node.get_children():
if child is DebugWidget:
register_debug_widget(child)
elif child.get_child_count() > 0:
_register_debug_widgets(child)
# Register a single DebugWidget to the DebugLayer.
func register_debug_widget(widgetNode) -> void:
for widget_keyword in widgetNode.get_widget_keywords():
_add_widget_keyword(widget_keyword, widgetNode)
In our _ready()
function, we’ll call _register_debug_widgets()
on the DebugLayer
root node. _register_debug_widgets()
loops through the children of the passed-in node
(which, during the ready function execution, is DebugLayer
). If any children with the DebugWidget
class are found, it’ll call register_debug_widget()
to register it. Otherwise, if that child has children, then _register_debug_widgets()
is called on that child, so that ultimately all the nodes in DebugLayer
will be processed to ensure all debug widgets are found.
register_debug_widget()
, meanwhile, is responsible for looping through the debug widget’s keywords (acquired from calling get_widget_keywords()
) and adding them to the keywords dictionary via _add_widget_keyword()
. Note that this function I chose to not mark as “private” (by leaving off the underscore prefix). There may be reason to allow external code to register a debug widget manually. Though I personally haven’t encountered this scenario yet, the possibility seems plausible enough that I decided to not indicate the function as private.
Let’s add the _add_widget_keyword()
function now:
# Adds a widget keyword to the registry.
func _add_widget_keyword(widget_keyword: String, widget_node: Node) -> void:
var widget_node_name = widget_node.name if 'name' in widget_node else str(widget_node)
if not _widget_keywords.has(widget_node_name):
_widget_keywords[widget_node_name] = {}
if not _widget_keywords[widget_node_name].has(widget_keyword):
_widget_keywords[widget_node_name][widget_keyword] = widget_node
else:
var widget = _widget_keywords[widget_node_name][widget_keyword]
var widget_name = widget.name if 'name' in widget else str(widget)
push_error('DebugLayer._add_widget_keyword(): Widget keyword "' + widget_node_name + '.' + widget_keyword + '" already exists (' + widget_name + ')')
return
That looks like a lot of code, but if you examine it closely, you’ll see that most of that code is just validating that the widget data we’re working with was set up correctly. First, we get the name of widget_node
(aka the name as entered in the Godot editor). If that node’s name isn’t already a key in our _widget_keywords
dictionary, we add it. Next, we check to see if the widget_keyword
already exists in the dictionary. If it doesn’t, then we add it, setting the value equal to the widget node. If it does exist, we push an error to Godot’s error console (after some string construction to make a developer-friendly message).
Interacting with Debug Widgets
At this point, we can register debug widgets so that our debugging system is aware of them, but we still don’t have a means of communicating with the debug widgets. Let’s take care of that now.
# Sends data to the widget with widget_name, triggering the callback for widget_keyword.
func update_widget(widget_path: String, data) -> void:
var split_widget_path = widget_path.split('.')
if split_widget_path.size() == 1 or split_widget_path.size() > 2:
push_error('DebugContainer.update_widget(): widget_path formatted incorrectly. ("' + widget_path + '")')
var widget_name = split_widget_path[0]
var widget_keyword = split_widget_path[1]
if _widget_keywords.has(widget_name) and _widget_keywords[widget_name].has(widget_keyword):
_widget_keywords[widget_name][widget_keyword].handle_callback(widget_keyword, data)
else:
push_error('DebugContainer.update_widget(): Widget name and keyword "' + widget_name + '.' + widget_keyword + '" not found (' + str(_widget_keywords) + ')')
Our API to interact with debug widgets will work like this: we’ll pass in a widget_path
string to update_widget()
, split with a .
delimiter. The first half of the widget_path
string is the name of the widget we want to send data to; the second half is the widget keyword we want to invoke (and thereby tell the widget what code to run).
update_widget()
performs string magic on our widget_path
, makes sure that we sent in a properly-formatted string and that the widget and widget keyword is part of _widget_keywords
. If things were sent correctly, the widget node reference we stored during registration is accessed, and the handle_callback()
method called, passing in whatever data
the widget node expects. If something’s not done correctly, we alert the developer with error messages and return without invoking anything.
That’s all we need to talk to debug widgets. Let’s make a test to verify that everything works!
Currently, our TestScene
scene doesn’t have an attached script. Go ahead and attach one now (calling it TestScene.gd
) and add the following code to it:
extends Node
var test_ct = -1
func _process(_delta) -> void:
test_ct += 1
if test_ct >= 1000:
test_ct = -1
elif test_ct >= 900:
Debug.update_widget('TextList1.remove_label', { 'name': 'counter' })
else:
Debug.update_widget('TextList1.add_label', { 'name': 'counter', 'value': str(test_ct % 1000) })
This is just a simple counter functionality, where test_ct
is incremented by 1 each process step. Between 0-899, Debug.update_widget()
will be called, targeting a debug widget named “TextList1” and the add_widget
widget keyword. For the data we’re passing the widget, we send the name of the label we want to update, and the value to update to (which is a string version of test_ct
). Once test_ct
hits 900, however, we want to remove the label from the debug widget, which we accomplish through another Debug.update_widget()
call to TextList1
, but this time using the remove_label
widget keyword. Finally, once test_ct
hits 1000, we reset it to 0 so it can begin counting up anew.
If you run the test scene right now, though, nothing happens. Why? We haven’t added TextList1
yet! To do that, go to the DebugLayer
scene, remove the existing test label (that we created during Part 1), and instance a DebugTextList
child, naming it TextList1
. Now, if you run the test scene and open up the debugging interface (with Shift + `
, which we set up in the previous part), you should be able to see our debug widget, faithfully reporting the value of test_ct
each process step.
If that’s what you see, congratulations! If not, review the tutorial code samples and try to figure out what might’ve been missed.
One More Thing
There’s an issue that we’re not going to run into as part of this tutorial series, but that I’ve encountered during my own personal use of this debugging system. To save future pain and misery, we’re going to take care of that now.
Currently, our code for debug widgets always assumes that we’re going to pass in some form of data for it to process. But what if we want a debug widget that doesn’t need additional data? As things stand, because debug widgets assume that there will be data
, the code will crash if you don’t pass in any data.
To fix that, we’ll need to add a couple of things to the base DebugWidget
class:
# Controls if the widget should allow invocation without data.
export(bool) var allow_null_data = false
# Called by DebugContainer when one of its widget keywords has been invoked.
func handle_callback(widget_keyword: String, data = null) -> void:
if data == null and not allow_null_data:
push_error('DebugWidget.handle_callback(): data is null. (' + widget_keyword + ')')
return
_callback(widget_keyword, data)
We’ve added an exported property, allow_null_data
, defaulting it to false. If a debug widget implementation wants to allow null data, it needs to set this value to true.
handle_callback()
has also been modified. Before it runs _callback()
, it first checks to see if data is null (which it will be if the second argument isn’t provided, because we changed the argument to default to null
). If data
is null
, and we didn’t allow that, we push an error and return without running callback()
. That prevents the game code crashing because of null data, and it also provides helpful information to the developer. If there is data, or the debug widget explicitly wants to allow null data, then we run _callback()
, as normal.
That should take care of the null data issue. At this point, we’re golden!
Congratulations!
Our debugging system now supports adding debug widgets, and through extending the base DebugWidget
class we can create whatever data displays we want. DebugTextList
was the first one we added, and hopefully it should be easy to see how simple it is to add other debug widgets that show our debugging information in whatever ways we want. If we want to show more than one debug widget, no problem, just instance another debug widget!
Even though all this is pretty good, there are some flaws that might not be immediately apparent. For instance, what happens if we want to implement debug widgets that we don’t want to be shown at the same time, such as information about different entities in our game? Or what if we want to keep track of so much debugging information that we clutter the screen, making it that much harder to process what’s going on?
Wouldn’t it be nice if we could have multiple debug scenes that we could switch between at will when the debug interface is active? Maybe we’d call these scenes “containers”. Or, even better, a DebugContainer
.
That’s what we’ll be building in the next part of this tutorial!
If you want to see the complete results from this part of the tutorial, check the tutorial-part-2 branch of the Github repo.