Rhythm Quest Devlog 72 — General Improvements and Optimizations

DDRKirby(ISQ)
11 min readNov 1, 2024

--

No fancy new level to show you all this time, but I’ve been doing good work regardless this month. I have a silly little — ok, maybe it’s really not little anymore, actually quite the opposite — Trello board where I track tasks and work items that I accomplish for the game, divided into weeks, so you sorta track the number of things that I end up tackling per week. This past month had some columns that are longer than can fit in my screen…

Weblate Site

I mentioned in a previous devlog that I had been working on migrating off of Crowdin for managing community translation efforts due to exceeding their free plan limits. I’m happy to report that my self-hosted Weblate site has been up and running and seems to be fully functional with no issues!

Currently (subject to change) this runs on an Amazon t3a.small EC2 instance, which costs me $0.0188 an hour on-demand (was easiest to go with AWS since I already have other stuff running there), which is pennies compared to the non-selfhosted options out there. More importantly, it all seems to work, and it’s even synchronizing everything to a github repository in XLIFF format, so I can breathe much easier knowing that rollbacks or tracking down issues are going to be that much easier.

No More Unity Branding

I finally bit the bullet and did a major engine upgrade to Unity 6 (aka Unity 2023). This involved fixing a couple of bugs that cropped up (the upgrade exposed some issues that were mostly my fault that had never surfaced before). Upgrading isn’t really painless right now because I have all of these custom changes to Unity packages (hacks to make things actually work like they should…), but I managed to handle it well enough. Probably the biggest relevant change (hah) in Unity 6 is the removal of the Unity splash screen requirement for free Unity licenses (huzzah!). I also took a few minutes to update the WebGL template for the web version of the game, so now we get a nice loading graphic and we’re totally devoid of any Unity branding!

I can probably stand to make this even nicer later on, but this is already a welcome change, and nobody has to wait those extra 5 seconds sitting through the Unity logo before the game starts, woo~

Loading Icons

Speaking of loading graphics…I also took another few minutes to implement a simple change that’s been on my list for a while: the loading indicator in the corner of the game now changes to reflect which character you have selected!

(More) Backdrop Optimizations

I was tired of seeing framerate drops when testing on lower-end devices (like the Switch…), so I went in and did another pass at performance optimizations. Besides random trivial “cache this lookup” or “use a binary search here” improvements, the main change that happened was with regards to how the backdrops are rendered (again).

A long time ago I did some nifty optimizations around backdrops that had large opaque areas (obscuring big chunks of what was behind them). Essentially, if we know that the entire screen below a certain point is covered up, we don’t need to bother rendering any of the backdrops below that point.

…eeexcept if the backdrop in question is transparent, in which case we =do= have to go ahead and still render everything. Now of course, I have a lot of backdrops that are opaque, so this is still a good improvement.

…eeexcept when I’m crossfading between two sets of backdrops in the menu. This special case has caused me a lot of headaches in the past and it was never performant on the Switch despite my best efforts. There’s multiple problems at play here:

  • We’re simply drawing twice as much stuff (twice as many texture lookups, twice as many “pixel paint” operations).
  • If you want something that’s a halfway fade between set A and set B, you can’t just render set A at 50% opacity and then render set B at 50% opacity on top of that. That’s not how blending works. So instead you need to reorder everything — you can render set A at 100% opacity, and then set B at 50% opacity on top of all that. That requires a lot of bookkeeping logic around the sort order of all of the different backdrop layers.
  • Rendering backdrop layers at less than 100% opacity throws all the nice optimization described above out the window.

Really what I wanted to do was just treat backdrop set A and backdrop set B as pre-rendered (moving) images and then fade between those two, instead of having to deal with the complexity of each one being made up of 20–30 different layers. So, I did just that!

In the new setup, each set of backdrop layers is rendered at full opacity to two separate temporary render textures — one for foreground layers, and one for background layers (for those of you who are familiar with how rendering works, we need to use premultiplied alpha here to handle transparency correctly). We can then easily crossfade between our temporary textures without having to worry about weird transparency blending artifacts. This took a bunch of work to set up, but actually massively simplified the menu backdrop handling code as I don’t have to worry about doing weird juggling of the sort order of a bajillion different individual layers.

There’s still more rendering optimizations I can potentially look at in the future (some of the world 6 backdrop sets are particularly heavyweight and could benefit from some additional techniques around intermediary textures), but this is already great progress and we successfully eliminated the framerate drops on Switch that were caused by menu transitions.

Editor Improvements

I also made some optimizations for the level editor! Previously I described how each time you make a change to a level, the editor currently needs to regenerate the level in its entirety. To make this faster, I have object pools for all of the level objects, so that I can reuse the instances instead of destroying all of them and recreating them from scratch (which would be much slower).

…except, somewhere along the way I ended up breaking that entirely, lol. When I started supporting different level tilesets, I had to swap out the “level generator” object that contained all of the tileset data, and the easiest way to do that was to just replace it every time you recreated a new level. Except, that object also contained the object pool…which therefore was getting destroyed every time…[facepalm]

Anyhow, that’s all fixed now. I actually found this bug because I was testing out the new fine-grained editor grid, which lets you place obstacles using a 16th-note grid. This is mostly so that you can create interesting syncopated rhythms, but yes, if you wanted to, you could just go wild and make “random note-spam” type charts:

Virtual Cursor

I also added a virtual mouse cursor implementation for the level editor that you can control using either the keyboard or a gamepad. Technically this isn’t really required for any platform (even for something like Nintendo Switch you can still use the touchscreen, unless of course you have it docked), but navigating the editor UI via keyboard doesn’t really make any sense anyways so I figured I’d try this out.

