This, finally, is the proper “what went right, what went wrong, and anything else you learned” postmortem. You’ll probably need to read the design devlog entry to make much sense of it.
What Went Right
Mostly I do my hobby dev on the Linux (technically a dual boot machine, but Windows is very much the secondary OS) machine that is really set up for media streaming and a console-like gaming environment, This means its got terrible ergonomics for long coding sessions. So I did the vast majority of the work during jam week on a MacBook set up in a more programming-friendly environment, The reason I don’t use this machine normally is that it’s on the elderly side and just can’t cope with the demands of Unity 3D, which is where most of my game dev has been done. But 2D, turn based in Godot? Yeah, it coped with that just fine.
Also, I’ve got a proper hi-fi in that room, and getting up to change CDs rather than having a continuous stream is great for encouraging drink/wrist/posture breaks.
Choice of engine
The more I use Godot, the more I appreciated the integrated development experience it provides compared with Unity, which always feels like a disjoint collection of program bundled into one interface. Except the code editor, which explicitly isn’t.
I’m also starting to get my head around GDScript properly, this being the first project of any size I’ve used Godot for.. Having a custom language was one of the things that initially put me off Godot, but experience with Unity now makes me appreciate having a language so tightly coupled to the game engine. The Pythonicity has been both a blessing (Python is my default language) and a curse. As I said in an earlier entry, no matter what anyone says, GDScript is only superficially like Python, and I would keep instinctively reaching for constructions like comprehensions and enumerators only to find them missing. And forgetting about key language features like signals.
And creating the UI is so much easier than in Unity. A benefit of the Godot Editor being a Godot application. It feels like the kind of UI design I was doing 15 years ago (yeah, it really is that long since I did any non-web UI work) rather than the 30 years ago (Xaw, if you’re curious), which is how Unity makes me feel.
OK, I didn’t get a chance to thoroughly test it, but it feels right at level 1. Enemies can kill you easily, but not one-shot you, and you can kill them with a bit of luck and cunning. And once you’ve managed to steal a ranged weapon, it becomes a fairer fight. I threw in a last minute bonus to melee weapons (compared to ranged weapons using the same damage calculation) to make it an interesting decision whether to shoot an enemy from a distance or get in close and personal, but it’s not clear how this (or my choices of random functions for combat outcome) have played out.
I submitted a game which could, in theory, be played through to an end following the designed progression through levels.
What Didn’t Go Well
(Nothing really went wrong)
The submitted game is horrifically buggy, and is missing at least 50% of the features and graphics it was designed to have. As the week progressed, I realised I’d have to descope more and more features to stand a chance of submitting anything at all playable, and the extra sprites I was planning on creating had to be dropped completely. To go into more detail:
My intention had been to have unique visuals for each weapon type — 8 in total, or possibly 7 with the EMP giving no warning sign. I had 3 pre-prepared for ranged weapons, and wound up duplicating those across the other 3 ranged weapons (including EMP) just to give some kind of clear sign of who’s going to shoot at you. The two explicit melee weapons are completely missing.
I had vague plans that the colour cast of the eye strip would indicate armour class, which would require two more sets of robots in different colour casts, plus a little @ decal for the player. (Decals on enemies could indicate level.) Dropping the player sprite into a created scene for the first time immediately convinced me this wasn’t going to work: 48x48px just isn’t enough space to have any kind of clear decal, and without that you can’t distinguish the player from unarmoured enemies. So I abandoned all that (and, conveniently, the work involved) in favour of yellow=player, green=enemy.
I had slightly more concrete plans to have yet another set of robot sprites with alternatives to the “walking treads” to indicate the drive class, hence how fast you could expect an enemy to move before they did so. But I always recognised this was going to be a bit of a stretch, and don’t really regret it’s lack.
It’s like you need these graphical clues to tell you about an enemy (or yourself) — it’s all there in the Status/Scan panels.
Choice of tileset
I still like the look of the tileset I chose, but didn’t realise until I’d pretty much committed to it that it doesn’t provide complete coverage if you’re using Godot’s 3×3 tilemap mask. Which means you sometimes (often) get these brownish “default” walls where there’s nothing for the autotile system to place. I suppose that’s another set of missing sprites, but not ones I was planning on creating myself.
Also, there’s no floor tile in this set. Instead, the floor is a 72x72px from the same asset pack as the robot tiled as a layer underneath. This meant that I had to flip the level generation code, which in the original carves floor out of roof, to add walls to a blank canvas.
Fog of war
The first thing that was in my plans when I started to get dropped because of time pressure was fog of war. Apart from the graphical aspect to it, there were code considerations to be made. I’d implemented panning the view independently of player movement really early on, and while that wouldn’t have to be directly affected it would mean the cursor could be placed outside the range of the player’s knowledge and used to gather supposedly hidden information. I think it’s justifiable in the game scenario: these levels aren’t unknown to the player, so having a full map is fair, and it would make sense for robots to have some kind of low-level automatic detection of each others’ presence.
Any minimap code could have been made a lot easier without fog of war, but even so it was a nice-to-have that didn’t make the cut. Being able to pan all over the real map makes it somewhat redundant in a turn-based game.
I was also planning on having an effectively side-on view of all levels (like Quazatron’s) to show what state each of them was in. This is just over the edge of nice-to-have into useful features, as there’s no other way to look up this information.
They probably could have gone in without much effort, but I think they’re really a nice-to-have, so I just tweaked the level placement of the fixed classes and quietly forgot about them.
This is what Quazatron calls the combat mode where you enter the mini-game (which I’d descoped before the start of the jam) which allows you to defeat enemies and steal their parts. I had everything in place, including the random outcome code based on the critical-win code I’d mocked up in the design phase, but had no idea how to map a win onto damage. (An unconvincing grapple win does a lot of damage, and may not leave you with anything worth salvaging. On the other hand, a grapple loss won’t destroy you, but will leave you damaged.) I was running out of time, and had other things to implement (specifically, the enemy AI), so grappling got dropped. Fortunately, I’d (unintentionally) designed the code in such a way that made this really easy.
EMP (sort of)
The EMP was intended to be a directionless ranged weapon (in Quazatron, it hits everything on the level it’s fired on). I never got around to the special code for it, so it’s just a directional weapon with a radius splash damage.
I didn’t consciously drop the implicit damage (or any kind of hunger clock). It just slipped my mind until it was too late.
This was the last thing to go in, started with less than 24 hours to go. So it’s there, but isn’t what I was planning on it being. Specifically, I’d intended the AI having some sort of “threat level” of the player, and either ignoring them, running away, attacking on sight, or deliberately hunting them out depending on that assessment of threat and the class of enemy concerned. I lost a lot of time in those last 24 hours debugging issues with the turn mechanism and detecting whether the player was within ranged weapon range, and in the end was lucky to squeeze out something a bit more interesting than “keep going forwards until you hit an obstacle, then pick a random new direction”. (There’s a 25% they’ll take a turn at a junction on a room level, a 10% chance they’ll randomly change direction in the middle of an open space, and a 28% chance they’ll do absolutely nothing.) Which means they present a challenge not by being pro-actively hostile, but in being utterly unpredictable.
Pretty much all the extra equipment I had designed only made sense in the context of features which in the end got descoped: masquerading stats doesn’t help the player if there’s no threat assessment, always have the advantage of initiative in a grapple is meaningless without grappling combat, and detect all enemies on a level happened automatically when fog of war went away.
This had the nice side effect of meaning the player sprite could be the (cuter, rounder) “unequipped” robot and the enemies the “horned” equipped version. Robots still have an extra equipment list in their stats, and chose a sprite accordingly, so this was achieved by giving enemies a piece of extra equipment called “none”.
I’d implemented a save/load/restart/quit UI about half way through, but only the “quit” really worked at that point. I always intended revisiting, but didn’t get back to it until there was 30 minutes to the deadline. I made a last-ditch effort to at least save the game seed, but even that failed and with no time to debug it, only the working “quit” was left.
To be fair, I’d never quite worked out whether I was going to do a proper roguelike no-backtracking save, or have a single save but letting you quit without saving, so you could bail out of hairy situations and resume at the last time you saved.
Apart from the font in the info panels (Nova mono), the entire UI is out-of-the-box Godot. One of these days I’ll learn how to use Godot’s UI theming.
What I’m Not Sure About
These are really things where my lack of genuine familiarity the genre have left me taking sometimes wild guesses as to what would be playable, and I don’t know how good that guess has been.
The original plan had been for each level to be a 30×30 square, which would mean the full width would be visible at 1080p, assuming no information panel. Given an information panel, that wasn’t that important, and given I was aiming for a browser game as primary target, and the default itch.io resolution for embedded games is 640x360px, I decided to target 720p instead (figuring the scaling down from that to 640×360 would be better than from 1080p). I still started with 30×30 tiles, but the BSP Tree code for room levels was struggling to produce anything interesting, so I doubled it up to 60×60. Which looked great, but when I finally got to something where you could move the player around and discover things I got the feeling that it was much too big. So it went back down to 30×30 and the level generation and made some heavy adjustments to the BSP Tree parameters. Although that produced nice looking levels, it would sometimes hang trying to place items due to the proximity rules I’d imposed, so those needed adjusting too. I think the result is OK.
As for the number of levels, I realised that 56 levels with no save mechanism was a bit crazy, so I made it adjustable. Unfortunately, I didn’t have time to adjust the enemy distribution, so you don’t get to see all the classes unless you play with at least 4 levels. I honestly don’t know what would make for a good game if you did have a save feature.
As I said, I never had a chance to give it a real test below level 1 (I did a bit on level 2 before adding the AI, which made all the enemies sitting ducks, and may have made the rail gun over-powered). And grappling of course never got tried. The thing that’s been a really wild guess is how to turn the original maximum-hit-only design code into something random. I went with the normal-distribution probability from GDScript’s random number generator, but while the mean value to choose in each case was pretty obvious, I’d really no idea what to use for deviation, and no time to experiment.
The removal of grappling, where the logic stat is key, rendered the weapons which specifically target logic (logic probe and EMP) slightly less useful. Possibly. Similarly, the lack of implicit damage to the chassis, or anything which drains power, makes attacks targeting those specifically less significant. In fact, those three critical stats become pretty much interchangeable, apart from those specific weapon vulnerabilities.
The original design was to require all robots on a level to be deactivated to unlock the downwards lifts. Fairly early on, I decided it would make more sense in-world, and possibly from a game-play perspective, to turn the “charging points” into “access points” that had to be visited and “reset” to unlock the lifts. And since this was while fog of war and the minimap were still on the table, maybe unlock those. I’m unsure whether this was the right thing to do, particular given how I changed …
I went in knowing the victory condition had to be “deactivate all robots” and in roguelike tradition return to the surface. But I had no in-world reason for the return, and no in-game challenge. But as I was writing the “All robots deactivated” message, it suddenly struck me that putting a turn timer on return would provide a challenge, and an in-world explanation would be that this is how long it would take the systems to reboot, after which all robots in the facility would be wiped. Although writing that now I realise it doesn’t make sense, as wouldn’t that solve the corrupt robot problem? Anyway, if victory condition is “deactivate all robots and return to the start in a tight time limit” and you can descend without clearing a level, you could just leave level 1 occupied once you’d upgraded a bit, clear everything else, make a leisurely return and one-shot everything left for an easy last phase.
Quite apart from the missing sprites and tiles, I’m not sure how well the graphics I do have work. Obviously, the artwork isn’t mine, but the choice to use it was. My feeling is that for the look I wanted I should have gone for 72x72px tiles/sprites at 1080p.
What I Learned
Be really careful with version control when doing game dev on two different machines. You may think it would help with sync, but the problem is if you’ve got things which can only be reliably edited by the game editor but are stored as text files which git thinks is just code (in this case, tscn files), you can get into a real mess if your local copies diverge and you try and merge them. I think in future each machine will get a branch of its own.
FSMs are a great idea, and I should implement them properly for character (player and AI) control, rather than doing half a job with a pile of enums and states being changed by simple assignment all over the place.
GDScript is not Python. It has its own strengths and weaknesses in comparison.
I’ve still not got the hang of estimating project size.
There’s a good game in here. I don’t know whether it’s from finishing off this turn-based version with all the planned features, or turning it into the action game I’d originally been thinking of for “Quazatron with procgen”. Or maybe, in between, a SuperHot (which is so a genre now).
Devoting a week to writing to a consequence-free deadline, for fun, is a really enjoyable. Especially when you’re in lockdown so don’t have to feel guilty about not going out to do something less work-like. Or is that just me?