Devlog 57 - Level Editor, Scoring Rework

Published: August 28, 2023

I've been kinda radio-silent over the past month. The first part of that was due to simply not getting that much done, but in the latter half of the month I had a better (and more exciting) reason for foregoing updates: I've been hard at work building out the initial skeleton of the Rhythm Quest Level Editor!

Changing My Mind on the Level Editor

Those of you who have been following along for a while will probably know that I've always had plans to build out a Level Editor for Rhythm Quest, but that I had initially pegged it as a post-release feature -- something that I'd build after the release of the initial game. I wanted to make sure that I focused my efforts on bringing the main game to a finished state, even if it meant sacrificing some extra features that couldn't make the cut. Building a level editor is no small task and I wasn't even sure exactly how I would be able to do it. Would it be an entirely separate application? What would the interface look like? Would I have to refactor the entire level generation process again? What formats would levels be saved in? How would I load in external music files? How should I handle copyright disclaimers?

Despite my strong belief that Rhythm Quest levels work best when you have someone (like me) carefully crafting both music and charting together, and ensuring that the levels follow canonical Rhythm Quest charting conventions, I understand that the level editor is a popularly-request feature and could really help to bring excitement to the game in a multitude of ways. But the reason that I decided to change my mind and start working on the level editor now (instead of post-release) is much simpler: I simply got interested in building it.


On-Demand Level Generation

Like so many other challenges that I've come across in working on Rhythm Quest (like the new coin/medal rework, which I'll talk about later), the level editor conundrum was one of those things that sort of just sat in the back of my mind for a long time until I had finally devoted enough spare idle cycles to it and was beginning to have some ideas of how to actually get started working on it. There is of course something to be said for keeping feature creep down, but I've learned that "working on whatever I'm excited about" is usually a good approach for keeping me going.

Rhythm Quest levels are authored as strings of events. Here's how the charting for level 1-3 is written out, for example:

"Obstacles": [
    "#... .... *... *... *... *...",
    "#^.^ .-11 .^.^ .+11",
    "#+.- .*.* .+.- .*.*",
    "#.11 ..^1 ..^^ 1111",
    "#*.* .*.* .111 1.11",
    "#.^^ ..** ..^^ ..**",
    "#.^^ ..** ..^^ ..**",
    "#1.+ .1.^ .1.- .11*",
    "#1.+ .1.^ .1.- .1**"
],

The different symbols here are a representation of different types of events. '#' represents a checkpoint, for example, while '1' is a basic enemy, and '^' is a normal jump. ('*' is shorthand for a jump followed by a flying enemy.) When the engine parses this string, it converts it into its respective sequence of timed events, so something like:

_events = {
    new EventData(0.0f, EventType.Checkpoint),
    new EventData(8.0f, EventType.NormalJump),
    new EventData(8.5f, EventType.SingleEnemy),
    new EventData(12.0f, EventType.NormalJump),
    new EventData(12.5f, EventType.SingleEnemy),
    ...
};

This (along with other metadata about the level) then gets passed off to the level generation procedure, which is responsible for processing all of the events in order and building the actual level out of them. Normally this is all done ahead-of-time when I author the levels (in a process I call "baking" the levels), so the end level objects are saved into the scenes directly to optimize load time.

Now, the way that the (work in progress) level editor works is simply by maintaining a similar list of events that compose the level being edited, and re-generating the level again every time there's any change. It might seem terribly inefficient to keep rebuilding the level compared to just editing the resulting level objects directly, but there's a lot of reasons why it makes sense to do things this way. For example, changing a list of events is simply more efficient than having to worry about editing the actual level objects (moving floors around, etc). and I already have the code to do all of this, so I just have to worry about providing an interface to visualize these changes well.

Testing the Prototype

Part of the reason I wanted to dive into working on the level editor right away was simply because I was curious whether this approach would even be feasible at all. I was worried that re-generating the level at each change might be too slow, for example. So I created a quick editor scene and made a script to hold a list of events, populated with some test data. I could then invoke the level generation process at runtime from there...

...and have everything be totally broken. All of the objects in the game are all built assuming that if the game is running, the level is supposed to be playing. They also assume that a song is playing, that they can query for the current music time, that a player instance exists, etc. So I had to do a bunch of refactoring to handle this unplanned-for case where we have all of these level objects, but they're not actively updating because we're in the level editor.

