This is a continuation of Pong tutorial with ggez.

At the end part 1, we had the skeleton of a ggez game:

use ggez::{
    event::EventHandler,
};

struct MainState {
}

impl EventHandler for MainState {
    fn update(&mut self, _: &mut ggez::Context) -> ggez::GameResult {
        Ok(())
    }

    fn draw(&mut self, _: &mut ggez::Context) -> ggez::GameResult {
        Ok(())
    }
}

fn main() -> ggez::GameResult {
    // create a mutable reference to a `Context` and `EventsLoop`
    let (ctx, event_loop) = &mut ggez::ContextBuilder::new("Pong", "Fish").build().unwrap();

    // Make a mutable reference to `MainState`
    let main_state = &mut MainState {};

    // Start the game
    ggez::event::run(ctx, event_loop, main_state)
}

In this part, we’ll set up our structs and the draw loop.


Struct setup

Pong essentially consists of just a few pieces. There’s two paddles and a ball. There’s also two score counters. We want to store all the data Pong needs in our MainState. To make our MainState struct, we need to figure out what type each field should be.

Paddles are pretty simple. They’re really just rectangles. They don’t even need to have a stored velocity because they only move when there’s input. A Paddle struct could look like this:

Paddle {
   // in ggez, positions are stored as floats
   x: f32, // top left x coordinate
   y: f32, // top left y coordinate 
   width: f32,
   height: f32,
}

But luckily, we don’t have to make our own Paddle struct because ggez conveniently comes with a nice Rect struct.

ggez’s Rect comes with some nice methods like .overlaps().

If we store each paddle in our game as a ggez Rect, our MainState so far looks like this:

MainState {
   l_paddle: ggez::graphics::Rect,
   r_paddle: ggez::graphics::Rect,
}

It’s easier not to keep writing out ggez::graphics::Rect, so add a use ggez::graphics::Rect at the start of the file and then you can replace ggez::graphics::Rect with just Rect.

The ball in Pong is also really just a Rect, but it also has velocity. Because of that, it’s best to make a new struct for it.

Ball {
   rect: Rect,
   x_vel: f32,
   y_vel: f32,
}

Instead of storing x_vel and y_vel separately as f32s, we could use ggez’s Vector type. ggez actually uses a specific math library which contains its Vector type called mint, so the full name is ggez::mint::Vector2<T>. The <T> denotes that Vector2 is a generic type. Since all the graphics stuff in ggez is done with f32s, we want to use Vector2<f32>s. To make things easier, add this somewhere near the start of your file:

type Vector = ggez::mint::Vector2<f32>;

Now, our Ball struct can be this:

Ball {
   rect: Rect,
   vel: Vector,
}

Now that we have our Ball struct, we can add it to our MainState struct:

MainState {
   l_paddle: Rect,
   r_paddle: Rect,
   ball: Ball,
}

Now, the only thing MainState needs is a way to store the scores, and for that we can use u8s. u8s will overflow if the score goes above 255 though, so maybe we should use u16 just to be safe.

MainState {
   l_paddle: Rect,
   r_paddle: Rect,
   ball: Ball,
   l_score: u16,
   r_score: u16,
}

You might’ve noticed that as soon as we started adding new fields to MainState the program stopped compiling properly. That’s because of this line in our main() method:

let main_state = &mut MainState {};

We have to set every field of our MainState struct to declare a new one.

 let main_state = &mut MainState {
     l_paddle: Rect::new(20.0, 300.0, 20.0, 50.0),
     r_paddle: Rect::new(280.0, 300.0, 20.0, 50.0),
     ball: Ball { rect: Rect::new(300.0, 300.0, 20.0, 20.0), vel: Vector {x: 0.0, y: 0.0} },
     l_score: 0,
     r_score: 0,
 };

This declaration is kind of awkward. I arbitrarily chose all the coordinates and sizes, and we’ll probably want to change everything later. To make that easier, let’s set up some consts at the start of the file. We also want to choose a screen size to work with.

Here’s what I end up with:

const SCREEN_HEIGHT: f32 = 600.;
const SCREEN_WIDTH: f32 = 600.;

const X_OFFSET: f32 = 20.; // distance from each paddle to their respective walls
const PADDLE_WIDTH: f32 = 12.;
const PADDLE_HEIGHT: f32 = 75.;

const BALL_RADIUS: f32 = 10.;

Now, we can declare main_state like this:

 let main_state = &mut MainState {
     l_paddle: Rect::new(X_OFFSET, SCREEN_HEIGHT / 2.0 - PADDLE_HEIGHT / 2.0, PADDLE_WIDTH, PADDLE_HEIGHT),
     r_paddle: Rect::new(SCREEN_WIDTH - X_OFFSET, SCREEN_HEIGHT / 2.0 - PADDLE_HEIGHT / 2.0, PADDLE_WIDTH, PADDLE_HEIGHT),
     ball: Ball { rect: 
         Rect::new(SCREEN_WIDTH / 2.0 - BALL_RADIUS / 2.0, SCREEN_HEIGHT / 2.0 - BALL_RADIUS / 2.0, BALL_RADIUS, BALL_RADIUS), 
         vel: Vector {x: 0.0, y: 0.0} },
     l_score: 0,
     r_score: 0,
 };

