Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length

 
Advanced search

1398450 Posts in 67594 Topics- by 60944 Members - Latest Member: rashwinbarwa

January 28, 2022, 03:51:27 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsCommunityDevLogsRhythm Quest - Chiptune music-based runner
Pages: 1 [2]
Print
Author Topic: Rhythm Quest - Chiptune music-based runner  (Read 2908 times)
DDRKirby(ISQ)
Level 0
**



View Profile WWW
« Reply #20 on: October 22, 2021, 01:37:51 PM »

Palette Shaders

Just wanted to do a look at a small technical thing today, so here's a post about the shader(s) that are used to render the level graphics in Rhythm Quest.

Context

I've used limited color palettes in a lot of my previous games -- in fact, this is one of the defining characteristics of most pixel art. It's also great if you're very unexperienced with color (as I used to be) as it allows you to focus more on values, and establishes a consistent and pleasing style. I started this practice back in 2014 when I made Ripple Runner, which uses 4-color palettes:



Hue Shifting

In the past, videogame consoles would only allow for using a certain number of colors at once, so sometimes games would use "palette modulation" to change the colors used in the palette, to provide a different look for different sections of the game, or for transition effects. Nowadays, Game Boy-styled games will often provide a number of different 4-color palettes for use, each with their own look and feel:



I was inspired by this aspect of old pixel art, and wanted to create color shifts inside my games. However, modern rendering doesn't actually make use of color palettes internally (colors are usually just represented as RGB values), so instead I applied a rendering effect which simply shifts the hue of all of the colors. Here's that in action for Rhythm Quest:



This was actually a built-in effect back when I was using FlashPunk, so it was incredibly easy to put in. It was as simple as this:

Code:
// (These are built-in FlashPunk classes)
var adjustFx:AdjustFX = new AdjustFX();
var fxScreen:FXImage = new FXImage();

fxScreen.effects.add(adjustFx);
addGraphic(fxScreen, -9000);

adjustFx.hue = 90;

Somehow this type of functionality has gotten =harder= to implement over the years rather than easier, so now you have to actually write a custom shader to do it. =( Yay hardware acceleration...?

My first attempt at this (fragment/pixel) shader was a flexible shader that converted each pixel from the RGB color space to the HSV color space, performed whatever shifting was desired, then converted back to RGB. I found that the operations required to do all that were too expensive for some mobile GPUs, so I ended up going with a much simpler algorithm instead that deals with only the hue alone.

Problems with Hue Shifting

That all worked "more or less fine", and I could honestly probably ship with that, but there were a couple of things that I wanted to make better. Most notably, naively shifting hue doesn't lead to the best colors. This is because of two reasons.

First, hue shifts (in HSV/HSL space) can change perceived brightness. This is due to human perceptual differences between colors -- green seems much brighter than blue for example. These two rows of colors are identical aside from hue, but look at how much brighter the top row seems:



Secondly, good palettes are formed differently depending on their hues. As a general rule, colors look more natural when brighter colors are shifted towards yellow and darker colors are shifted towards blue/purple (this mirrors how many things appear in nature). Doing a straight hue-shift doesn't really take this into account, so you can get messed-up color relations:



Palette Shader

So instead of just starting with a set of colors and then shifting the hue, I want to actually handcraft multiple different color palettes and then swap between then. The brute-force approach to this would just be to make separate texture exports for each palette, then just crossfade between them, but that's a huge headache in multiple ways, so I definitely didn't want to do that.

Instead I made a fragment/pixel shader that takes the input colors and then clamps them to a given color palette. There's a couple of different ways you could speciy the different color palettes for the shader...for example, you could just have the shader take 4 different color values as inputs, and then modify those programatically whenever you want to switch the palette. However, I chose to be a little more clever and instead have all of the color information for an entire level specified in a "palette lookup texture":



Here, each row represents a different color palette. This texture is super small, and easy to edit using something like Aseprite. And, as another advantage, I can add as many rows or colmuns as I end up needing on a case-by-case basis. For example, I might want to expand beyond 4 colors, so I'd just add some additional columns to the texture.

For doing the palette mapping, I'm having the shader just take the average RGB value of the input pixel and then mapping it to each of the 4 output colors based on that. That might be a little awkward if I decide to start having more complicated palettes with lots of colors (I probably won't...), but for now this is fine. I can just have my source graphics be grayscale and then have the shader pull from that. Here's the fragment shader code:

Code:
fixed4 frag(v2f IN) : SV_Target
{
    // Normal texture sampling, to get the input pixel color.
    fixed4 texcol = SampleSpriteTexture(IN.texcoord);
   
    // Get the average RGB value (no need to worry about perceptual brightness here)
    // We use this as the "U" coordinate in the UV texture mapping.
    fixed u = (texcol.r + texcol.g + texcol.b) * 0.333;
   
    // Convert the palette index to a "V" coordinate.
    // (_PaletteTex_TexelSize is 1 divided by the texture height)
    // Add 0.5 so we're at the "middle" of each pixel, not the edge.
    float v = _PaletteTex_TexelSize.y * (_PaletteIndex + 0.5);
    // We need to invert since v = 0 is the bottom of the texture, not the top.
    v = 1.0 - v;

    // Sample from the palette to get the final resulting color.
    texcol.rgb = tex2D(_PaletteTex, half2(u, v));

    // Apply tint like normal (mostly for alpha effects).
    return texcol * IN.color;
}

