[SOLVED] Jerky drop animation with ggez

Hello,

I’m a beginner in Rust and I’m trying to use ggez game framework.
I made a small animation drop falling vertically, but when the number of drops becomes too large (max 400 drops), the animation becomes jerky.
is there a way for the animation to be more fluid?
Is there something wrong with my code?

extern crate ggez;
extern crate rand;

use rand::Rng;

use ggez::event;
use ggez::graphics;
use ggez::timer;
use ggez::nalgebra as na;
use ggez::{Context, GameResult};

const WIDTH : f32 = 800.0;
const HEIGHT: f32 = 800.0;
const MAX_DROPS: i32 = 400;
struct Drop {
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    speed: f32
}
impl Drop {
    fn new() -> Drop {
        let mut rng = rand::thread_rng();
        let x : f32 = rng.gen();
        let y : f32 = rng.gen();
        let s : f32 = rng.gen();
        let init_x = x * WIDTH;
        let init_y = -y * HEIGHT;
        let init_speed = s * 10.0 + 50.0;
        Drop {
            x: init_x,
            y: init_y,
            w: 3.0,
            h: 6.0,
            speed: init_speed,
        }
    }
    fn update(&mut self, dt: f32) {
        self.y += self.speed * dt;
        if self.y > HEIGHT {
            let mut rng = rand::thread_rng();
            let y : f32 = rng.gen();
            self.y = -y * HEIGHT;
        }
    }
}

struct MainState {
    drops: Vec<Drop>
}

impl MainState {
    fn new(_ctx: &mut Context) -> GameResult<MainState> {
        let mut all_drops = Vec::new();
        for _i in 0..MAX_DROPS {
            all_drops.push(Drop::new());
        }
        let s = MainState {
            drops: all_drops 
        };
        Ok(s)
    }
}

