Devlog 28 - WebGL Port, Checkpoint Graphics

Published: March 11, 2022

I was a bit busy with other stuff this week (got a new phone, diagnosed a bug in OBS Studio, etc...), but still managed to do some interesting work on Rhythm Quest anyways. I switched gears a bit and instead of working on more levels I'm starting to do some more polish work in hopes of putting together a more-publicly-facing demo build.

Checkpoint Animations

Part of this polish pass is going through a lot of the older graphics that have been around forever as more placeholders than anything. For example, the checkpoint flag, which is also used as the level end graphic:

It's not =terrible= or anything, but I can probably do better. I thought it would be nice to have the flag waving in the wind as an animation, so I tried that. I started with the level end, which I wanted to try doing as a checkered flag. Here's the first attempt at just laying down the checker pattern and adding a wave motion:

This was kind of nice to work on because it was very pattern-based -- sort of like busywork where you don't need to think about it too hard. It looks pretty unnatural right now, that's because the movement is purely vertical and applies across the entire flag, which doesn't really make sense.

However, this was a good base to work off of and adjust. I first took the concave-furled segments and shortened them to portray the correct perspective a little more. Then I fixed the left end of the flag and tried to reduce the amount of waving that happens toward that side (it's attached to the pole, so it shouldn't wiggle up and down...). It looks more natural now, I think:

Still not perfect, but eh, that'll do for me. Next I just copy-pasted the same shape for the checkpoint and tried to work out the movement of the checkmark on the flag. This was a little trickier than the checkerboard pattern, but I guess I managed to make it work:

Flag Raising

With those new animations in place, I also wanted to make the checkpoint flags actually raise up from the floor instead of simply...instantly appearing out of thin air once you passed them. Here's how that looks:

This was pretty simple to implement; I just use LeanTween (the tweening library used in Rhythm Quest) to smoothly change the flag's y position. It's using an EaseOutQuad easing curve on it as well, so that it'll slow down as it reaches the top. (It's also drawn behind the rest of the ground layer)

Checkpoint Particles

That was all looking good, but the particle effect when you trigger a checkpoint still looked really ugly, just using big white squares:

I had the idea of coming up with some sort of colorful confetti effect instead and played around with some particle systems in Unity until I got something that looked nice:

The sprites here are just simple blobs of pixels with minimal shading, using fixed random colors (only 50% saturation to give them a more pastel look). The particles shoot upwards from an arc with high speed, but I'm using the "Limit Velocity over Lifetime" feature to quickly decelerate the confetti to a certain speed, which kills the upward momentum and lets gravity take effect. This also slows down the descent of the particles, so they can float down slowly instead of dropping like rocks. This limit is higher for the smaller point-like particles, since in real life they wouldn't be hampered by the air as much relative to their size. Finally, I add a random horizontal force, which causes their descent to vary side to side a bit.

When I drew the pixel blobs for these particles, I was envisioning the "z" shape of the particles used in Chapter 4 of Celeste. I only recently remembered that the entire confetti-checkpoint idea was also used in Celeste as well: that may have unconsciously inspired me. Great minds think alike...?

Anyways, here's how it all looks together now, in-game:

WebGL Port and Demo Build Considerations

Something else I worked on this week was porting the game to WebGL to get a working web (demo) build. Web isn't one of the final release platforms for Rhythm Quest, so this isn't something that's strictly necessary for me, but I'm well aware of the fact that web builds provide immensely less user friction when trying to get people to try out your game, so it's something that would be nice to have around. At the very least, playable web demos have worked really well for certain other rhythm games out there.

I'm not doing anything too crazy in terms of rendering, and I already know that my performance is working fine on mobile, so the main thing I was worried about was precise audio scheduling. I've done a rhythm game with latency calibration before, but never with such heavy use of the AudioSource.PlayScheduled function and trying to synchronize with audio DSP time.

Addressables and Resource Management

The first thing I wanted to do was to reorganize my release asset management. As usual, Unity has about 4 different ways to load assets, each with their own caveats (sigh)...but anyways, the "Addressables" System is Unity's most modern asset-loading solution and has some nice new functionalities in addition to trying to generally be more sensible and cleaner than what we had before.

I had already been using Addressables in my project to provide a nice clean way to load music, palettes, and level data, but I wanted to reorganize those assets into more cleanly-defined groups for a couple reasons.

