Thank you for sharing
Making of Patrolmech 2121
This morning, I spent a few hours playing through the awesome games developed by so many talented folks for Mech Jam 2021. I've been extremely impressed with the high quality submissions despite the fact that this jam was relatively small. I decided to take a break to write up a postmortem on my submission, Patrolmech 2021, because who doesn't like game jam postmortems? Well, I hope you like them at least, since you're here :P
All I knew when I started the jam was that I wanted to have an enormous mech that would stomp around at night and shine an imposing spotlight at people who were trying to escape from somewhere. I wanted to have motion in the fog by utilizing particles or a noise texture in a volumetric shader along with rain, but my computer wasn't able to keep up with all the particle effects I threw at it. Unfortunately, I had to scrap them. Instead, I went with simple depth and height fog that was directly built into Godot, and faked the volumetric light with a cone mesh that had a transparent gradient texture material on it. Inside of the mesh, I placed a spotlight that cast shadows, so you could more clearly see when a prisoner was in the light.
This choice of using a mesh to emulate volumetrics ended up directing the visual style for the rest of the assets. For instance the rocket engines that are visible while hovering used a similar technique.
The mech design was literally just whatever popped into my head when I decided to model it. It ended up looking like a chicken, which isn't particularly what I was going for, but the spotlight is a prominent part of the design which helps distinguish the mech. I might try to refine the design a bit more for some more promo renders, but we'll see...
After modeling, rigging, and animating a walk cycle, I wanted to get it into the game as quickly as possible. I put together a simple character controller in Godot, and hooked up a function call that would trigger a camera shake on each footstep. This setup was good enough to provide a feeling of weight, and was finished fairly quickly so I was able to move onto other aspects of the game at that point.
For grayboxing, I used Godot CSGMeshes to quickly slap together a level with collisions. Level design is definitely a skill I want to improve, but I think the level was dynamic enough to be interesting for the short duration of the game. I wasn't able to do much in the way of environment art, unfortunately. If I worked on it more, making the environment look industrial and run down would benefit the atmosphere of the game, and adding some details might help clarify scale.
I really wanted to focus on sound design for this jam, partially because the idea I had was more reliant on atmosphere, and partly because I've wanted to apply some of the sound design lessons I've learned recently. From the start, I wanted to make a clunky, slow-moving mech that felt heavy (and a bit awkward) to control. I needed some heavy impact sounds for footsteps, some hydraulic sounds, and some sound that indicated that the spotlight was turning.
Before recording anything, I watched a few videos to see what kind of noises people used for mech sounds. One video especially had some ideas I wanted to replicate. For the footsteps, I combined the sound of closing a car door with slamming a filing cabinet shut. These layers give a nice metallic impact sound, and with a bit of distortion and OTT, I think the result was pretty nice.
For the hydraulics, I recorded a pump for inflating balloons and pitched it down, then had that play whenever the legs were animated. The spotlight rotation sound was a recording of the power-windows on my car that was edited so it looped cleanly, with the in-game version dynamically adjusting the pitch based on the speed of the motion (so it sounds like the motors are expending effort to rotate faster).
The drone/ambience sounds built from looped wind recordings with a few Serum pads playing underneath to add some bass. I also made some UI blips using Serum synths, but didn't end up having any UI to add them to.
I wrote the majority of the main theme song in one sitting. The beginning of the game needed more ambiance and atmosphere, while the end of the game needed to be higher energy with drums and driving synths. The main piano track that plays through most of the song was a key element in the structure and overall sound. For tools, I used GarageBand for the DAW, Serum for the synths, and the Architype Plini plugin for the amp sim that the rhythm guitar uses in the bridge.
In the game, I built a dynamic music player that was capable of swapping out different tracks of the song in time with the music based on the BPM. It had two audio players so it was possible to crossfade two tracks. I built it this way so I could dynamically increase the tension of the music with a function call. I was originally going to write multiple versions of the verse that would play depending on the level of tension required by the game (similar to the music system in the new Doom games). However, since I only wrote one version of the verse, this feature basically went unused.
To be honest, I wasn't sure what I was going to do with gameplay from the start. As I mentioned earlier, I was originally envisioning the atmosphere of the game, so the gameplay ended up taking a back seat until the last half of the jam. At that point, I scrambled to put together something cohesive. I ended up adding a lot of extra behaviors to the prisoners so that the game loop progressed beyond just increasing the number of prisoners you had to capture.
Originally, the mech wasn't going to hover, but after realizing it would be laborious just to walk across the map due to the animation speed, I decided I needed a faster mode of locomotion. Changing the walk speed at that point would have been challenging because a lot of the sounds were already in place and assumed a slow footstep frequency.
I also wanted to make sure I added a few safeguards against cheesing because I've built games for jams in the past, and have learned that people will cheese the game if they're given the choice, which can ruin the experience. Therefore, I made sure that there were two spawn points and two exits to force the player to move around. I later added the proximity sensors and sabotage mechanics as a way of encouraging the player to move back and forth across the map.
This was the first 3D game I've made for a game jam and also the first full project I've built in Godot and also, so I learned a lot about how the engine and scripting API. The code is horribly inconsistent because I was learning features and best practices as I went along, so it's a mix of several different paradigms. But overall, here are a few of the main lessons I learned during this jam, including some more general points.
Functionality of the yield statement in Godot
The yield keyword was something that especially confused me. I'm familiar with how Python's yield statement works, and assumed Godot's was similar. In it's most basic form it is the same. But it's a different story when you pass arguments to yield.
In both Python and Godot, you can pause the generator or coroutine with the no-arg yield statment.
# Python def f(): for i in range(10): print("I'm at " + str(i)) yield x = f() next(x) # prints: "I'm at 0" next(x) # prints: "I'm at 1" # Godot func f(): for i in range(10): print("I'm at " + str(i)) yield() x = f() f.resume() # prints: "I'm at 0" f.resume() # prints: "I'm at 1"
However, they differ when you pass arguments to yield. In Python, the argument you pass gets returned to the caller (kind of like an intermediate return value)
def f(): for i in range(10): print("I'm at " + str(i)) yield('A yeilded value ' + str(i)) x = f() print(next(x)) # prints: "I'm at 0" followed by "A yielded value 0" print(next(x)) # prints: "I'm at 1" followed by "A yielded value 1"
But in Godot, use this syntax to subscribe to a signal.
func f(): for i in range(10): print("I'm at " + str(i)) yield($ATimerNode, "timeout") x = f() # prints "I'm at 0" # ... then waits for the timer to timeout # then prints "I'm at 1" # ... then waits for the timer to timeout # ... etc ...
What confused me is that you don't call f.resume() anywhere. Yielding to the signal automatically adds a callback somewhere internal to Godot. It'll automatically re-enter the function and pick up where it left off when that signal triggers (effectively calling f.resume() for you). If you call f.resume() yourself (like I did), it will continue without error, ignoring the signal entirely.
This is an extremely useful feature, though. It allows you to avoid the callback-hell that's prevalent in some other languages. However, it is a bit like a goto, in that it doesn't compose nicely. For instance, when writing my dialog code, I often had the following pattern:
func do_dialog(): say("Some line of text") yield($DialogBox, "finished_speaking") say("Another line of text") yield($DialogBox, "finished_speaking") say("Yet another line of text") yield($DialogBox, "finished_speaking") # ... etc ...
You might ask why I didn't abstract "say" and the following yield into a sub function. Well, if I did that...
func say_and_wait(line): say(line) yield($DialogBox, "finished_speaking") func do_dialog(): say_and_wait("Some line of text") say_and_wait("Some other line of text")
...then yield will only suspend the say_and_wait function, not the do_dialog function. In effect, it'll run all the say_and_wait functions without waiting, then each one will wait for it's yield function to execute, and do nothing afterwards (because there's nothing left to do in say_and_wait after yielding).
It's a curious control flow primitive. Definitely useful, just limited in what it can do. I wonder if there are plans for making it more composable, perhaps by passing in a context that allows you to reference which function you want to pause when yielding.
Playtest the control scheme early on
My plan was always to make the controls a bit awkward to use, since the mech is supposed to be an outdated machine that's a bit clunky. However, after getting some feedback from folks reviewing the game (thanks, by the way, to everyone who has reviewed it so far!), I've realized that I pushed the awkward controls too far. Since the bottom half of the mech goes in the direction its feet are pointing (rather than where the mouse is pointing), and it only turns towards the mouse very slowly, knowing where the mech is going to move when hitting WASD is frustrating since the player is focusing on where the spotlight is pointed. I saw this issue when I got my brother to play the game before releasing it, but didn't think it was a big deal.
I still think making the bottom half a bit delayed is a fine design decision, but I would change the implementation next time and make the bottom half turn towards the mouse more quickly or show the arc of movement to the player.
Since the control scheme is such an important part of an action game, it's especially important to pay attention to feedback about it.
Focusing on aesthetics instead of gameplay can be fun!
I really enjoyed making sounds and animations to sell the weight of the mech. From a gameplay perspective, it's not the strongest, but it turned out better than I was expecting. It's a bit short, but I was able to add progression to the game within a few hours of development at the end. Since many game jams only give you 48 hours to develop the aesthetics and gameplay, the two week duration of Mech Jam allowed me to focus on aesthetics most of the time and leave the gameplay to the last 48(ish) hours of dev time. This seems to be a valid strategy that I might use again.
Godot is pretty great!
Of the engines I've used, I was impressed with how much functionality is packed into the engine, while still being relatively light-weight. I've used Unity before, but always thought it was a bit bloated. I like GDScript a lot, since Python is my go-to language for most of my programming these days. I'd like to see certain features like list comprehensions and tuple destructing added, but it has a lot of the syntactic elements I like, and the type checking is good. The editor has first-class Linux support, so I was able to stick to my preferred development environment the whole time. The API documentation and how-to guides are high-quality and complete (for the most part), and I love the built in documentation browser. The cross-platform builds were pretty much as seamless as they could be. Once I generated the binary, I could just upload it to Itch and be done with it (though I did have some issues with the Windows/Mac builds that I think were due to underlying platform issues -- I discuss these in the next section).
It has it's quarks though. I ran into some weird issues with the glb import pipeline and it would sometimes throw an error and not refresh the resource until I reopened the scene. The navmesh support is undocumented and lacks some useful features like being aware of other agents. The animation tree doesn't allow you to play an animation in reverse, so you have to create a unique animation track for walking forwards, backwards, left, and right. The web build never seemed to work. These are all relatively minor complaints though, and the engine is open source so I'm hoping to contribute some patches to address some of these issues in the future.
Test your release builds on your target platforms at the halfway mark
I was almost burned by some unexpected platform differences that didn't crop up during development because I was on Linux the whole time. These issues didn't manifest until I tested the final release builds right after I submitted my game.
The first issue was that the light cone didn't seem to affect the prisoners on Windows and Mac. Fortunately, I had each of these platforms to test on, so I installed Godot to my Windows laptop, built the project and popped in a few print statements. Eventually I realized that the _process and _process_physics frames didn't run in lockstep. This was a problem, because I was modifying a boolean in both functions that was used for gameplay logic that impacted each frame.
When a prisoner was in the light cone, I had an in_light boolean that would flip to true in the _process function. The _physics_process function would increment/decrement a time_in_light value by the delta time since the last frame based on when the prisoner was in the light. Then it would set in_light to false. On Linux, the _process and _physics_process threads would run at roughly the same speed, so this kind of worked. However, in the Windows/Mac build, _physics_process ran three times to every _process call. This resulted in time_in_light always being zero, since it would be set to true by _process, then _physics_process would call three times for every _process call. The first time it increase time_in_light by the delta time, then set in_light to false, then for the next two calls, it would subtract the delta time from the time_in_light, effectively keeping it at 0 the whole time. The fix was to move the code from _process into _physics_process so the boolean was set to true in lockstep.
The other issue I ran into was that audio would pop during playback. This issue was once again only apparent on Mac/Windows. I couldn't figure out the cause, only that it seemed to be related to slow frames. I eventually happened across a Github issue that suggested upgrading to 3.2.4 to fix some bugs in the audio playback. This worked, which was good, because the game would have been really uncomfortable to play with the pops that I heard when testing the 3.2.3 exports.
Fortunately, these were relatively small issues to fix and I discovered them before the deadline because I submitted my game a day early. But I didn't have time to get them fixed until a few hours before the final submission deadline. If there had been worse issues, I may have missed the deadline for Windows/Mac builds.
To avoid this issue in the future, I'll be doing release builds for my target platforms (particularly Windows, since that's the most popular one) throughout development. This will help avoid a nasty surprise right before submission time.
There's probably not going to be much more work done on Patrolmech 2121 -- it was a fun concept that gave me the opportunity to practice some game dev skills that I normally don't focus on. But I'm going to call it a wrap on this project -- I might work on a few more renders of the mech, but other than that, I don't think there's much more to develop. I accomplished largely what I wanted to in the last two weeks, and am glad that I have a (somewhat) polished release to show off.
I had a lot of fun participating in this game jam. It's the first one I've done in a while, and was a nice change of pace from the frantic 48-hour jams I normally participate in. The two week time frame gave me the freedom to build something that was fleshed out, but was a compressed enough schedule that I had to focus on finishing the project rather than letting it languish into a mess of scope-creep.
A special thanks to Luke (https://itch.io/profile/dungeon) for all the work he did to host the jam, manage the discord server, and playtest the submissions.
Thanks for reading this post mortem. If you haven't played Patrolmech 2121 let me know what your thoughts are! In the meantime, I'm going to get back to playing and ranking some more mech games :)