Adding consts makes the lines a lot longer. I’ve self formatted a bit, but luckily we can use cargo fmt to automatically format. This will also adjust anything else in the file that doesn’t meet the official Rust formatting guidelines.

 let main_state = &mut MainState {
     l_paddle: Rect::new(
         X_OFFSET,
         SCREEN_HEIGHT / 2.0 - PADDLE_HEIGHT / 2.0,
         PADDLE_WIDTH,
         PADDLE_HEIGHT,
     ),
     r_paddle: Rect::new(
         SCREEN_WIDTH - X_OFFSET,
         SCREEN_HEIGHT / 2.0 - PADDLE_HEIGHT / 2.0,
         PADDLE_WIDTH,
         PADDLE_HEIGHT,
     ),
     ball: Ball {
         rect: Rect::new(
             SCREEN_WIDTH / 2.0 - BALL_RADIUS / 2.0,
             SCREEN_HEIGHT / 2.0 - BALL_RADIUS / 2.0,
             BALL_RADIUS,
             BALL_RADIUS,
         ),
         vel: Vector { x: 0.0, y: 0.0 },
     },
     l_score: 0,
     r_score: 0,
 };

Drawing

So we’ve set up all our structs and written like 50 lines but the behavior of our program hasn’t changed a single bit. Now that our MainState actually has stuff in it though, we can draw it. This is, of course, done in the draw() method of our MainState.

Before, the signature of draw() was:

fn draw(&mut self, _: &mut ggez::Context) -> ggez::GameResult

We didn’t use the field of type &mut ggez::Context before, so we just left it unnamed with an underscore. Now, we’re going to use it so we should rename it to ctx.