One of those reasons was for demo builds -- I want the demo builds to only include a given subset of levels, so my build scripts should be able to for example exclude all of the assets for worlds past world 2 when making a demo build. Technically, I could just include the entire full game in the demo build and then just lock out the later worlds via code, but that has a number of issues: it increases the build size needlessly, it allows for scraping the unused content, and it may even allow for loading those levels via something like a memory scanner/debugger.

Another reason is for patch size considerations. Both Steam and Nintendo will intelligently parse out the deltas between different versions of your builds when doing post-release updates and only update files which have changed in order to require smaller downloads for patches, but obviously this doesn't really work if all of your assets are just in a single massive blob. The Addressables system allows you to chunk apart your assets in a consistent manner. It even contains some static analysis tools for detecting assets duplicated across multiple places, which is nice.

So I did all that, only to realize something terrible: synchronous Addressable loading isn't supported on WebGL...

Synchronous Addressable Loads

See, Addressables by default are loaded asynchronously, so you could do something like this using a coroutine (or even use async/await):

IEnumerator SomeCoroutine() {
    // Kick off the Addressables load operation
    var asyncHandler = Addressables.LoadAssetAsync(pathToAudioClip);
    // Wait for completion...
    yield return asyncHandler;
    // Do something with the result
    _myAudioSource.clip = asyncHandler.Result;

That's fine, but sometimes for simple things during initialization, it's easier to just use a blocking synchronous call. For example, loading a level's palette data or music when you jump into that level scene. It's more straightforward to just have those calls block instead of having to turn your entire initialization logic into an asynchonous flow and juggle all of the dependencies in a more complicated way. So you can do this instead:

void Start() {
    var asyncHandler = Addressables.LoadAssetAsync(pathToAudioClip);

    // Synchronous blocking call
    _myAudioSource.clip = asyncHandler.WaitForCompletion().Result;
    // (Rest of initialization)

The level's audio clip has to be loaded, for example, before the audio can be scheduled, and various other components assume that that's just done on the first frame of initialization without having to wait for it. Except, this doesn't work at all in Unity WebGL builds, probably due to threading being completely unsupported.

For the level audio, I managed to add in some stopgaps that keep most things paused until the audio can be loaded and the music scheduled and initialized properly, but for some other smaller files (level metadata, font mappings, etc), I actually just decided to "downgrade" and use the Resources.Load solution for loading those assets because it would be a pain to add asynchronous handling to my code for all those cases. Bleh.

Debugging WebGL Audio

So now my game loaded, but it was still pretty broken, as the audio scheduling seemed to be completely off. To make matters worse, audio clip playback...wasn't stopping. I'd schedule a sound with AudioSource.PlayScheduled(), then stop it with AudioSource.Stop(), except...the sound would just continue playing?

Turns out that any audio assets that are imported with the setting "Compressed in Memory" or "Streaming" will behave strangely and fail to stop playing in this way. That's fine, it's probably not a bad idea to load most of my sounds into memory anyways, aside from shop unlock previews -- and even then, those probably won't be in the WebGL build.

So that was working, but unfortunately audio scheduling itself still seemed to be broken. After adding some testing logic I determined that calling AudioSource.PlayScheduled() was simply starting the audio clip right away instead of doing any scheduling, despite PlayScheduled theoretically being supported in WebGL.

...scratch that, using PlayScheduled() actually seemed to work.......but only the very first time that the clip is played (and the audiosource's timing was wrong...). Past that, even if you try to unload the audio clip, completely recreate the AudioSource, etc., any future calls won't do the right thing anymore.

Turns out that this issue occurs even with the PlayDelayed() function (sheesh), so at this point I gave up and implemented my own simple audio scheduler that simply attempts to trigger audio clips on the appropriate frame. It's far from sample-accurate, but it's the best I can do at the moment and wasn't too much of a pain to conditionally wrap in for WebGL. I'll have to write up and submit an issue for this when I get a chance...

Finally, the last thing I had to fix was that mouse input callbacks were giving a completely bogus value for the time of the mouse click for some reason, so I read that from the current game time instead.

And with all of that debugging done, I finally have the game working in-browser! Behold:

There's still a bunch of kinks here and there, but for the most part, it's actually not too bad! I'm excited to be able to share the WebGL build with everyone someday once it's more polished up and ready for the light of day.

<< Back: Devlog 27 - Level 2-4, 2-5
>> Next: Devlog 29 - Level Select Rework