Skip to content

A PLAY EXPERIENCE MAKER'S WORK LOG FOR FUTURE SELF©2001 – 2023 Kyle Li 李肅綱 All Rights Reserved.

8bitworkshop Notes – Player Running and Jumping

Posted on October 4, 2026October 4, 2026 by admin
// -----------------------------------------------------------------------------
// 8BITWORKSHOP ASSET LINK DIRECTIVES
// -----------------------------------------------------------------------------
//#link "chr_generic.s"

#include "neslib.h"

// Physics Constants
#define FLOOR_Y     192
#define PLAYER_X    32
#define GRAVITY     1
#define JUMP_FORCE -10

// -----------------------------------------------------------------------------
// PALETTE SETUP
// -----------------------------------------------------------------------------
const unsigned char sprite_palette[16] = {
    0x0F, 0x16, 0x27, 0x30, // Palette 0: Black, Red, Orange, White
    0x0F, 0x0F, 0x0F, 0x0F,
    0x0F, 0x0F, 0x0F, 0x0F,
    0x0F, 0x0F, 0x0F, 0x0F
};

// -----------------------------------------------------------------------------
// 2x2 METASPRITES (16x16 pixels)
// Structure per tile: x_offset, y_offset, tile_id, attributes
// Terminated with 128 (0x80)
// -----------------------------------------------------------------------------
const unsigned char metasprite_run1[] = {
    0,  0, 0xDC, 0, // Top-Left tile
    8,  0, 0xDE, 0, // Top-Right tile
    0,  8, 0xDD, 0, // Bottom-Left tile
    8,  8, 0xDF, 0, // Bottom-Right tile
    128
};

const unsigned char metasprite_run2[] = {
    0,  0, 0xE0, 0,
    8,  0, 0xE2, 0,
    0,  8, 0xE1, 0,
    8,  8, 0xE3, 0,
    128
};

const unsigned char metasprite_jump[] = {
    0,  0, 0xE8, 0,
    8,  0, 0xEA, 0,
    0,  8, 0xE9, 0,
    8,  8, 0xEB, 0,
    128
};

// Running animation frame table
const unsigned char* const run_animation[] = {
    metasprite_run1,
    metasprite_run2
};

// -----------------------------------------------------------------------------
// PLAYER STATE VARIABLES
// -----------------------------------------------------------------------------
unsigned char player_y = FLOOR_Y;
signed char player_vy = 0;
unsigned char is_grounded = 1;

unsigned char anim_timer = 0;
unsigned char anim_frame = 0;

// -----------------------------------------------------------------------------
// FUNCTIONS
// -----------------------------------------------------------------------------

void update_player(unsigned char pad) {
    // Jump trigger on A button
    if ((pad & PAD_A) && is_grounded) {
        player_vy = JUMP_FORCE;
        is_grounded = 0;
    }

    // Apply Velocity & Gravity
    player_y += player_vy;
    player_vy += GRAVITY;

    // Floor Boundary
    if (player_y >= FLOOR_Y) {
        player_y = FLOOR_Y;
        player_vy = 0;
        is_grounded = 1;
    }
}

void update_animation(void) {
    if (is_grounded) {
        anim_timer++;
        if (anim_timer >= 6) { // Advance frame every 6 ticks (~10 FPS)
            anim_timer = 0;
            anim_frame = (anim_frame + 1) % 2; // Toggle between frame 0 and 1
        }
    }
}

void draw_player(void) {
    const unsigned char* current_metasprite;

    // Select airborne or grounded MetaSprite
    if (!is_grounded) {
        current_metasprite = metasprite_jump;
    } else {
        current_metasprite = run_animation[anim_frame];
    }

    // Render 2x2 MetaSprite to OAM
    oam_meta_spr(PLAYER_X, player_y, 0, current_metasprite);
}

// -----------------------------------------------------------------------------
// MAIN PROGRAM LOOP
// -----------------------------------------------------------------------------
void main(void) {
    // Write sprite palettes to PPU
    pal_spr(sprite_palette);

    // Turn on graphics rendering
    ppu_on_all();

    while (1) {
        // Read Controller 1 Input
        unsigned char pad = pad_trigger(0);

        update_player(pad);
        update_animation();

        // Clear sprite hardware memory and re-draw
        oam_clear();
        draw_player();

        // Synchronize with NES TV refresh rate (60 FPS)
        ppu_wait_nmi();
    }
}

Using C with the neslib library in 8bitworkshop lets you prototype game loops, player gravity, and scrolling background mechanics cleanly without writing raw 6502 assembly right away.

Core Technical Concept

To simulate an endless runner on NES:

  1. Player Position: The player stays fixed horizontally on the X-axis (e.g., $X = 32$) and only moves vertically on the Y-axis when jumping or falling under gravity.
  2. Scrolling: The NES background hardware fine-scrolls horizontally (scroll_x++). As background tiles move off-screen to the left, new tiles and obstacles spawn on the right seam.
  3. Collision Detection: Check bounding box collisions between the fixed player character sprite and incoming obstacle coordinates.

