Devlog 6 - Spike/Teleport Enemies Demo

Published: June 25th, 2021

This week I continued mostly working on new mechanic prototyping. I've got a couple more new things to show off!


First up we've got these new teleporting ghost enemies:

Each one of these takes three hits to destroy and currently uses a dotted-quarter-note rhythm to provide some syncopation. I can potentially switch this rhythm per-level, but I think the three dotted-quarter-note hits probably work better than anything else so I don't really see a need to do that.

I wanted another attack-based mechanic and thought this might be a good one as it makes reading rhythms a bit less obvious. The double-hit enemies play around in a similar space, but this one is a bit more complicated as it can be interleaved with other obstacles. Part of the charm of making a platformer/rhythm hybrid is that you can make the rhythms harder to "translate" than with traditional music games, so your brain has to work a little more to parse them out. My other game Melody Muncher plays with this in the same way.

There's a neat little after-image "teleport" effect that's used between hits. Here's what that looks like slowed down:

Implementation-wise this is actually pretty simple. The enemy object moves at a fast rate towards the destination (actually, the hitboxes and main entity teleport there instantly; it's just the visuals that lag behind), and as it does so, it emits particles.

The yellow star particles are pretty obvious -- they just get created with a random outward velocity and fade out quickly. However, the "after-images" are also actually particles as well -- they just happen to not have any motion, so they just appear and then fade out while remaining in place. (and they of course are sized to be the same as the actual main sprite)

Intuitively, you could implement this by simply attaching a particle emitter to the enemy and then toggling it on and off. I actually didn't do that -- I chose instead to just have a global particle emitter that's shared across all of the enemies, and then each enemy just tells that global emitter to emit particles at the appropriate locations. This is most likely (?) better for performance, though it probably doesn't matter a ton here.

As a final touch you might notice that the main enemy sprite flashes as it travels. This is done very simply using a shader that inverts the color of the sprite. Something like this:

fixed4 frag(v2f IN) : SV_Target
{
    fixed4 texcol = SampleSpriteTexture (IN.texcoord) * IN.color;
    if (_Invert) {
        texcol.rgb = float3(1.0, 1.0, 1.0) - texcol.rgb;
    }

    // ...
    
    return texcol;
}

The "_Invert" flag here uses Unity's "[PerRendererData]" construct along with "MaterialPropertyBlock"s so that it can be modified per instance whilst still using a single shared material. Feel free to read up on those concepts elsewhere if that all sounds foreign to you.


Next up we have these fun little rolling spike enemies:

These were actually incredibly easy to code up. The entirety of the SpikeEnemy script looks like this:

public class SpikeEnemy : MonoBehaviour
{
    [NonSerialized]
    public float Beat; // This gets set during level generation.

    protected virtual void Update() {
        float currentBeat = MusicController.CurrentBeat();
        float x = LevelController.Stats.BeatToX(Beat + 0.5f * (Beat - currentBeat));
        float y = LevelController.Stats.XToGroundY.Get(x);
        transform.position = new Vector3(x, y, 0.0f);
    }
}

That's it! The actual interaction is done by simply affixing a "killzone" collider to it -- I already have logic setup so that any collider that is in the "Death" layer ends up triggering a respawn instantly on contact.

LevelController.Stats is the bookkeeping singleton that gets populated with all sorts of different lookup/conversion functions and tracking data during the level generation process. For example, converting between x-coordinates and music beats, or looking up the height of the "ground" at a given x-coordinate, or what the player's y-coordinate is supposed to be at a given x-coordinate.

The only real tricky part here is the calculation of the x-coordinate, which uses a funny Beat + 0.5f * (Beat - currentBeat) calculation. This is essentially just making it so that when currentBeat = Beat, we just use BeatToX(Beat) which places the enemy at the appropriate x-coordinate. And outside of that, the 0.5f * currentBeat factor means that the enemy will travel to the left at 50% the speed of the song. (100% would be too fast to react to!)


One other thing I showed off in the video is the seamless respawn procedure, which gives you a "rewind, cue in" when you restart a section, all to the beat:

I could easily write an entire article on how this is done (though I've forgotten about a lot of the details since I implemented it ~2 years ago), but essentially, the game is always playing a seamless background loop that's perfectly in sync with the main music track -- except it's muted during normal gameplay. When you respawn, we mute the normal track and unmute the background track. (Since they're both in sync, the beatmatching is preserved)

Then it's just a matter of calculating the next downbeat at which we can drop the player into the checkpoint. We then figure out the timing and scheduling for rewinding the music position to the appropriate point, reschedule audio playback, and set up the appropriate tweens. That's all super complicated to actually achieve in practice, but I'm going to just handwave it all away at the moment as I don't want to get into it right now. But it's all very slick when it comes together and really lets you stay in the action across respawns.


The last thing I wanted to point out today is more of a minor change. There's now a "cloud poof" animation for air jumps, as well as a quick _Invert flash, which is meant to serve as a more obvious/flashy visual confirmation for doing air jumps and flying:


Phew -- that's all for this week!

<< Back: Devlog 5 - Water/Air Jump Prototyping
>> Next: Devlog 7 - Build Pipelines