Mastodon
Table of Contents
Sub Pixel Resolution
Table of Contents

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.

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 Godot, Unity, 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

Thanks 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 values

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.

Smooth Movement demo using 4 modes

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

Super Mario Land 2 Sprites

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.

Sub-Pixel Resolution Animation 1
// 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;

Slowing down the frame rate

Using the second method, we fix the problem with Mario’s fast animation. However, the result isn’t very smooth. In addition we’ve slowed down the mushroom.

Sub-Pixel Resolution Animation 2

Since we’ve slowed down the frame rate, we have to increase how fast we move Mario.

// With the frame rate method , the logic is just like the default method
// Accept we need to increase some values more since code executes less often
case FRAME_RATE:
    if(movement!=0){
        marioX+=movement*5;
        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.
Sub-Pixel Resolution Animation 3

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.

Sub-Pixel Resolution Animation 4

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
Graph courtesy of @bbbbbr

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.

I hope you found this helpful. Check out my How to make Games for Gameboy tutorial series if you want to learn more about making Gameboy games. I also utilize this in my How to make Pacman for Gameboy tutorial.

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

If you want to test out your game on an actual Game Boy, you can run your game on a SD-Based Flash Cart like the EZ Flash JR
Who else is making Game Boy Games?

There are many individuals who are developing games for retro consoles. Here’s one you might like:

GB Wordyl

Created By: bbbbr

A portable word game that tests your skill! Use strategy and knowledge to guess the hidden word within 6 tries!

Other Tutorials:

Sign-Up for the "Junkyard Newsletter" for more Game Boy news!

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