The cool part about this is that if we set the texture to use bilinear filtering, we can transition between different palettes by using fractional palette indexes! Using a _PaletteIndex of 0.5, for example, will give us colors that are halfway between palette 0 and palette 1. Nifty! Here's the final result, working with the palette texture shown above:



Again, not a huge difference from what I originally had, but it's cleaner in the ways that I mentioned before, and allows me to experiment more with creative palettes, which will probably be important as I look towards giving everything a graphical makeover. (It also ought to be super lightweight in terms of performance)
Logged
DDRKirby(ISQ)
Level 0
**



View Profile WWW
« Reply #21 on: October 29, 2021, 05:35:25 PM »

Level Graphics Experiments

I didn't get a ton done this week, but the work I did get around to was mostly focused around revamping the level graphics, which is a still-ongoing task that I'm trying to tackle. This'll probably be a shorter post as I don't have a lot to show yet.

Palette Shader Note

Before I get into it, a few minor follow-up notes on last week's palette shader article. First, I realized that I had done the 0.5 pixel correction in the y (v) direction, but not the x (u) direction, so that needed to be added.

Secondly, I talked about how I was able to use bilinear filtering to transition between different palettes. That's working just fine for the levels, which go through a sequential series of palettes, but for the menu screen, I actually need to handle multiple different palette transitions depending on which menu you drill down into, so instead of relying on texture filtering I needed to actually blend between two different palette textures there. Just thought I'd mention it for completeness.

Spikes and Spike Walls

Part of the work I did this week was to consolidate all of the graphics for each world into a single spritesheet image as opposed to saving them as individual png files. I'm...not actually sure why I didn't do this in the first place, but it makes it much easier to edit the graphics for a world all at once rather than having to open a bunch of different individual files (and having to edit the palette of each one...). This also lets me create a sort of "template" spritesheet layout for each world, which hopefully will reduce the amount of boilerplate work.

As I was doing this I decided to make the spike graphics larger:



I honestly can't remember why the spikes were so small to begin with, but it may have had something to do with the fact that everything used to be tile-based and thus needed to line up nicely with a 20-pixel grid (?). That's no longer really an issue and I've worked it out so that the spikes can just be clipped off by the edges of jumps, it looks fine.

You'll also note that the vertical "spike walls" have been converted to normal spike pit jumps. The spike walls were something that were sort of a legacy carry-over; they're functionally equivalent to a normal jump, and they were only ever used in like one or two levels (and then I sort of forgot about them for the rest of the game). They're intuitive and present a kind of a fun sort of variety, but ultimately they seem unnecessary, so I'm cutting them for now in favor of simplicity and elegance.

You might also notice that the coins are gone. I talked about trying this out in an earlier post. The jury is still out on this one -- I sort of have to try it out in action to see how it feels to have them missing.

Palettes

Part of the whole goal of having the palette shader was to be able to do more interesting things with the color palettes for the levels. Here's an extreme example of a sort of synthwave/outrun-ish color scheme:



Something I really wanted to try was using palettes with more than four colors, to see if that would help the levels look better. Here's a rough attempt at that that I came up with:



It's certainly more...colorful than the monochromatic color themes (d'uh). But in my opinion there's a certain loss of cohesion in the image. The foreground characters also stand out a little less from the background since everything is a little more saturated. Of course, you could argue that this is due to me not choosing a nice palette, but I think that's sort of my point exactly -- sticking to the simplicity of the four-color palette makes it simpler for me to author a pleasing color scheme.

Much of the strength of chiptune music and pixel art arguably lies in adhering to restrictions that enforce a uniform style. I'm sure that if I was a more talented artist, I could come up with artwork that takes advantage of more than four colors. You know, something like this:



But part of being a solo gamedev is playing to your strengths. That said, I think this experimentation was still worthwhile, and I'll probably still keep the foreground and background elements as using different sets of four colors (possibly only during certain sections of the song?). But limiting each individual part of the level to four colors seems to work well. Fortunately, even if I decide to later break this rule, my palette shader is flexible enough that it should support that just fine.

Grass

I figured that grass might be interesting to try out for the first level(s) (sticking to the generic "first level = grass level" videogame trope), so here's my attempt at that:



It's a bit noisy, but overall I don't hate it. I've added two different layers of grass -- one behind the characters and one in front of them, which sort of gives it a subtle (barely noticable) feeling of depth. Unfortunately, drawing grass like this on sloped surfaces is kind of a pain in the butt to get right, so that's something I still need to work on for next week. Each world will probably end up having unique graphical stylings like this, so I'll have to play around with it as I go through them.

Particles