As usual, Unity gets you kinda halfway there but then you have to pick up the pieces, leaving you wondering whether you should have just implemented the entire feature yourself in the first place. Unity provides a virtual mouse cursor implementation that (fortunately) will hook into their UI input system, except:

  • It doesn’t properly handle UI canvases that are scaled (like mine)
  • Disabling and re-enabling the cursor causes a HUGE lag spike as it re-registers a new virtual input device and does god-knows-what-else
  • For whatever reason it seems to not interact with UI (yet still move around fine) if you’re using specific parts of the new input system (device-based control schemes)

But after some cursing and debugging, and some added hacks, it seems to all be working fine! The system is smart enough to enable the cursor when you use keyboard or gamepad input, and turn it back off if you start using a mouse, plus you can even scroll the camera by putting the virtual camera at the screen edges (or via right analog stick).

Gamepad Fixes

As is common, gamepad support is yet another piece of functionality that has landed firmly in the bucket of “I should have just reimplemented this myself manually from the beginning instead of using the Unity-provided solution”. So far I’ve been using Unity’s newer “Input System” solution, which actually has a number of advantages over older implementations — including automatically switching between control schemes based on player input, as well as supporting input rebinding. After (sigh) some hacks here and there, including one to deal with the fact that Steam forcibly takes controller input and translates it to keystrokes (thanks, no thanks…), I actually had that all mostly working.

…eeexcept for the fact that on some systems (??), certain controllers would simply not register input under Unity’s new input system whatsoever. Not just a “my implementation” problem either, as I downloaded some sample projects and the issue shows up there, too. But…clearly the device has connectivity, and Unity even throws up a helpful message reporting when the controller connects.

Gamepad input is understandably something really hard to deal with (given the number of hoops you have to go through sometimes to get a given console controller working with your system), but I’m going to blame Unity for this one given that their old input manager implementation still picks up the gamepad’s input in this case.

I hope I’m not going to be regretting this later on, but for now I’m leaving the old (new?) input system implementation in place (with my hacks, it does end up switching more-or-less gracefully between input schemes, which I like), but I implemented a legacy gamepad fallback implementation for when the new input system claims to not have gamepads attached but the old one does.

Of course, with the legacy input system, there’s no standardization for how the buttons and sticks on various controllers present themselves, which is probably part of why Unity had that old input configuration dialog that used to pop up every time you started up a poorly-made game jam game:

Ah yes, the telltale sign that you were about to play somebody’s first Unity game. Anyways, fortunately for me, Incontrol’s old open-source package contains device profiles for many known controllers. Granted, this hasn’t been updated in the past 3 years (since Incontrol became a commercial closed-source asset), but I found that it was good enough for my purposes, at least for now. I integrated Incontrol’s input handling and mapping, updated it to work with Unity 6, turned on XInput support, and “just like that”, I have gamepad input testing and working for my XBox360 controller on Windows and on my PS3 controller across Windows and Mac.

I mean, forget the fact that I needed two separate programs to even get the PS3 controller to hookup to those systems successfully, and the fact that the XBox360 controller support is just plain borked on Mac…

Font Rendering Fixes

Updating to Unity 6 meant some small shifts in how Unity packages its text-rendering system, which exposed a few issues with pixel snapping and sorting order that I ended up ironing out. While I was at it, I noticed that the unicode pixel font that I use (unifont) rendered OK when the game was at small resolutions, but at higher resolutions it got all blurry and the sampling didn’t seem right.

This one is kind of on me. Unifont is designed to render minimally at size 16 (or 12, depending on what you’re plugging it into), but because my game pixels are often already upscaled, I was having it sample at that size and then setting it to display at 50%. The UI canvas would for example be scaled up by 2x, so it ends up balancing out to present at 1x scale and render crisp pixels (which are 50% the size of the rest of the game’s “pixels”).

Unfortunately that just doesn’t extend to other canvas scaling situations. In the end I just decided to get rid of that 50% factor, so now unifont displays one to one with the rest of the pixels in the game UI:

Of course, now the font is just bigger, so I run into issues where the glyphs can’t really fit properly into the areas they’re supposed to. But if you’re using a language where the unicode font is used, you reallllyyy ought to not use the pixel-based font anyways (and the game is smart enough to default you to smooth font rendering in this case). Unless of course, you’re playing at like 500x300 resolution for who-knows-what reason. In that case, this is actually an excellent change because the unicode pixel font now reads really well at that size, and you probably don’t mind that the lettering is bigger!

What’s Next?

Phew. There’s still an endless list of things to take care of, and we’re already two months out from the end of the year, but I’m happy with the amount of time I’ve been putting into things, at least. Moving forward, there are some remaining big-ticket items that’ll make me feel much better if I can knock them out, including but not limited to:

  • Unifying the selection menu for both bonus songs and custom levels (and main campaign levels, why not) into one big “song select/free play” menu
  • Doing some thinking about whether I want to support multiple game difficulties (and more importantly, how I’d like them to work)
  • It’s awkward that the game settings are half split between “settings” and “game mods”, I’d like to reorganize things if possible
  • I still need a proper export flow for the level editor…

With any luck I will be able to at least get a few of these done by the end of the year. Thanks for reading, as always!

Wishlist Rhythm Quest on Steam today at https://store.steampowered.com/app/1635640/Rhythm_Quest/

--

--

DDRKirby(ISQ)
DDRKirby(ISQ)

Written by DDRKirby(ISQ)

9-bit chiptune artist and indie game developer — http://ddrkirby.com/

Responses (1)