Skip to content

Build Flappy Cat

This guide builds a small Flappy-style game with Spottedcat. By the end, you will have a playable loop with movement, input, obstacles, collision, score, restart, and the built-in splash.

You can also play the finished WASM demo.

1. Create the project

bash
cargo new flappy-cat
cd flappy-cat
cargo add spottedcat@1.0.0

Replace src/main.rs as you go through the guide.

2. Start with state

Spottedcat games are Spot types. Put the game state in a struct:

rust
use spottedcat::{Context, DrawOption, Image, Key, MouseButton, OneShotSplash, Pt, Spot, Text, TouchPhase, WindowConfig, run};

const BIRD_X: f32 = 92.0;
const BIRD_SIZE: f32 = 28.0;
const GRAVITY: f32 = 920.0;
const JUMP_VELOCITY: f32 = -330.0;
const PIPE_WIDTH: f32 = 58.0;
const PIPE_GAP: f32 = 150.0;
const PIPE_SPEED: f32 = 150.0;

#[derive(Clone, Copy)]
struct Pipe {
    x: f32,
    gap_y: f32,
    scored: bool,
}

struct FlappyCat {
    bird: Image,
    pipe: Image,
    ground: Image,
    font_id: u32,
    bird_y: f32,
    bird_vy: f32,
    pipes: [Pipe; 3],
    score: u32,
    game_over: bool,
    time: f32,
}

The important pattern is simple: keep positions and velocities in your struct, update them in update, and draw them in draw.

3. Generate tiny art in code

For a first game, generated pixel art is enough. You do not need an asset pipeline yet.

rust
fn solid_image(ctx: &mut Context, width: usize, height: usize, color: [u8; 4]) -> Image {
    let mut pixels = vec![0; width * height * 4];
    for pixel in pixels.chunks_exact_mut(4) {
        pixel.copy_from_slice(&color);
    }
    Image::new(ctx, Pt::from(width as f32), Pt::from(height as f32), &pixels).unwrap()
}

Use one square-ish image for the cat, one 1-pixel-high image scaled into pipes, and one small ground strip.

4. Initialize the game

rust
impl Spot for FlappyCat {
    fn initialize(ctx: &mut Context) -> Self {
        const FONT: &[u8] = include_bytes!("../assets/DejaVuSans.ttf");
        let font_id = spottedcat::register_font(ctx, FONT.to_vec());

        let bird = solid_image(ctx, 28, 28, [241, 126, 72, 255]);
        let pipe = solid_image(ctx, PIPE_WIDTH as usize, 1, [79, 241, 217, 255]);
        let ground = solid_image(ctx, 1, 24, [41, 28, 24, 255]);

        Self {
            bird,
            pipe,
            ground,
            font_id,
            bird_y: 220.0,
            bird_vy: 0.0,
            pipes: [
                Pipe { x: 420.0, gap_y: 180.0, scored: false },
                Pipe { x: 630.0, gap_y: 245.0, scored: false },
                Pipe { x: 840.0, gap_y: 150.0, scored: false },
            ],
            score: 0,
            game_over: false,
            time: 0.0,
        }
    }

    // update, draw, and remove come next.
}

If you do not want text yet, skip the font and add score later.

5. Add flap input and gravity

Read input in update. Use dt so movement stays stable across frame rates.

rust
fn update(&mut self, ctx: &mut Context, dt: std::time::Duration) {
    let dt = dt.as_secs_f32().min(1.0 / 20.0);

    let flap = spottedcat::key_pressed(ctx, Key::Space)
        || spottedcat::key_pressed(ctx, Key::Up)
        || spottedcat::mouse_button_pressed(ctx, MouseButton::Left)
        || spottedcat::touches(ctx)
            .iter()
            .any(|touch| touch.phase == TouchPhase::Started);

    if flap {
        self.bird_vy = JUMP_VELOCITY;
    }

    self.bird_vy += GRAVITY * dt;
    self.bird_y += self.bird_vy * dt;
}

That is already the heart of the game: input changes velocity, gravity pulls the cat down, and the draw step shows the result.

6. Move pipes and score

Each pipe moves left. When it leaves the screen, recycle it to the right.

rust
for i in 0..self.pipes.len() {
    self.pipes[i].x -= PIPE_SPEED * dt;

    if !self.pipes[i].scored && self.pipes[i].x + PIPE_WIDTH < BIRD_X {
        self.pipes[i].scored = true;
        self.score += 1;
    }

    if self.pipes[i].x + PIPE_WIDTH < -20.0 {
        let farthest = self.pipes.iter().map(|pipe| pipe.x).fold(0.0_f32, f32::max);
        self.pipes[i] = Pipe {
            x: farthest + 210.0,
            gap_y: 130.0 + ((self.time * 1.7 + i as f32 * 2.1).sin() + 1.0) * 75.0,
            scored: false,
        };
    }
}

This is a common arcade pattern: a small fixed pool of obstacles, recycled forever.

7. Draw the frame

Draw the pipes by scaling a tiny image. Draw the cat at its current position.

rust
fn draw(&mut self, ctx: &mut Context, screen: Image) {
    let (_w, h) = spottedcat::window_size(ctx);
    let ground_y = h.as_f32() - 36.0;

    for pipe in self.pipes {
        let top_h = (pipe.gap_y - PIPE_GAP * 0.5).max(0.0);
        let bottom_y = pipe.gap_y + PIPE_GAP * 0.5;
        let bottom_h = (ground_y - bottom_y).max(0.0);

        screen.draw(
            ctx,
            &self.pipe,
            DrawOption::default()
                .with_position([Pt::from(pipe.x), Pt::from(0.0)])
                .with_scale([1.0, top_h]),
        );

        screen.draw(
            ctx,
            &self.pipe,
            DrawOption::default()
                .with_position([Pt::from(pipe.x), Pt::from(bottom_y)])
                .with_scale([1.0, bottom_h]),
        );
    }

    screen.draw(
        ctx,
        &self.bird,
        DrawOption::default().with_position([Pt::from(BIRD_X), Pt::from(self.bird_y)]),
    );
}

8. Add collisions

Use rectangle overlap checks. For this game, simple axis-aligned collision is perfect.

rust
fn overlaps(ax: f32, ay: f32, aw: f32, ah: f32, bx: f32, by: f32, bw: f32, bh: f32) -> bool {
    ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by
}

Check the bird against the ground and the two pipe rectangles. On collision, set game_over = true.

9. Add restart and splash

When the game is over, use the same flap input to reset positions and score. Then wrap the game with the built-in splash:

rust
fn main() {
    run::<OneShotSplash<FlappyCat>>(WindowConfig {
        title: "Flappy Cat".to_string(),
        width: Pt::from(640.0),
        height: Pt::from(480.0),
        ..Default::default()
    });
}

The splash gives the tutorial a complete “from startup to game loop” shape.

Finished version

The embedded demo is built from:

examples/wasm/flappy_cat

Build it with:

bash
wasm-pack build examples/wasm/flappy_cat --release --target web --out-dir pkg

Then copy the generated pkg folder into the docs demo folder.

Released under the MIT OR Apache-2.0 License.