Larold’s Jubilant Junkyard” has become “Larold’s Retro Gameyard“. I’ve been working on this re-branding and migration for a while. All old tutorials should redirect you to this new site. If you see any errors, please let me know! See here for more information: What happened to Larold’s Jubilant Junkyard?

Smooth Movement using Sub-Pixel Resolution

Smooth Movement using Sub-Pixel Resolution

Movement is an important part of video games. It helps bring your game to life. Smooth character movement gives players a good sense of control. On the other hand, poor character movement can make a game unnecessarily difficult. In addition, poor character movement may make your game completely unplayable for some players.

Movement is an important part of video games. It helps bring your game to life. Smooth character movement gives players a good sense of control. On the other hand, poor character movement can make a game unnecessarily difficult. In addition, poor character movement may make your game completely unplayable for some players.

This tutorial features an GBDK-2020 gameboy example later. The source code for that tutorial can be found on Github. See my How to make Games for Gameboy tutorial series for more info.

The Modern Solution

In modern engines, like GodotUnity, or Unreal Engine smooth movement is achieved using floating-point numbers. A floating-point number (Also called a “Float”) is a number with a decimal place.

Integer1
Floating-Point Number1.2

With floating-point numbers, variables can change in smaller increments compared to normal integers. For integers, you can increase/decrease by no less than 1.

Integers12345678910

Example of increasing Integers.

For floating-point numbers, they can increase by much smaller amounts. For example:

Floating-Point Numbers1.11.21.31.41.51.61.71.81.92.0

Example of Floating-Point numbers increasing by .1. Smaller values could be used even!

The Retro Solutions

However, when developing for some retro game consoles (like The Gameboy), floating-point numbers are not a efficient. We can only efficiently utilize Integers. Each integer will represent one pixel, and We cannot handle for the (sub-pixel) decimal values between two pixels. The resolution for this problem, called Sub-Pixel Resolution, uses “Scaled Integers”. Scaled Integers is commonly and generally called “Fixed-Point Arithmetic“.

Conceptually, each variable will have a “face” value and a “true” value. The “face” value is the variable’s as-is. The “true” value, simply put, is the “face” value divided by a consistent integer value.

Basic Example

Let’s imagine we have a variable called “playerX“. “playerX” is where we want to draw the player at. In this example, we are going to use 16 as our scale amount.

If the “true” value of “playerX” is 1.6, then the face value for “playerX will be 25 (remainder is discarded). When we want to move the player, we can increase/decrease “playerX” freely. However, when drawing our player, we need to divide by our scale amount: 16.

One thing to note is that this isn’t perfect. 25/16 (our face value divided by our scale amount) equals 1.5625. However because the decimal remainder is discarded, Drawing the player at 25/16 will result in the player essentially being drawn at 1. Here’s a table for visualization

True Value11.06251.1251.8751.251.31251.3751.4751.5
Integer Face Value161718192021222324
Integer Draw Location
(Remainder Discarded)
111111111

Comparing “Face” Value, “True” Value, and where things are drawn.

Bit Shifting & Powers of 2

hanks to Toxa for help on this section. (Check out his gameboy game Color Lines DX, published by Ferrante Crafts)

On retro consoles, division by non “powers of 2” can be slow. So, with scaled integers, always use a power of 2 as our scale amount.

In addition, rather than dividing by that scale amount, we should perform bit shifting. Bit shifting is the same as multiplying/dividing by powers of 2, except its faster.

For bit-shifting, we’ll shift whichever “power of 2” our scale amount is. If our scale amount is 16, we’ll shift by 4. Since 16 is 24 ( 2 “to the fourth power”). Here’s table for visualization. (Although it’s literally just a table showing powers of 2)

Scale AmountBit Shift Amount
21
42
83
164
325
646
1287

Powers of 2

Here’s a visualization of Sub-Pixel Resolution, complimentary of @bbbbbr (who also makes Gameboy Games . Check out Petris and GB Wordyl, Published by Ferrante Crafts.)

Sub-Pixel Resolution Alternatives