fn draw(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult

Do the same thing with your update() method.

Drawing in ggez is pretty straightforward for Pong. We just have to draw each paddle and the ball. We also have to clear the screen in between frames. With more advanced games ggez has more efficient ways to draw things, but for Pong we can just draw each shape individually.

To clear the screen, we just use ggez::graphics::clear. From the ggez documentation, we can see its full signature:

pub fn clear(ctx: &mut Context, color: Color)

From this, we know that it takes a mutable reference to a Context, which we have, and a Color, which we don’t. If you click on Color in the ggez web documentation, it will take you to the Color documentation. From the Color documentation, we see that we can initialize Colors with ggez::graphics::Color::new(r, g, b, a). Knowing that, here’s our new draw() method:

    fn draw(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult {
        ggez::graphics::clear(ctx, ggez::graphics::Color::new(0.0, 0.0, 0.0, 1.0));
        Ok(())
    }

ggez::graphics::Color is really long to write out, but we won’t use it anywhere other than in draw() so we can do a scoped use ggez::graphics::Color at the start of it. We should also just use ggez::graphics because there’s a lot more stuff from it we’ll use in draw().

 fn draw(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult {
     use ggez::graphics::Color;
     use ggez::graphics;

     graphics::clear(ctx, Color::new(0.0, 0.0, 0.0, 1.0));
     Ok(())
 }

After clearing the screen, we need to actually present() it to see the changes. This is done with ggez::graphics::present(). All drawing code needs to be done between clear() and present().

 fn draw(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult {
     use ggez::graphics::Color;
     use ggez::graphics;

     graphics::clear(ctx, Color::new(0.0, 0.0, 0.0, 1.0)); // Color::new(0.0, 0.0, 0.0, 1.0) is black
     // all drawing stuff goes here
     graphics::present(ctx);
     Ok(())
 }

On compiling and running this, we get a black window. We also get a familiar compiler warning:

warning: unused `std::result::Result` that must be used
  --> src/main.rs:38:9
   |
38 |         graphics::present(ctx);
   |         ^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

ggez::graphics::present returns a Result, more specifically, a GameResult. Rust wants us to handle this if it’s an error, but the game can’t really continue if the graphics don’t work. Because of that, we can just unwrap() the Result, or .expect() it if we want a more specific error message (IMO it’s best to always use expects instead of unwraps but it gets tedious). This is a common theme with game programming in Rust because a lot of game errors just aren’t recoverable.

Our new draw() method looks like this:

 fn draw(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult {
     use ggez::graphics::Color;
     use ggez::graphics;

     graphics::clear(ctx, Color::new(0.0, 0.0, 0.0, 1.0)); // Color::new(0.0, 0.0, 0.0, 1.0) is black
     // all drawing stuff goes here
     graphics::present(ctx).expect("error presenting");
     Ok(())
 }

There’s a few steps to actually draw the paddles and the ball. First, we have to make graphics meshes, using ggez’s ggez::graphics::Mesh. ggez::graphics::Mesh has a new_rectangle() method with this signature:

pub fn new_rectangle(
    ctx: &mut Context,
    mode: DrawMode,
    bounds: Rect,
    color: Color
) -> GameResult<Mesh>

We already have ctx and bounds, and we know how to get color, but DrawMode is new. From clicking on DrawMode in the documentation, we find that ggez::graphics::DrawMode has a fill() method, which specifies that ggez will fill the shape as opposed to just drawing its outline. We also now know that new_rectangle() returns a GameResult, but it’s still unrecoverable so we should expect() it.

Knowing this, to make a mesh for our ball, we can use this line:

let ball_mesh = graphics::Mesh::new_rectangle(ctx, graphics::DrawMode::fill(), self.ball.rect, Color::new(1.0, 1.0, 1.0, 1.0))
   .expect("error creating ball mesh");

To draw it, we use ggez::graphics::draw(), which takes a reference to a Drawable and DrawParams. Our ball_mesh is Drawable, so we just need to figure out what to use for DrawParams. From the (DrawParam documentation)[https://docs.rs/ggez/0.5.1/ggez/graphics/struct.DrawParam.html] we can see that it has a default() implementation, so let’s just use that. Again, draw() returns a result which we should .expect()

graphics::draw(ctx, &ball_mesh, graphics::DrawParam::default()).expect("error drawing ball mesh");

Our draw method (after running cargo fmt) now looks like this:

 fn draw(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult {
     use ggez::graphics;
     use ggez::graphics::Color;

     graphics::clear(ctx, Color::new(0.0, 0.0, 0.0, 1.0)); // Color::new(0.0, 0.0, 0.0, 1.0) is black

     let ball_mesh = graphics::Mesh::new_rectangle(
         ctx,
         graphics::DrawMode::fill(),
         self.ball.rect,
         Color::new(1.0, 1.0, 1.0, 1.0),
     )
     .expect("error creating ball mesh");
     graphics::draw(ctx, &ball_mesh, graphics::DrawParam::default())
         .expect("error drawing ball mesh");

     graphics::present(ctx).expect("error presenting");
     Ok(())
 }

If we compile and run our program, we get a white rectangle roughly in the middle of the screen. It’s not quite centered because we haven’t actually set our screen width and height properly, but we’ll do that later.

Repeat the steps for drawing the ball on the l_paddle and r_paddle and we get our final draw() method for now:

    fn draw(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult {
        use ggez::graphics;
        use ggez::graphics::Color;

        graphics::clear(ctx, Color::new(0.0, 0.0, 0.0, 1.0)); // Color::new(0.0, 0.0, 0.0, 1.0) is black

        let ball_mesh = graphics::Mesh::new_rectangle(
            ctx,
            graphics::DrawMode::fill(),
            self.ball.rect,
            Color::new(1.0, 1.0, 1.0, 1.0),
        )
        .expect("error creating ball mesh");
        graphics::draw(ctx, &ball_mesh, graphics::DrawParam::default())
            .expect("error drawing ball mesh");

        let l_paddle_mesh = graphics::Mesh::new_rectangle(
            ctx,
            graphics::DrawMode::fill(),
            self.l_paddle,
            Color::new(1.0, 1.0, 1.0, 1.0),
        )
        .expect("error creating ball mesh");
        graphics::draw(ctx, &l_paddle_mesh, graphics::DrawParam::default())
            .expect("error drawing ball mesh");

        let r_paddle_mesh = graphics::Mesh::new_rectangle(
            ctx,
            graphics::DrawMode::fill(),
            self.r_paddle,
            Color::new(1.0, 1.0, 1.0, 1.0),
        )
        .expect("error creating ball mesh");
        graphics::draw(ctx, &r_paddle_mesh, graphics::DrawParam::default())
            .expect("error drawing ball mesh");

        graphics::draw(ctx, &ball_mesh, graphics::DrawParam::default())
            .expect("error drawing ball mesh");

        graphics::present(ctx).expect("error presenting");
        Ok(())
    }

There’s a lot of duplicated code here from creating all the new rectangle meshes and drawing them so sometimes I make a simple function like fn draw_rectangle(ctx: &mut Context, rect: &graphics::Rect) -> GameResult, but that’s overkill for Pong.


It’s annoying that stuff isn’t properly centered, so let’s set our window size to the consts that we used earlier. To create our window and ggez context before, we used this:

    let (ctx, event_loop) = &mut ggez::ContextBuilder::new("Pong", "Mikail Khan")
        .build()
        .unwrap();

Here, we created a ContextBuilder with the default settings and used build() immediately. To set our screen size, we want to set the ggez::conf::WindowMode of our ContextBuilder. There’s a lot of settings but you can find them all in the documentation. Here, all we have to use is .dimensions().

    let (ctx, event_loop) = &mut ggez::ContextBuilder::new("Pong", "Mikail Khan")
        .window_mode(ggez::conf::WindowMode::default().dimensions(SCREEN_WIDTH, SCREEN_HEIGHT))
        .build()
        .unwrap();

After adding this, the window size is set and everything is nicely centered.


You can find the updated code for this part here.

In the next part, we’ll make Pong playable by finishing the update loop. You can find it at https://mkhan45.github.io/2020/05/21/Pong-tutorial-with-ggez-Part-3.html