impl event::EventHandler for MainState {
    fn update(&mut self, ctx: &mut Context) -> GameResult {
        const FPS: u32 = 30;
        while timer::check_update_time(ctx, FPS) {

            let dt = 1.0 / (FPS as f32);
            for drop in self.drops.iter_mut() {
                drop.update(dt);
            }
        }
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult {
        graphics::clear(ctx, [0.1, 0.2, 0.3, 1.0].into());
        for drop in self.drops.iter() {
            if drop.y > 0.0 {
                let rectangle = graphics::Mesh::new_rectangle(
                    ctx,
                    graphics::DrawMode::fill(),
                    graphics::Rect::new(drop.x, drop.y, drop.w, drop.h),
                    graphics::WHITE,)?;

                graphics::draw(ctx, &rectangle, (na::Point2::new(0.0, 0.0),))?;
            }
        }
        graphics::present(ctx)?;
        Ok(())
    }
}

pub fn main() -> GameResult {
    let cb = ggez::ContextBuilder::new("Space Shooter", "LittleBoxes")
        .window_mode(ggez::conf::WindowMode::default().dimensions(WIDTH, HEIGHT))
        .window_setup(ggez::conf::WindowSetup::default().title("Space Shooter"));
    let (ctx, event_loop) = &mut cb.build()?;
    let state = &mut MainState::new(ctx)?;
    event::run(ctx, event_loop, state)
}

Thanks

Does the smoothness improve if you do cargo run --release? Rust game frameworks can get CPU bottlenecked quite easily without optimizations enabled.

Also, you might want to consider storing your Mesh objects instead of re-creating them every frame.

1 Like

I had already tested with cargo --release.It’s more smothness but it’s still not good enough.
I will try to store my Mesh instead of recreating them each time.

Thanks for your reply

Looking closer at the implementation of Mesh, I think that’s probably your problem - creating a Mesh allocates a new vertex buffer, which is not a fast operation (to the point where some engines go out of their way to only ever use a single vertex buffer and minimize uploads to it, or offer a type that does so).

Your best bet is probably to either store all of the Mesh objects and re-use them, or go one step further and use a single Mesh with changes to the scaling/positioning or vertex data before each draw.

1 Like

I’ve tried with a single Mesh, like this :

extern crate ggez;
extern crate rand;

use rand::Rng;

use ggez::event;
use ggez::graphics;
use ggez::timer;
use ggez::nalgebra as na;
use ggez::{Context, GameResult};

const WIDTH : f32 = 800.0;
const HEIGHT: f32 = 800.0;
const MAX_DROPS: i32 = 400;
struct Drop {
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    speed: f32,
}
impl Drop {
    fn new(ctx: &mut Context) -> Drop {
        let mut rng = rand::thread_rng();
        let x : f32 = rng.gen();
        let y : f32 = rng.gen();
        let s : f32 = rng.gen();
        let init_x = x * WIDTH;
        let init_y = -y * HEIGHT;
        let init_speed = s * 10.0 + 100.0;
        Drop {
            x: init_x,
            y: init_y,
            w: 3.0,
            h: 6.0,
            speed: init_speed,
        }
    }
    fn update(&mut self, dt: f32) {
        self.y += self.speed * dt;
        if self.y > HEIGHT {
            let mut rng = rand::thread_rng();
            let y : f32 = rng.gen();
            self.y = -y * HEIGHT;
        }
    }

}

struct MainState {
    drops: Vec<Drop>,
    rectangle: graphics::Mesh
}

impl MainState {
    fn new(ctx: &mut Context) -> GameResult<MainState> {
        let mut all_drops = Vec::new();
        for _i in 0..MAX_DROPS {
            all_drops.push(Drop::new(ctx));
        }

        let rect= graphics::Mesh::new_rectangle(
            ctx,
            graphics::DrawMode::fill(),
            graphics::Rect::new(0.0, 0.0, 3.0, 6.0),
            graphics::WHITE,)?;
        let s = MainState {
            drops: all_drops,
            rectangle: rect
        };
        Ok(s)
    }
}

impl event::EventHandler for MainState {
    fn update(&mut self, ctx: &mut Context) -> GameResult {
        const FPS: u32 = 30;
        while timer::check_update_time(ctx, FPS) {

            let dt = 1.0 / (FPS as f32);
            for drop in self.drops.iter_mut() {
                drop.update(dt);
            }
        }
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult {
        graphics::clear(ctx, [0.1, 0.2, 0.3, 1.0].into());
        for drop in self.drops.iter() {
            if drop.y > 0.0 {
               

                graphics::draw(ctx, &self.rectangle, (na::Point2::new(drop.x, drop.y),))?;
            }
        }
        graphics::present(ctx)?;
        Ok(())
    }
}

pub fn main() -> GameResult {
    let cb = ggez::ContextBuilder::new("Space Shooter", "LittleBoxes")
        .window_mode(ggez::conf::WindowMode::default().dimensions(WIDTH, HEIGHT))
        .window_setup(ggez::conf::WindowSetup::default().title("Space Shooter"));
    let (ctx, event_loop) = &mut cb.build()?;
    let state = &mut MainState::new(ctx)?;

    event::run(ctx, event_loop, state)
}

It works, the result seems a little better, but not convincing (edit : on debug version).
I have already tried this kind of animation with javascript and p5js and the result is incomparably more smooth… I don’t understand why

Edit :
The release version is now better (the difference between the release version and the debug version is obvious :open_mouth:

The reasoning for this is mentioned in ggez’s FAQ - in debug mode, Rust skips a lot of optimizations to avoid slowing down compilation. Libraries that do a lot of complex calculations (like game engines!) get hit particularly hard by this, and so run far slower in debug mode.

The recommended approach is to enable some optimizations in debug mode, by adding the following to your Cargo.toml:

[profile.dev]
opt-level = 1 // 0 is the default, 3 is what release mode uses

This strikes a good balance between compile times and performance, and you can continue to increase it if things start to chug.

1 Like

I see a difference with anti-aliasing
.samples(ggez::conf::NumSamples::Two)
maybe you js version is using it by default.

1 Like

Thank a lot for your help.
The members of this forum are a real gold mine for a Rusty beginner motivated like me.

1 Like