[Breakpoint] The code runs, but nothing shows up on the screen.

To handle a character made of 4 hardware tiles ($16 \times 16$ pixels in a $2 \times 2$ grid), you can use MetaSprites or standard relative coordinate drawing functions. neslib provides built-in functions like oam_meta_spr() specifically designed for multi-tile objects.

[Breakpoint] I added logic for character animation and collision, and while the code checked out, the screen just flashed between black and red with nothing rendering. I soon realized I hadn’t linked the default CHR file to the game likely because my original prompt didn’t include details about the CHR data and color palette.

// -----------------------------------------------------------------------------
// METASPRITES
// -----------------------------------------------------------------------------
const unsigned char metasprite_run1[] = {
    0,  0, 0x01, 0, // Top-Left
    8,  0, 0x02, 0, // Top-Right
    0,  8, 0x11, 0, // Bottom-Left
    8,  8, 0x12, 0, // Bottom-Right
    128
};

const unsigned char metasprite_run2[] = {
    0,  0, 0x03, 0,
    8,  0, 0x04, 0,
    0,  8, 0x13, 0,
    8,  8, 0x14, 0,
    128
};

const unsigned char metasprite_jump[] = {
    0,  0, 0x05, 0,
    8,  0, 0x06, 0,
    0,  8, 0x15, 0,
    8,  8, 0x16, 0,
    128
};

const unsigned char* const run_animation[] = {
    metasprite_run1,
    metasprite_run2
};

The flickering occurs because check_collision() is evaluating to 1 (true) constantly on every frame, causing the game loop to alternate palette color 0 between red (0x16) and black (0x0F).

The Cause: Initial Y-Coordinate Overlap

  1. FLOOR_Y is set to 192.
  2. player_y starts at 192.
  3. player_y + 16 equals 208.
  4. obstacle_y is set to FLOOR_Y + 8 = 200.

Because (player_y + 16) ($208$) is greater than or equal to obstacle_y ($200$), the vertical collision condition (player_y + 16) >= obstacle_y evaluates to TRUE continuously, even when the obstacle is on the far right of the screen ($X = 240$).

Adjust obstacle_y to sit at FLOOR_Y and refine the bounding box collision logic to require both top and bottom $Y$-axis overlaps:

[Breakpoint] Screen was still back, I finally realized I need to set up color palettes.

The color palette setup is incomplete. At boot, the NES PPU contains random or zeroed palette RAM. Calling pal_col(0, ...) only modifies index 0 (the background fill color), leaving all sprite colors uninitialized or black.

What Is Happening

  1. Undefined Sprite Palettes: Without initializing sprite palettes using pal_spr() or pal_bg(), the PPU reads color 0x0F (black) for every sprite pixel. The sprites are actually rendering, but they are invisible against the black background.
  2. False Collision Flashing: Because the sprite tiles are invisible, it felt like a random flashing bug. Modifying obstacle_x -= 2 altered how often the invisible obstacle bounding box collided with the player.

In NES hardware, palettes are defined in 16-byte arrays (4 palette groups of 4 colors each). Index 0 of each group is the shared background color.

Add palette initialization inside the main() setup before entering the while(1) loop:

// Define background and sprite palette arrays (16 bytes each)
const unsigned char bg_palette[16] = {
    0x0F, 0x00, 0x10, 0x30, // Palette 0 (Black, Gray, Light Gray, White)
    0x0F, 0x01, 0x11, 0x21, // Palette 1
    0x0F, 0x06, 0x16, 0x26, // Palette 2
    0x0F, 0x09, 0x19, 0x29  // Palette 3
};

const unsigned char sprite_palette[16] = {
    0x0F, 0x16, 0x27, 0x30, // Palette 0: Red, Orange, White (Player)
    0x0F, 0x11, 0x21, 0x30, // Palette 1: Blue, Light Blue, White (Obstacle)
    0x0F, 0x09, 0x19, 0x29, // Palette 2
    0x0F, 0x0F, 0x0F, 0x0F  // Palette 3
};

In 8bitworkshop, comment directives like //link "chr_generic.s" tell the build system to assemble and link the CHR graphics asset bank into the final ROM cartridge. Without it, the NES PPU renders completely blank or garbage graphics banks. This was the final missing link. Here is the working code for character running.

Posted in 8-Bit, games

Post navigation

Native Web Speech API
8bitworkshop Notes – Display an external Nametable V1

Recent Posts

  • 8bitworkshop Notes – Include binary files
  • 8bitworkshop Notes – Display an external Nametable V1
  • 8bitworkshop Notes – Player Running and Jumping
  • Native Web Speech API
  • AI: You are _____ correct.

Archives

Proudly powered by WordPress | Theme: MiniZen by Martin Stehle.