Despite being 30 years old Gameboy’s processor can run at 60 frames-per-second. Incrementing an integer 60 times in one second can lead to insanely fast & uncontrollable movement. Without sub-pixel resolution, developers have 3 (lesser) choices for reasonable & smooth character motion.

  • Accepting possibly uncontrollable motion
  • Slowing down the frame rate of the entire game
  • Using a counter to change character motion variables

Uncontrollable motion is out of the question, as it will ruin the player’s experience.

Slowing down the frame rate of the entire game will effect almost every other visual/physical aspect of the game, and also make changing other visual/physical aspects confusing.

Using a counter to change character motion variables solves our previous problem. I use it in my How to make Flappy Bird for the Nintendo Gameboy tutorial. However, it adds complexity when dealing with multiple variables.

Sub-Pixel Resolution Actual Example

Here’s a an actual Gameboy example of Sub-Pixel Resolution. In this example, move “Mario” left & right (using the d-pad) to collect the falling mushrooms. The mushrooms will fall from the top of the screen at a constant rate.

The sprites are from Super Mario Land 2: Six Golden Coins. I found them on The Spriters Resource.

The A button can be used to toggle between 4 different modes:

  1. Default Motion
  2. Slowing down the frame rate for the entire game
  3. Using a Counter
  4. Sub-Pixel Resolution

A switch-case statement is used to switch between logic. The following variables should be noted:

  • movement – This is an signed integer value. If the player isn’t pressing left or right, it’s value is 0. Otherwise, it’s value is -1 when the user is pressing left, and +1 when the user is pressing right.
  • marioFrame – This value is changed by the following logic method. It can range between 0 and 4 inclusive.
  • marioX – This value will be changed by the following logic methods.
  • marioDrawFrame – This is the actual frame that Mario will be drawn with. The reason it is separate from the marioFrame variable is for consistent logic flow. With Sub-Pixel Resolution, we’ll set the value of this variable to be the “true” value of the marioFrame variable.
  • marioDrawX – This is the actual x position mario will be drawn at. The reason it is separate from the marioX variable is for consistent logic flow. With Sub-Pixel Resolution, we’ll set the value of this variable to be the “true” value of the marioX variable.

NOTE: Because of potentially large values, the variables need to be 16-bit

Default Motion

Using the first method, Mario moves at a good smooth speed. However, since it’s animation frame increments each frame, he’s animated way too fast.

// This is okay for certain move speeds( It can't do less than 1px/second though)
// However, animation is out of control
default:
    if(movement!=0){
        marioX+=movement*2;
        marioDrawX=marioX;
        
        // Increase mario's frame
        // Loop background after 3
        if(++marioFrame>=MARIO_RUN_FRAMECOUNT)marioFrame=0;

        // Frame 0 is or standing
        // Increase by one for running frames
        marioDrawFrame=1+marioFrame;
    }else{

        // Draw with our standing frame 0
        marioFrame=0;
        marioDrawFrame=0;
    }
    break;

Using a Counter

In the third method, adjusting the frame rate, we’ve fixed the previous problems.

  • Mario runs at an acceptable pace.
  • The mushroom falls at our desired speed.
  • Mario’s animation isn’t insanely fast.

With this method, Mario is moved 5 pixels every 3 frames. In addition, Mario is animated 1 frame every 6 frames. However, this method is not very smooth. Also, the code, when only using 2 variables, is slightly more complicated. This is because we have to increment, check, and reset 2 different counter variables. One for motion (simply called “counter”), and one for animation (called “runCounter”).

// In the counter method, we use a extra counter to increment values at a slower rate
// This avoids the issue of having to adjust frame rate.
case COUNTER:
    if(movement!=0){

        // Change Mario's X position by 5 every 3 frames
        if(++counter>=3){
            counter=0;
            marioX+=movement*5;
            marioDrawX=marioX;
        }

        // Increase our animation counter until it reachces 6, then reset
        if(++runCounter>=6){
            runCounter=0;

            // Increase mario's frame
            // Loop background after 3
            if(++marioFrame>=MARIO_RUN_FRAMECOUNT)marioFrame=0;

            // Frame 0 is or standing
            // Increase by one for running frames
            marioDrawFrame=1+marioFrame;
        }
    }else{

        // Reset our counters
        counter=0;
        runCounter=0;

        // Draw with our standing frame 0
        marioFrame=0;
        marioDrawFrame=0;
    }
    break;

