Over the past month I have mostly been cleaning up a lot of issues and blank spots in the rest of the game (after the demo ends). This means filling in missing art, doing a writing pass over a lot of the rougher text and dialogue (there was one scene in particular that I struggled with for a few days), fixing a number of bugs that have been low priority until now, cutting an idea or two that didn’t work out, and drawing and scripting the final boss (still in progress).
I have also released two more pretty sizable patches for the demo, cleaning up a lot of (mostly minor) errors left over from the Unity conversion and adding a few QoL features. I am actually still iterating on the first level, the subway. This has been the part of the game that I have gone over the most. This is partly just because it’s the oldest, but mainly because it’s a fairly linear prologue and tutorial, and getting the balance and pacing of exposition, tutorializing, and actual gameplay, while trying not to give the wrong impression of the real game, is really hard. Among other things, I trimmed some of the more front-loaded dialogue, which I think makes the first few minutes flow better (and the first few minutes are important).
One request I have gotten from several people is to be able to hold down a movement key to move continuously in one direction, so I decided to implement that. I thought this would be relatively simple, but it ended up being ridiculously complicated. Following are the dry implementation details, which I just really felt like talking about.
Since the game is grid-based but still animates motion, my input handling buffers a direction for a short time, then processes it when the player is able to move again (with no buffer, movement feels too staccato). In order to get the feel of continuous movement right, so that it carries the player along intuitively without feeling too slippery, I had to:
- Refactor movement input buffering from the player mover class into the input handler. This is probably a better place for it, but the buffering dates back to the original jam code, long before I even had a dedicated input handler class.
- Adjust the buffering behavior to also keep track of the held movement key, but only after a short delay. With no delay, it is very easy to accidentally move twice.
- Process turning and sidestepping while “running.” This way, the player can make turns or adjustments without interrupting their momentum and having to wait for the delay again. However, the standard buffer (0.08 seconds) is too short for this; when testing it out, I found that I was rarely able to time it correctly. My solution was to suspend clearing this buffer as long as a movement key is held down, which gives a lot more leeway.
– My first solution for handling additional movement input was to have sidestepping take over as the new run direction, but I found that I didn’t like this. I was much more likely to want to correct left or right and maintain the same general direction than I was to suddenly want to run to the side or backwards without watching where I was going.
- Implement the same behavior for the on-screen movement arrow buttons (not something I like to use, but some people do). These originally issued a buffering request directly, but I found that the best solution was to simulate an input event and let the input handler process it like any other (luckily Godot has built-in functionality to do this).
- Do all of this without altering the current behavior of press-and-release input handling.
- Fix A LOT of new input bugs. These were mostly around either buffered input being processed (or cleared) when it shouldn’t be, or around held input not being released when it should be (the click buttons were the worst about this, because they have a tendency to get stuck in certain circumstances). One of the most alarming - something I hadn’t even thought about until I just happened to test in an area where it could happen - was the player dying instantly upon touching a hazard wall. This was because movement wasn’t being halted properly on touching a wall, so the trap damage script just queued up a few dozen copies of itself.
- Fix some existing input bugs, too, that were uncovered by all of this effort. Unsurprisingly, my input code wasn’t the best, being (by necessity) one of the first things I wrote when I migrated to Godot.
– The biggest problem was one that my brother found while streaming to his Steam Deck using a PS4 controller. He was getting stuck in the items menu, with the GUI SFX indicating that the game was hearing his input, but often not returning him to the field. It took me several hours to figure out how to reproduce this, and it turned out to be because of analog noise. Analog sticks have a tendency to jitter a bit and feed useless input to the system even while apparently at rest; this is partly what deadzones are for. However, Godot defines deadzones per action, not per physical input, so this jitter still sends an input event through standard processing (efficiency fail). My problem was that I was routing all the menu input and such in _UnhandledInput - which can be called multiple times per frame, and thanks to the analog noise, often was. This meant that e.g. the item menu key could be processed multiple times per frame, thus closing and then re-opening it. I solved this (at least, I think I have solved it) by moving the command routing into _Process, so that it only happens once per frame. This is probably where it should have been in the first place.
– Naturally, THAT change messed up the frame timing of scripts that relied on the popup dialogue window, because now pressing a button to continue was handled after _Draw, instead of before. This caused the text window and some other GUI elements to flicker between scripts. I solved this by changing some wait-for-next-frame code to wait-for-signal code. The funny part is that waiting for the next frame was originally a solution for GUI elements flickering.
Here’s the final product in motion.
All told, this took the better part of a week to implement, most of which was testing and bug fixing. It is actually a good thing that I decided to work on this sooner rather than later, since it exposed some flaws in my input handling that were causing actual problems. I finally pushed it live last night, along with some other fixes and improvements. Now I really need to get back to finishing the post-demo content.
I have mixed emotions about my demo being live now, but it is probably for the best that I released it well in advance of Next Fest, so that it can be in the best possible shape. If I had waited to release it, a lot of the issues I have addressed over the past month would probably have stayed undetected. I was already mortified when one player experienced a hard lock early in the game; having that issue live during Next Fest would have been a disaster. I don’t expect a lot of traffic from Next Fest, honestly, but if I am going to participate at all, I might as well give it the best showing I can.
I make it a point of pride to at least deliver a smooth and working experience to the player. Crashes, visual glitches, gameplay and input bugs, and so on are unacceptable to me in my own work, to the extent that I can realistically prevent them.