One thing I wanted to shoot for was to be able to instantly jump from the level editor into playing the level, without having to go through any sort of scene transition or anything like that. So I needed to make sure the level editing scene also contained everything needed for the base game, including the player instance, the music controller, etc. I also wanted to see if I could successfully load in audio files specified by the user. Here's what all of that looks like in action:


After doing all of these refactors, I had a simple prototype and I could add in basic enemies or ground ramps by pressing a key on the keyboard. One of the first things I did after that was to see what the performance was like when I triggered level generation, especially after I added a ton of events and made the level longer. To my delight and surprise (especially because the full level baking process normally takes a bit), the performance was actually pretty acceptable! I was initially expecting to see like 1-2 second pauses once the level got longer, but it seemed like it was only a minor hiccup most of the time.

This is also without any sort of optimization -- of which there could be many. Besides just raw "cache things, do work ahead of time, make the code faster", there's also the fact that most events shouldn't require the =entire= level to be rebuilt. Yes, a change in the ground ramp early on does mean that the height of the rest of the level will change, but at least you can skip re-generating everything that came before that. And adding or removing enemies shouldn't require the entire rest of the level to change. If it came down to it, I could force you to work on only one checkpoint section at a time. But it looks like I don't have to worry about any of those optimizations (yet).

Input Timeline

There's going to be a lot of work for me to do in the upcoming weeks for implementing various tools so that the editor can actually provide enough functionality to create a full level -- both in terms of all of the actual level obstacles (water zones, flight paths, etc.), as well as the level metadata (music BPM, scroll speed, background palettes). One thing I did in the meantime was to implement what I'm calling an "input timeline" feature, where the expected inputs are displayed as colored diamonds in a separate track below the level. I added this mostly for use in the level editor, but I also made it function in-game in case you want to use it there:


The exact look of this will probably need to be adjusted (not very colorblind-friendly right now either), but this is a really useful view for the editor already, and will probably become even more important once I look into more advanced editing features (editing via keyboard or even a "record" style live play). One thing about this input timeline is that you can see just how boring of a game Rhythm Quest is in terms of the raw inputs. A big part of the appeal of the game (to me, at least) is parsing the platformer-like obstacles into rhythmic elements; if the chart is just laid out in front of you like this it's really not too interesting.

Scoring Rework

I did this a while ago but never wrote about it. Despite the fact that I've already tweaked the coin / respawn / medal scoring system a few times (at various points in time it's alternatively been based on respawns and coins), I've iterated on the system once again. I was never happy with how the medal thresholds felt both arbitrary and also not very visible, so I worked out a "progress bar"-style animation in the level end screen to show that off visually:


The thresholds are now straightforward and easy to remember based on the visual (25% = bronze, 50% = silver, 75% = gold, 100% = perfect). Previously you were awarded a bronze medal simply for completing a level, but I've changed that, so you'll just have no medal if you finished with less than 25% coins.

Along with this, I'm trying out a new system for coin loss amounts. Previously you always lost a fixed amount (5 coins) on every respawn, but this usually led to people either getting very close to perfect, or losing almost all of their coins on a particular section or two that they had trouble on, even if they performed very well through the rest of the song. I've always wanted something that scales more cleanly, like for example every time you respawn you lose 50% of your coins, but that by itself doesn't work well because it's extremely punishing for single mistakes that are made late in a level.

The way it works now is more complicated, but should hopefully be more "balanced" in terms of coin losses. The new system internally maintains two different coin numbers -- the coins that you have recently collected, and the coins that you have "banked". At every checkpoint, half of the coins you have on hand are put into the "bank" and can never be lost from then on. And at every respawn, half of your non-banked coins are lost. The idea is that this system rewards you for performing well, and can't fully "take away" that reward even if you mess up a lot afterwards. It's a bit obtuse in that it's a pretty hidden mechanic, but I like the simplicity of implementation and the fact that I'm not using some really random number like 5. We'll have to see how it works in practice, though!


That's going to do it for now. I'm trying my best to get the level editor off the ground...it's a lot of work, but also interesting and exciting since there's so many little systems that need to be written, for the first time! There's unfortunately a good chance that this will end up pushing back my launch date to 2024, but...I'm hoping you'll all agree that the custom levels that will come out of this will be worth the wait.

<< Back: Devlog 56 - Gameplay Modifiers and More
>> Next: Devlog 58 - Level Editor Progress