Sub-Pixel Resolution

The fourth method, sub-pixel resolution (via scaled integers), solves all the previous problems.

Both the “marioX” and “marioFrame” variables are scaled integers. Our scale amount is 16 (16 is the 4th power of 2, so we shift by 4 bits). We can add to them normally. Adding 16 is equivalent to adding 1px. Adding 4 is equivalent to adding 0.25px. We need shift the bits of the “face” values rightward in two situations.

  • When checking/using what animation frame Mario is on.
  • Mario’s X to be drawn at.
// This is the best method
// We don't need an extra variable, and we have more control over slower rates
case SUB_PIXEL_RESOLUTION:

    // Increase/Decrease the "face" value 
    marioX+=movement*25;

    // Draw mario at the true value
    marioDrawX=marioX>>4;

    // If the player is moving
    if(movement!=0){

        // Increase mario's frame by 2
        // This is a sub-pixel resolution value
        marioFrame+=2;
        
        // Increase mario's frame
        // Loop background after 3
        // Shift right 4 bits to get it's true value
        if((marioFrame>>4)>=MARIO_RUN_FRAMECOUNT)marioFrame=0;
    
        // Frame 0 is or standing
        // Increase by one for running frames
        // Shift right 4 bits to get it's true value
        marioDrawFrame=1+marioFrame>>4;
    }else{

        // Draw with our standing frame 0
        marioFrame=0;
        marioDrawFrame=0;
    }
    break;

Analyzing the Demo

Minus the comments, you’ll notice that the third (counter) method’s logic is slightly more complex. This is because we have to manage 2 counter variables. Within the scope of this demo, that may seem alright. However, when you think about a full game and all the different variables in it; possibly a lot of different counters will be needed.

If we compare the third (counter) method, and fourth (sub-pixel resolution) method; we can see what makes the fourth method more smooth.

The third method moves Mario’s X position 5 pixels every 3 frames.

Using a counter for MarioX
Frame123456789101112
Counter012301230123
Mario X00055551010101015
Difference000500050005

Graph courtesy of @bbbbbr

The fourth method move’s the “face” value for Mario’s X position by 25 each frame. The scale amount is 16

Using Scaled Integers for MarioX
Frame123456789101112
Face Value0255075100125150175200225250275
Mario X (True Value)01346791012141517
Difference012121212211

Note how in the fourth method, the difference between Mario’s X value’s never goes above 2. On the contrary, the same cannot be said for the third method. Where the difference jumps to 5 for one frame every 3 frames.

If we compare against using floating points. The ideal difference for the third method is 15/12 (The distance moved divided by the number of frames), which equals 1.25. The ideal difference for the fourth method is 17/12, which equals 1.4167. This is why the fourth method appears more smooth. The difference in the final value is more consistently closer to the ideal situation using Floats.

The source code is available for free on GitHub. You can run this example using a Gameboy emulator like BGB or Emulicious.

Leave a Reply

Your email address will not be published. Required fields are marked *

THANKS FOR READING!

If you have any suggestions, and/or are confused about anything: feel free to leave a comment, send me a email, or a message on social media. Constructive criticism will help the gameyard, and others like yourself. If you learned something from this tutorial, and are looking for more content, check other these other tutorials. 

Smooth Movement using Sub-Pixel Resolution

Thanks for reading my tutorial, here are the files for that tutorial. 

Download Instructions

Unzip the attached zip file. You’ll need GBDK-2020 downloaded on your computer.  Update the GBDK_HOME environment variable in the make.bat file, then run the make.bat file from the command line/terminal.

If you want to make/request changes for the code, you can send me an email or put in a PR request on GitHub. Here’s a GitHub link for the code below.

Sign-Up for the "Gameyard Newsletter" for MOre Game Development News

If you like Retro Game Development, subscribe to the Gameyard Newsletter. Stay up-to-date with all the latest Game Boy news. It’s free, and you can unsubscribe any time!

Be sure to check your email inbox for a confirmation email.