This was an obvious one that I wanted to try, as particles tend to be a nice way to add some additional motion to scenes to make them feel less static. Still needs more iteration, and these will probably end up being different per world, but here's the first stab at it with a generic leaf sprite:



It already looks pretty nice! Funnily enough, I actually just copied the leaf sprite over from my previous game Melody Muncher, and the idea of using particle effects to accentuate the high points of the song is probably going to be reused as well. Hooray for building up upon previous work!

That's it for this week! A lot of this work was more on the experimental side, and I wasn't sure at the beginning what would end up working or not, but I think I've got a better idea of where to go from here now. Sometimes you definitely run into these sort of phases where you just need to go through and try some different things, even if it results in some amount of wasted work.
Logged
DDRKirby(ISQ)
Level 0
**



View Profile WWW
« Reply #22 on: November 05, 2021, 10:06:18 AM »

Coin Collection System

I mentioned last week that I was still trying to figure out what to do about the coins that I've had floating in the air in the middle of jumps. These things:



I liked having these coins around since they provided visual interest (they're animated, which is nice) as well as a simple way to visualize the path that the player should take across the gap. To a lesser extent, they also serve as a way to visually show the timing of the midpoint of each jump -- which becomes important in later levels when instead of a coin you have an additional mid-air jump or flight path.

The Issue

However it was a bit weird that the coins didn't actually serve any gameplay purpose and didn't actually serve as currency. They weren't really set up to be treated as such, either, as there wasn't really any way to "miss" a coin and the number of coins per stage was pretty arbitrary (the number of jumps that didn't happen to have any other obstacles in the middle of them). But I didn't want players to just be left with a lingering thought of "I got all these coins, how do I use them...?".

At the same time, I was also thinking about the level clear UI, which currently just shows the number of times you died during a level. That's working just fine, and sort of serves to suggest the goal of doing perfect clears (which reward you with special medals). But I thought it was kind of weird how when you finally clear a level, the first thing you see is the amount of times you =failed=. That's a feel-good moment if you did it perfectly, but sort of a negative emotional feeling if you happened to die a bunch.

I was wondering if I could show something more positive, like a combo count or some sort of score. At the same time, I've been pretty set on not introducing scoring mechanics to the game, as I want to keep away from any extraneous gameplay "baggage" to allow players to just focus on the gameplay and music.

Two Birds with One Stone

When you have questions or problems like these in game design, sometimes you just have to make a list of things to try out and then see what sticks as you iterate on the possibilities. Other times you need to just need to keep the problem around in the back of your head long enough and then come back to it later once you've had a breakthrough or see a new solution.

This particular pair of problems fell more into the latter camp. I had both of these problems in my mind at some point, and it occurred to me that I could try to tackle both of them at the same time.

One of the problems with using the coins as currency was that they were only tied to empty jumps and not to anything else. Well, I can change that by having enemies also give you coins when you attack them:



So now the number of coins in a stage is more directly tied to the difficulty of that stage. But it still doesn't really make sense to use as a currency because you can't "miss" coins -- going through the entire stage means you've collected all the coins by design. What if we make the player lose a portion of their coins every time they respawn?



Here I'm using a particle emitter attached to the player, with the collision module set to "world" collision, so that the coins collide with the surfaces of the level (which are on their own collision layer). I've also disabled the white flash on respawn so that you can see the coins spilling out, and I've added a flickering effect on the player to represent that you're respawning. To be honest, I always thought that the flash was a little harsh to begin with, but I kept it in because I needed something to cover up the fact that all of the defeated enemies pop back into existence. (I'll probably need to fade them back in gradually instead now...)

Okay, so now you can only get all of the coins in the level if you do a perfect clear. That means I can use the coin count as a score to show at the end of the level:



This still isn't quite making sense though, because all of the obstacles reset every time you respawn. So if you die, you lose coins, but then you collect some back as you replay that section...? That's weird. So instead let's make it so that the enemies respawn, but the coins don't. That means you have one shot at each coin, and it's your job to try to hang onto as many of them as possible.

...aaand that's where I'm at right now. There's still some kinks to iron out...for example, the actual number of coins that you lose on respawn is something that I'm still iterating on. And, right now the total number of coins that you're carrying is never actually shown until the end of the level. I could of course just add a numeric UI counter, but again, I'm trying to keep the game screen clean of clutter and distraction. And then I need to deal with how to display the total amount of currency that you've accumulated in the menu screens, ugh...

All of this maybe seems very logical in hindsight, but honestly when I started thinking about this I had no idea what I was going to do with it, and it wasn't obvious to me what the way forward was. Hopefully this example illustrates a little bit of how these systems get designed. Again, a lot of it (for me) tends to be about just keeping random issues in the back of my head and then dealing with them once I figure out something I want to try and iterate on.
Logged
DDRKirby(ISQ)
Level 0
**



View Profile WWW
« Reply #23 on: November 19, 2021, 02:45:51 PM »

Character Unlocks

Took a week off last week, but I'm back in action for this week. The holiday/end-of-year season is coming up, which means I might be working a bit less than usual, but we'll see if I can keep up some steady progress regardless.

Last time I covered the new system for collecting coins across levels. I tried having this coexist with the medal system for a bit, but in the end decided to drop the medals altogether for simplicity so that there's only a single metric for level scoring; the UI was looking too inelegant and messy otherwise.



New Characters

Now that I have that system in place, I can start actually adding unlockable content to purchase with those coins, such as new characters!



Just a rough draft of the animations (no shading yet), but you get the idea! There aren't any gameplay differences between different characters; it's purely cosmetic at the moment.

Shop Button

Of course, I also needed to implement an entirely new UI flow to allow you to purchase the new characters in the first place. As with many things in game development, this is something that seems extremely straightforward conceptually but actually involves a lot of different moving parts.

First we have to add a new button somewhere to allow you to access the shop/unlocks menu:



Seems straightforward enough, but in doing this I had to remove the "Quit" button at the bottom of the main menu -- it was beginning to feel a bit too cluttered otherwise. I ended up changing it to an "X" button at the top-right of the main menu. It's not the prettiest, but I like it better than having 5 buttons in the main menu. Of course, this button doesn't show up on any of the mobile builds.

Simple enough, right? Well, no, not really. The X looks really weird if it scrolls to the left with the rest of the other buttons:



So instead I made it fade in and out instead when you transition in and out of the main menu. Simple enough, right? Well, no, not really. It looks nicer if you line up the fade to sync up with the beginning/end of the transition (depending on whether you're navigating to or away from the main menu). And of course...the transition length is variable depending on the position of the music. So I had to add a little bit of extra logic there to expose the transition duration and sync everything up. These are the types of little things that nobody on the outside ever thinks about when a new feature needs to get implemented...

Coin Display

Currently coins act as "high scores", so they aren't farmable currency (you can only get up to a certain maximum number of coins each level). I wanted to make sure that there was some sort of UI in the shop explaining how many coins you've collected in total across all levels (your total high score), but also showing how many coins you have left to spend. The system definitely isn't 100% intuitive, so I wanted to make sure to give a visual aid for understanding. Right now this is a static box that remains visible at the bottom throughout all of the shop flows (I got to reuse the same fading logic that I had for the X button).



These numbers aren't too hard to show -- the total collected one is just the sum of all your level scores, and then I dynamically calculate the second one based on the sum of the prices of all of the unlocks that you've purchased (I had to set up new data structures for all of that...).

Of course, it's never quite that simple. I wanted to have the numbers animate, ticker-style, whenever you made a purchase, so I needed to track the currently-displayed number separately from the actual value and then have it track accordingly. (Little details...) I also wanted some sort of confirmation screen since there are no "take-backs" on purchases.

Final Flow

And with all that, we finally have a flow for unlocking a new character!



Actually swapping in the new character wasn't too bad! I have a static mapping between string keys (corresponding to each character) and different Animator Controllers. This is stored in a ScriptableObject and then referenced at runtime to load in / switch out to the appropriate set of animations.

Next week I'm going to be trying to create the flow for unlocking extra bonus stages via the shop, so it'll probably be more of the same stuff (but probably more complicated). The build is actually still in a pretty shoddy state as I haven't updated most of the levels in accordance with all the visual/shader updates I made a while back, but that can wait until later.
Logged
DDRKirby(ISQ)
Level 0
**



View Profile WWW
« Reply #24 on: December 03, 2021, 01:42:23 PM »

Extra Stage Unlocks

This particular post is going to be more oriented towards coding rather than game design. I did some work on the menu flow for unlocking extra levels this week. Here's what that looks like right now:





This is pretty analogous to the flow for unlocking characters, except with some more complicated logic for handling the song previews. I don't (yet?) have song previews for the main levels, but I figured it would be nice to have for the unlockable bonus levels because you're actually spending in-game currency on them -- song titles alone don't really help that much when deciding which songs to buy. (I may end up displaying some sort of difficulty rating as well, but right now I haven't even charted any of these songs, so that's not relevant yet) As always, the exact UI isn't really locked in yet, it's just a rough draft...

Song Previews

This is the kind of engineering task that is pretty nontrivial yet will probably never show up on any sort of programming interview or anything... In my head this is something that could easily snowball into a pile of weird edge conditions if not handled robustly. In other words, this is the exact sort of thing that gamedev programming is made of...

First, let's outline how things =should= work:

    You can press any of the preview buttons, at any time (except during transitions)
    The music preview should loop, with a fade at the beginning/end
    The menu music should fade in/out as the music preview starts/stops
    Pressing the same preview button again should stop the music
    Pressing a different button should stop that song and start the new one
    The preview should automatically stop if you exit that menu

Some edge cases and other issues might already be jumping out at you. For example, how do we load and play the preview clips without having to load all of the audio into memory (causes the initial scene load to slow down), and without any sort of stutter when firing off a new one?

Ideally you could just specify time segments out of the original audio clips and use those as your previews (adding the volume fades programatically). You could use Unity's new Addressables.LoadAssetAsync functionality, which should allow us to load them in on-demand asynchronously, so that there's no stuttering or hitching. ...except, there's actually still a stutter because the disk load operation blocks the main thread. You could just load all of the clips into memory at the beginning of the scene, but I believe that bogs down scene loading (for similar reasons). I ended up just creating separate preview clips that are set up for streaming (this is also covered in https://exceed7.com/native-audio/rhythm-game-crash-course/import-settings.html), since Unity can't use the same audio clip data for streaming + non-streaming usage. I can then just bake in the beginning/end audio fades for each clip as well, so playing them is a simple matter of looping the entire audio clip. (I do still use the Addressables API to load these separate streaming clips on-demand by name)

With that all taken care of, think about how you would handle state management for playing and stopping the audio.

One possible approach would be something like this:

Code:
enum AudioState {
    Stopped,
    FadingIn
    Playing,
    FadingOut,
}

AudioSource _audioSource;

AudioState _currentState = AudioState.Stopped;

string _currentClip = "";

Then when a preview button is clicked, you need to check the current state and do the appropriate thing...

Code:
void PreviewButtonClicked(string clipName) {
    if (_currentClip != clipName) {
        // We need to transition to the new clip...
       
        // Start loading the new clip asynchronously (?)
        AsyncOperationHandle _asyncHandle = Addressables.LoadAssetAsync(clipName);

        if (_currentState == AudioState.Playing) {
            // Start fading out the current clip...
            _currentState = AudioState.FadingOut;
            BeginFadeAudioSource(_audioSource, 0.0f);

            // Maybe use a coroutine to wait until the volume is 0??
            // What if the preview button gets pressed again in the meantime??
            ...
        } else if (_currentState == AudioState.FadingOut) {
            ...
        }
    }
    ...
}

This can get messy really fast since you have to handle so much bookkeeping. Instead I actually just did away with all of the intermediary state. I can use the current volume of the audio source itself to determine whether I need to fade in / fade out / switch clips. The only other state we need to keep around is the AsyncOperationHandle.

Code:
AudioSource _audioSource;

string _targetClip = "";

AsyncOperationHandle _asyncHandle;

// The button click handler changes _targetClip, but does nothing else.
void PreviewButtonClicked(string clipName) {
    if (_targetClip == clipName) {
        // Stop playback.
        _targetClip = "";
    } else {
        _targetClip = clipName;
    }
}

// Frame-based logic determines at each frame what needs to be done
// with the audio source to match _targetClip.
void Update() {
    if (_targetClip == "") {
        // We don't want to play anything.  Fade volume towards 0.
        _audioSource.volume = Mathf.MoveTowards(_audioSource.volume, 0.0f, Time.unscaledDeltaTime);
    } else if (_audioSource.clip.name != clipName) {
        // We're playing the wrong clip.  Fade volume toward 0.
        _audioSource.volume = Mathf.MoveTowards(_audioSource.volume, 0.0f, Time.unscaledDeltaTime);
       
        // Start loading the new clip, if we haven't already.
        if (!_asyncHandle.IsValid()) {
            _asyncHandle = Addressables.LoadAssetAsync(_targetClip);
        }
       
        if (_audioSource.volume == 0.0f && _asyncHandle.IsDone) {
            // New clip is loaded and fading is done.
            // Replace the clip and play.
            _audioSource.clip = _asyncHandle.Result;
            _audioSource.volume = 1.0f;
            _audioSource.Play();
           
            // (This is a struct, not a class)
            _asyncHandle = new AsyncOperationHandle();
        }
    }
}

As always with code examples, I'm handwaving away some details, but hopefully this is illustrative of how this pattern of keeping less intermediary state can help simplify logic. The basic idea is to "recalculate from scratch" what should be done each frame based on the current state of the world rather than maintaining some sort of internal finite state machine. There's often a tradeoff involved here between maintaining less state vs being more performant, but in many simpler cases it's totaly fine to do these sorts of recomputations.
Logged
DDRKirby(ISQ)
Level 0
**



View Profile WWW
« Reply #25 on: December 10, 2021, 02:47:50 PM »

Speed Zones

Water Zones

Half a year ago (has it already been that long??) I showcased a water mechanic that slowed down the scroll speed and introduced triplet rhythms:



Thematically this works great. I worked out a nice visual representation for it which is probably fairly intuitive as well, and a "water" theme can work well musically without too much trouble.

The problem is that the mechanic doesn't function well gameplay-wise. It's a nice changeup in the rhythm of the song, but since the scrolling speed decreases, it makes rhythms harder to read. Overall it's interesting but not exciting.

Speed Zones

Enter the reworked mechanic -- speed zones:





Instead of reducing the player's speed, I'm now increasing it. The beat grid markers are still spaced the same width apart, but correspond to quarter note triplets instead of quarter notes, so the "beat" is 50% faster.

I have to be careful about increasing the scroll rate =too= much with this, but I'm happier with this mechanic since it creates little bursts of higher energy within the song, forcing you to read and execute rhythms more quickly. Here I've only used basic rhythms inside the speed zones, but I could even insert double-hit enemies and flying enemies if I wanted to really go crazy.

I also stumbled upon my musical theme for this world while playing around with ideas for this mechanic: pentatonic scales! I'm excited to flesh out this world later on with music written with this theme in mind.

Implementation-wise, the cleanest way to get this working would have been to change the tempo or length of a beat during that section. Unfortunately, my code really isn't set up for that sort of thing, so instead I ended up keeping the BPM/beat length the same, and instead increasing the scroll rate. This means that I needed to put the obstacles and beat grid markers on weird fractional beats, like 0.6666, 1.3333...which is a little awkward. I feel like these kinds of "hacks" are more invaluable than people think sometimes -- it's important to see your new mechanic in action as quickly as possible, even if the implementation isn't always ideal. If it ends up causing problems for you, you can always reimplement it once you're sold on the design...

Visual Representation

One of the nice things about this mechanic is that it doesn't require an icon-based tutorial -- once you run into it, it's pretty obvious how it works. Even if you don't conceptually understand triplet rhythms, listening to the music should make it obvious what the new timing is within the zones.

I initially started off with a tinted visual area similar to the water zones, but that just ended up looking like water itself, which didn't make sense (why is it speeding you up?). Then I thought about using an obvious platformer trope -- conveyor belts:



Okay, maybe not literal conveyor belts; I'm using arrows instead to represnt the direction of motion. The animation here is actually pretty simple; it's done by just cycling through this sequence of frames:



This is then tiled horizontally across arbitrary widths using Unity's SpriteRenderer component. I had to write the logic for figuring out exactly where to add the speed floor tracks within the level, but that wasn't too hard. Replacing the beat grid markers with red-colored versions was pretty simple as well (I adjusted their look slightly to fit better with the conveyor track).

I then added a particle system, using a vertical plane to kill off particles as they hit the end of the zone. The particles themselves are actually just single pixels -- I'm using Unity's particle trail system to automatically give them fading trails of variable length.

As a final touch, the red tint glows yellow and pulses slightly while you're inside the speed zone. Hopefully this makes it look more like a "field of energy" that you're running through, and less like just an awkward pair of vertical lines.

Remaining Issues

The astute among you will probably already have an important question on their minds: What about sloped ramps? Yeah, that...doesn't work yet, it would actually be a huge headache to try and implement. In theory I could add separate sloped animations for each different ramp steepness (there are 5 right now), as well as both ramp directions (up vs down, I can't just flip the sprite horizontally because the arrow would be backwards). That part is easy enough...the messy part is that there's no easy way to automatically tile this across slopes, so I'd have to construct each ramp segment-by-segment, AND trim off the last part since the total length won't be evenly-divisible. Getting the arrows to line up across different slope heights would be a nightmare, but come to think of it...there should always be beat grids between slop changes, so maybe that's a non-issue.

In the end I actually ended up deciding that if I had to, it was acceptable to just put down a restriction to simply disallow slopes across speed zones altogether. The speed zones weren't going to be super long, anyhow, and I probably don't want to make parsing them too difficult. The conveyor belt idea seemed too sensible to discard, even if I had to make a compromise in order to get it to work.

I did manage to put grass onto slopes in the end with a similar amount of work, so maybe I'll be able to get sloped conveyors working after all if I try. We'll see!
Logged
DDRKirby(ISQ)
Level 0
**



View Profile WWW
« Reply #26 on: January 14, 2022, 08:16:07 AM »

Level Graphics Revamp

Screenshot time! I'm back from a long break over the holidays, and I've been doing a ton of work on level graphics for this first week back in action. I've got a lot of juicy gifs to show off as a result!

I mentioned in a previous devlog that I had revamped the rendering of the level graphics to use a new palette shader. I've been working on other things, so that has actually gone mostly unutilized until now (more accurately, it made most of the levels graphically broken...), but I finally jumped in and started working out the color palettes for each existing level. I've also been meaning to draw unique backdrops for each level as well as come up with some different particle effects for accentuating chorus sections. Here's a showcase of what I have going on so far.



Here's level 1-2 in action. You'll notice that most of the background graphics throughout are very simple, using large brushes, simple shapes and not a lot of detail (these "clouds" are just large blobs drawn with a circle brush). This is mostly because it happens to be way easier for me to do, but it also serves well functionally as to not distract from the foreground as much.

I've been learning a lot this week about how to effectively utilize the color palettes to highlight different points in the music. Here, the colors shift to become more vibrant (saturated) and brighter here at the chorus checkpoint, but something else you may not notice at first is that the number of colors also increases. Before the chorus, the background and foreground both share the same 4-color palette, but at the high point of the song, the background and foreground each get their own separate palette, for a total of 8 colors. This helps bring an extra level of visual interest to the scene at that moment, so I've been using this as a consistent pattern throughout all of the levels.

As an aside, the main reason I wanted to implement the palette shader by using a lookup texture (as opposed to simply inputting numeric color values) was that I really wanted to be able to edit my palettes visually rather than by typing in numbers. Here's the palette for level 2-3, as an example of what I've been working with:



The leftmost 1 column and rightmost 2 columns are currently reserved for black and white, but other than that, you can see on the left half the 4 colors used for the foreground, and on the right side the 4 colors used for the background. You'll notice that these are the same until the bottom part of the texture, which corresponds to the chorus -- this is where the two palettes diverge and we get 8 different colors. Being able to edit my palettes like this not only lets me use my handy-dandy HSV color picker, but also serves as a nice "color graph" of the level at a glance.



Level 1-3 doesn't really have anything too special going on, aside from the special water shader, which I added in ages ago. A lot of these backgrounds are inspired by Game Boy games -- which, of course, also make use of a 4-color palette. I actually ended up going with less saturated colors in the background for the chorus here, which leads to the foreground being more in focus. Perhaps the oppposite of what you might expect, but I think it still works out.



The backgrounds in level 1-4 are also inspired by some backdrops from a Game Boy game (Kirby's Dream Land 2). I had to do a bit of experimenting to come up with a texture for the "trees" that looked right but wasn't too visually distracting.

Again, notice that the color set used for the foreground is slightly different than the background (especially the yellow highlights). Of course, all 8 colors still need to work cohesively with each other. It's definitely been an interesting discovery process playing around with the freedom of using 8 colors. There's also a surprising amount that I can play around with for how to distribute the 4 colors across the different backdrop layers -- I remember having to play around with that a bit to get it looking right. I think the parallax scrolling effect really stands out in this particular level due to the complex layering.



Here's level 2-1. I should mention here that each world now has its own tileset for the ground / ramps / jumps! I decided to see how world 2 would look with no "solid" fill for the ground, and was pretty much sold on that instantly, as it really helps to fit the "cloud/flying" aesthetic of the world.

Something else that I discovered while working on this level was that I could have a little bit of vertical parallax scrolling in addition to the horizontal scrolling. You might notice it if you watch how the vertical positions of the cloud layers shift relative to each other. I think this helped add another layer of depth to the otherwise-simple background. I also really like how the "falling petals" particles turned out here.



Level 2-2's background is perhaps a bit simplistic, but the cool part here is how the edge of the water has this shimmering effect. There's no lighting or anything going on here, it's literally just two different patterns of coloration that scroll at different rates and then blend together. This is an age-old trick that I used way back in Ripple Runner. Unfortunately the reflection of the sun on the water is static right now, but maybe that'll change later?

I've had to keep readability in mind while designing all of these backdrops. Notice that the central horizontal band (where most of the gameplay elements are) is pretty plain here. It also helps to keep most of the primary background colors desaturated, as that lets the foreground stand out more. I wrote a blog post a while back giving some examples of how background saturation can really affect scene readability, so it's important to check and tone down if necessary.



Level 2-3 is focused on combinations of air jumps and flying, so as the level progresses you go up and up. This created a really cool opportunity for me to create a level backdrop that slowly scrolls vertically over time as you go through the level, starting with a nighttime cityscape, going up through the clouds, and ending with a starry sky.

There's this fantastic moment where the moon scrolls into view right as you enter the chorus and I'm really proud of how this ends up working. It almost seems like I designed the music and gameplay around this concept, but really it was the other way around, haha. If you look closely, you'll see some of the stars shimmering in and out of view -- this is the particle effect for this level! It's very subdued, but the other things I tried detracted from the main focus of the moon in the starry sky, so this is what I went with instead.



World 3 is the "retro" world, so for level 3-1 I'm trying to evoke old game console imagery with the simple shapes and bright outlines against dark colors. I'm also starting to experiment with having some of the backdrop layers dip into the 4-color foreground palette, which again serves to increase the level of visual interest at the chorus point.

The particle effect here is dead simple, just some dots flying through the air. This is a callout to games like Mega Man 2 which used this "starfield" effect. There's actually two different particle systems running here: one is used for larger dots in the foreground, and the other one handles smaller ones in the back which travel less quickly. Just another way to add the illusion of depth to the 2D scene!


That's all I've got for now! It's only been a week but the game is looking wayyy better now thanks to all the visual work. Things like this are always super satisfying to work on since you can really see/feel the game coming to life as you add them in. I'm almost done working through the graphics for all of the existing levels, so after that I'll probably move onto something else like more character animations.

I'd like to re-emphasize how simple the actual spritework is in all of these backdrops (the last one is literally just a bunch of triangles/zigzag lines...). Most of the appeal comes from good use of layering, as well as careful color selection. Perhaps this can serve as a bit of inspiration to anyone who thinks that they can't make something look good because they're not confident with their artwork. Sometimes, keeping it simple is more than enough!
Logged
DDRKirby(ISQ)
Level 0
**



View Profile WWW
« Reply #27 on: January 21, 2022, 03:49:02 PM »

Character Animations

This week I did the backdrops and palettes for level 3-2, took care of some bugfixes, and also worked on some character animations!

I've finished the animations for ducky:



And you can also now play as Furball!



I thought it might be fun to look at some of these animations in a little more detail. I'm by no means very experienced with animation, but maybe it'll be somewhat illustrative to look at anyways.

Ducky

Having recently had pet Pekin ducks in my life, I had a lot of reference photos and videos on hand to look at when drawing ducky...



The proportions aren't entirely accurate to life, though -- I went with more of a chibi-styled look where the head is drawn a bit larger, otherwise it wouldn't really show nicely in a sprite of this size. With pixel art it often helps to first decide how the smallest important features -- the eyes -- are going to be drawn, since at this size, even one pixel can change the way it reads entirely.

For the run cycle, I really wanted to accentuate two things -- the characteristic head-bobbing of bird movement, and the side-to-side duck waddle motion. This is where thinking about body physics/kinesthetics can help (or just referencing videos). For example, ducky's head goes forward when taking a step, to help maintain the right center of balance. There's some sub-pixel motion involved here, especially with the front of ducky's belly.

The shading in the feathers is fairly light here and was only put in after I tested the main silhouette and liked how it looked in motion. I did this by deciding on a few main "lines" to follow to indicate shading of the feathers, then essentially drew those freehand over each frame. A little bit of inconsistency is actually important here to portray motion, otherwise it just looks like a weird decal shape that's shifting around.



I wanted to have minimal wind-up / anticipation frames for the attack animations, since it's important that they feel very responsive (in-game, the enemy actually gets hit immediately, without waiting for the animation to reach the "attack" point). So there's actually only a single "in-between" frame here between the normal running pose and the fully extended attacking pose. Because of this, I'm using smearing on that frame in order to convey the path of the attack motion better.

For pretty much all of the attack animations, it looks way better if some sort of full body motion is involved to give some sense of weight to the attack. Here, ducky lunges with their whole body, and I'm using a sort of elastic stretch to enhance the impact.



Ducky's jump animation is the same as the run animation, but without the head bobbing. This is played twice as fast as the running animation, which is really cute; it looks like ducky is paddling their feet in the air, which is actually what can happen if you lift a ducky up in real life...



The flying animation is just the same thing, with the wings overlaid. Although I'm reusing the same green wing graphic, I can't just overlay a shared wing animation on top of each character, because I need to manually position and adjust the wings to line up with wherever makes sense on the character.



This is doubly true for the flying attack animation. Notice how the wings move along with the rest of the body as ducky stretches horizontally. I'm also choosing specific frames of the wing animation to match with the attack animation frames instead of just letting it play out independently.

Furball

Furball was originally drawn by my friend for Pet Furball and Hyper Furball:





Building the run animation was mostly a matter of just translating that to pixels, but I also had to figure out how to get the stretch and squash to look natural, which involves some subpixel shenanigans. It's tempting to just drag an entire rectangle of pixels and then shift it, and that's indeed what I did to start, but you have to manually adjust things from there otherwise it just looks very weird.

My first attempt at Furball's attacking animation really fell flat:



I drew this one based on the graphics from Hyper Furball, but as soon as I put it in the game it was obvious that it lacked the right sort of movement and follow-through. Here's the newer one:



I didn't do as great of a job of it as I did with ducky's animation, but again notice the attempt to introduce some body movement, both with the little hop, and with the body turning slightly from left to right. Really big smear arcs work well for quick slashing motions like this, I think; you see it all the time in other games for sword slashes. It's important that the smears have some contrasting colors (dark blue against white, in this case), otherwise they can fail to show up against certain backgrounds.



Furball's jump animation is really fun as it's a spinning somersault jump. This is actually just two unique frames, which I then copied and rotated in increments of 90 degree angles to form the rest of the frames. You might be asking why I didn't just use one frame and rotate it in increments of 45 degrees, and that's because pixel art genrally doesn't rotate cleanly across those angles.

When tweaking these frames I learned that since they go by so quickly, I needed to de-emphasize smaller details since they could just lead to weird-looking afterimages when the animation is played at full-speed. I tried to lighten some of the lines around Furball's tail and smear out the body fur patterns a bit more as a result.



I originally tried having Furball's flying animation be the same as his walk/run animation, but that ended up looking really weird, as the sretch/squash didn't make sense and his tail just sort of floated horizontally in the air awkwardly. The new version tries to have the motion work together with the wing flaps a little better.

That's it for this week! I may try to work on one or two more additional characters next week, so stay tuned for more updates!
Logged
bayersglassey
Level 0
***



View Profile
« Reply #28 on: January 21, 2022, 09:17:44 PM »

I like the descriptions of what's going on technically... I don't play or make music-centric games, so it's cool to see what kinds of problems are involved with that and how one might think through them, without having to do it myself. Smiley

I also like how the main character seems to hint towards a medieval theme, but the unlockable characters are just cute & whimsical. The duck is really well animated...
Logged
Pages: 1 [2]
Print
Jump to:  

Theme orange-lt created by panic