Pong: With Rust and Macroquad
Tags: gamedev, rust, macroquad, beginners
Published: 2023-04-10 10:55:27


![Game Preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eduld8rr9ldjl8gswou3.png)
# Introduction
In this short tutorial, learn how to make a two-player pong game in Rust.
We will be using Macroquad for this.
## Prerequisites
Having Rust installed on your machine.
Some basic knowledge of Rust (structs, tuples, async/await, loops and random numbers etc.).
If you want to learn some basics of Macroquad, click [here](https://dev.to/paper010/rust-create-a-clicker-game-with-macroquad-1820)
# Getting Started
## Create a new Rust project:

```terminal
cargo new pong
```
## Add Macroquad and Rand Crate:
Either add it in your ``Cargo.toml`` file:
```toml
[dependencies]
macroquad = "0.3.25"
rand = "0.8.5"
# these were the latest versions on the time of making this post
```
Or add it using ``cargo add``:
```
cargo add macroquad
cargo add rand
```
# Blank Window
In your ``src/main.rs`` file, add:
```rs
use macroquad::prelude::*;

#[macroquad::main("Pong")]
async fn main() {
   loop {
     clear_background(SKYBLUE);
     
     next_frame().await;
   }
}
```
This sets up a window with sky-blue as the background color.
Click [here](https://dev.to/paper010/rust-create-a-clicker-game-with-macroquad-1820) for some explanation of the code.

If you wanted more control over the window properties:
```rs
//snip
fn conf() -> Conf {
      Conf {
      window_title:"Pong".to_owned(),
      ...
    }
}

#[macroquad::main(conf)]
```
Specify custom properties to [Conf](https://docs.rs/macroquad/latest/macroquad/prelude/struct.Conf.html) struct. For this tutorial, I'll set ``window_resizeable`` to false.

## Creating Paddles
Let's start by making a ``Paddle`` struct.
```rs
//snip
#[derive(Copy,Clone)]
struct Paddle {
    rect:Rect,
}
```
The [Rect](https://docs.rs/macroquad/0.2.9/macroquad/struct.Rect.html) has these fields:

- The X-Coordinate
- The Y-Coordinate
- The width of the rectangle
- The height of the rectangle
This will make it easier for us to manage the position and size of the paddles.
Speaking of size, let's create some constants for the height and width of the paddles and also their color:
```rs
// snip
const PADDLE_W:f32 = 20f32;
const PADDLE_H:f32 = 80f32;
const PADDLE_COLOR:Color = DARKBLUE;
```
Implementations for Paddle struct:
```rs
// snip
impl Paddle {
    fn new(rect:Rect) -> Self {
        Self {
            rect,
        }
    }

    fn draw(&self) {
        let r = self.rect;
        draw_rectangle(r.x,r.y,r.w,r.h,PADDLE_COLOR);
    }
}
```
The ``new`` method to create a new instance of ``Paddle`` and ``draw`` function to draw the paddle onto the screen.
the [draw_rectangle](https://docs.rs/macroquad/0.2.9/macroquad/fn.draw_rectangle.html?search=rect) function takes the x and y position and width and height (all of f32 type) and also the color to draw.
``screen_width()`` and ``screen_height()`` return the width and height of the screen respectively. We assign the x and y values to be half of the width and height of the screen so that the paddle is at the center of the window.

Coordinate system of Macroquad, where the black rectangle is the screen:

![Coordinate system](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/thu1k7ccht1p8sa9pmus.png)

In your main function, create a paddle and draw it on to the screen:
```rs
//snip
async fn main() {
    let paddle = Paddle::new(Rect::new(screen_width()/2.,screen_height()/2.,PADDLE_W,PADDLE_H));
    loop {
        clear_background(SKYBLUE);
        paddle.draw();
        next_frame().await();
    }
}
```
The resulting code should give:
![Gamwe Prevbiew](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rys8ubm74jau925jrynw.png)

And the code:
```rs
use macroquad::prelude::*;

const PADDLE_W:f32 = 20f32;
const PADDLE_H:f32 = 80f32;
const PADDLE_COLOR:Color = DARKBLUE;

fn conf() -> Conf {
      Conf {
      window_title:"Pong".to_owned(),
      window_resizable: false,
      ..Default::default()
    }
}

#[derive(Copy,Clone)]
struct Paddle {
    rect:Rect,
}

impl Paddle {
    fn new(rect:Rect) -> Self {
        Self {
            rect,
        }
    }

    fn draw(&self) {
        let r = self.rect;
        draw_rectangle(r.x,r.y,r.w,r.h,PADDLE_COLOR);
    }
}

#[macroquad::main("Pong")]
async fn main() {
   let paddle = Paddle::new(Rect::new(screen_width()/2.,screen_height()/2.,PADDLE_W,PADDLE_H));
   loop {
     clear_background(SKYBLUE);

     paddle.draw(); // Make sure its after the clear_background function
                    // else it cannot be seen

     next_frame().await;
   }
}
```
Now let's make two paddles, one for the right side and another for the left. Modify your ``main`` function:
```rs
//don't worru about the unused mut warning,we'll use them later.
  let mut paddle_left = Paddle::new(Rect::new(PADDLE_W,screen_height()/2.,PADDLE_W,PADDLE_H));
   let mut paddle_right = Paddle::new(Rect::new(screen_width()-PADDLE_W*2.,screen_height()/2.,PADDLE_W,PADDLE_H));
    
   loop {
     clear_background(SKYBLUE);

     paddle_left.draw();
     paddle_right.draw();
     next_frame().await;
   }
```
Creating two new paddles for the left and the right and aligning them properly, and drawing them on the screen
Output:
![Game Preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qiv5gj2l1q92sy04eeoz.png)

## Movement
Create a new constant for the paddle speed:
```rs

const PADDLE_SPEED:f32 = 10f32;

```
In your Paddle struct, create a new implmentation for movement.

```rs

impl Paddle {
  ...
 fn movement(&mut self,up:KeyCode,down:KeyCode) {
        if is_key_down(up) {
            self.rect.y -= 1.*PADDLE_SPEED;
        }else if is_key_down(down) {
            self.rect.y += 1.*PADDLE_SPEED;
        }
    }
  ...
}

```
We need to change the position (x and y coordinates) of the rect, so we get a mutable refernce. Since we have two paddles with two different keys for movement, we get the keys through the parameters.
```is_key_down`` function takes a [KeyCode](https://docs.rs/macroquad/latest/macroquad/input/enum.KeyCode.html) and returns true if the key is being held down.
If the key to move up is held down, then we move the paddle up by negating the y coordinate multiplied with the paddle speed.
Same logic to move down, but now we add to the y coordinate.

In your main function loop:
```rs

  //snip
   ...
     paddle_left.movement(KeyCode::W,KeyCode::S);
     paddle_right.movement(KeyCode::Up, KeyCode::Down);   
     paddle_left.draw();
     paddle_right.draw();
   ...

```

And now you should be able to move the paddles using the keys!
Here's the whole code till now:

```rs

use macroquad::prelude::*;

const PADDLE_W:f32 = 20f32;
const PADDLE_H:f32 = 80f32;
const PADDLE_COLOR:Color = DARKBLUE;
const PADDLE_SPEED:f32 = 10f32;
fn conf() -> Conf {
      Conf {
      window_title:"Pong".to_owned(),
      window_resizable: false,
      ..Default::default()
    }
}

#[derive(Copy,Clone)]
struct Paddle {
    rect:Rect,
}

impl Paddle {
    fn new(rect:Rect) -> Self {
        Self {
            rect,
        }
    }

    fn movement(&mut self,up:KeyCode,down:KeyCode) {
        if is_key_down(up) {
            self.rect.y -= 1.*PADDLE_SPEED;
        }else if is_key_down(down) {
            self.rect.y += 1.*PADDLE_SPEED;
        }
    }

    fn draw(&self) {
        let r = self.rect;
        draw_rectangle(r.x,r.y,r.w,r.h,PADDLE_COLOR);
    }
}

#[macroquad::main(conf)]
async fn main() {
  let mut paddle_left = Paddle::new(Rect::new(PADDLE_W,screen_height()/2.,PADDLE_W,PADDLE_H));
   let mut paddle_right = Paddle::new(Rect::new(screen_width()-PADDLE_W*2.,screen_height()/2.,PADDLE_W,PADDLE_H));
    
   loop {
     clear_background(SKYBLUE);
     paddle_left.movement(KeyCode::W,KeyCode::S);
     paddle_right.movement(KeyCode::Up, KeyCode::Down);   
     paddle_left.draw();
     paddle_right.draw();
     next_frame().await;
   }
}

```
But you'll probably note that the paddles can go out of the screen.
To restrict the paddles into the screen, add this in your ``movement`` implementation:

```rs
        if self.rect.y > screen_height()-PADDLE_H{
            self.rect.y = screen_height()-PADDLE_H;
        }else if self.rect.y < 0. {
            self.rect.y = 0.;
        }
  
```
Just some simple logic.
If the paddle is gone above screen (less than 0) then we bring it back to the screen (0). If its below the screen (screen_height()-PADDLE_H) then get it back to the screen.

# Adding the Ball
Create some constants for the ball:
```rs

const BALL_RADIUS:f32 = 15f32;
const BALL_COLOR:Color = PINK;
const BALL_SPEED:f32 = 8f32;

```
Create the ``Ball`` struct

```rs

#[derive(Copy,Clone)]
struct Ball {
    circle:Circle,
    dir: Vec2 // direction or velocity of the ball
}

```
``dir`` is the velocity or the direction towards which the ball should travel.
Implementations for the ball:

```rs

impl Ball {
    fn new(circle:Circle) ->Self {
        Self {
            circle,
            dir:vec2(0.,1.) // for now, we'll use these
                            // we'll add random values later
        }
    }

    fn draw(&self) {
        
draw_circle(self.circle.x,self.circle.y,self.circle.r,BALL_COLOR);
    }
}

```
Add the ball to the game:
```rs

async fn main() {
   ...
   let mut ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS)); 
   loop {
     clear_background(SKYBLUE); //MAKE SURE: all the drawing
                                // happens below this function
     ...
     ball.draw();
     ...
   }
}

```
And on running it:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/atl3gulwyg1nviryhudj.png)

Great! Now to make it move, we simple add the dir value (x and y) multiplied by the speed to the balls's position.
Add another function to the ball implementation:
```rs

    fn move_ball(&mut self) {
         self.circle.x += self.dir.x * BALL_SPEED;
         self.circle.y += self.dir.y * BALL_SPEED;
    }

```
And move the ball in your loop:
```rs

//snip
 loop {
     ... 
     ball.move_ball();
     ...
   }

```

Now the ball moves...out of the window. But this has an easy fix!
In your ``move_ball`` function:
```rs

       ...
       if self.circle.y > screen_height()-BALL_RADIUS || self.circle.y < 0.0 {
            self.dir.y = -self.dir.y;
        }
      ...

```
We invert the sign of ``dir.y`` so that it goes the other way. Now if you run this you can see that the ball moves up and down the screen. We aren't doing this with the ``x`` value because we will use that for scoring system.

## Random dir value
Import ``rand`` crate and some of its items:
```rs

use ::crate::{thread_rng,Rng};
```
In your new function for ball, make these changes:
```rs
 let mut rng = thread_rng();
        let dir_x = rng.gen_range(-1.0..1.);
        let dir_y = rng.gen_range(-1.0..1.);
        Self {
            circle,
            dir:vec2(dir_x,dir_y)
        }

```
Done!
The code so far:
```rs

use macroquad::prelude::*;
use ::rand::{thread_rng,Rng};

const PADDLE_W:f32 = 20f32;
const PADDLE_H:f32 = 80f32;
const PADDLE_COLOR:Color = DARKBLUE;
const PADDLE_SPEED:f32 = 10f32;

const BALL_RADIUS:f32 = 15f32;
const BALL_COLOR:Color = PINK;
const BALL_SPEED:f32 = 8f32;

fn conf() -> Conf {
      Conf {
      window_title:"Pong".to_owned(),
      window_resizable: false,
      ..Default::default()
    }
}

#[derive(Copy,Clone)]
struct Paddle {
    rect:Rect,
}

#[derive(Copy,Clone)]
struct Ball {
    circle:Circle,
    dir: Vec2
}


impl Paddle {
    fn new(rect:Rect) -> Self {
        Self {
            rect,
        }
    }

    fn movement(&mut self,up:KeyCode,down:KeyCode) {
        if is_key_down(up) {
            self.rect.y -= 1.*PADDLE_SPEED;
        }else if is_key_down(down) {
            self.rect.y += 1.*PADDLE_SPEED;
        }

        if self.rect.y > screen_height()-PADDLE_H{
            self.rect.y = screen_height()-PADDLE_H;
        }else if self.rect.y < 0. {
            self.rect.y = 0.;
        }
        
    }

    fn draw(&self) {
        let r = self.rect;
        draw_rectangle(r.x,r.y,r.w,r.h,PADDLE_COLOR);
    }
}

impl Ball {
    fn new(circle:Circle) ->Self {
        let mut rng = thread_rng();
        let dir_x = rng.gen_range(-1.0..1.);
        let dir_y = rng.gen_range(-1.0..1.);
        Self {
            circle,
            dir:vec2(dir_x,dir_y)
        }
    }

    fn move_ball(&mut self) {
         self.circle.x += self.dir.x * BALL_SPEED;
         self.circle.y += self.dir.y * BALL_SPEED;
        
        if self.circle.y > screen_height()-BALL_RADIUS || self.circle.y < 0.0 {
            self.dir.y = -self.dir.y;
        }
        
    }

    fn draw(&self) {
        draw_circle(self.circle.x,self.circle.y,self.circle.r,BALL_COLOR);
    }
}

#[macroquad::main(conf)]
async fn main() {
  let mut paddle_left = Paddle::new(Rect::new(PADDLE_W,screen_height()/2.,PADDLE_W,PADDLE_H));
   let mut paddle_right = Paddle::new(Rect::new(screen_width()-PADDLE_W*2.,screen_height()/2.,PADDLE_W,PADDLE_H));
   let mut ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS)); 
   loop {
     clear_background(SKYBLUE);
     paddle_left.movement(KeyCode::W,KeyCode::S);
     paddle_right.movement(KeyCode::Up, KeyCode::Down);   
     ball.move_ball();
     ball.draw();
     paddle_left.draw();
     paddle_right.draw();
     next_frame().await;
   }
}

```
# Collisions!
Now we'll add ball-to-paddle collisions.
Create a new implementation in your ball struct:

```rust

    fn collision_with_paddle(&mut self, paddle_1 : &Rect, paddle_2 : &Rect) {
        let ball_rect = Rect::new(self.circle.x,self.circle.y,self.circle.r,self.circle.r);
        if ball_rect.intersect(*paddle_1).is_some() || ball_rect.intersect(*paddle_2).is_some() {
            self.dir.x = -self.dir.x;
        }
    } 

```
We take in two refernces of paddles. Then we create a rect of the ball because the ``rect`` has a method ``intersect`` which allows us to see if a rect is touching or intersecting another rect. We pass the x and y as coordinates and radius as the height and width (more of a square than a rectangle then).
Then we check if any one of the paddles intersect with the ball and then invert the sign of the dir.x value. Easy!

Call the method in your loop:
```rs
loop {
 ...
 ball.collision_with_paddles(&paddle_left.rect,&paddle_right.rect);
 ...
}
```
Run it!
And now you can play pong until one of you loses the ball.
# Ball respawn and scores
Create a scores tuple variable in your main function (outside the loop):
```rs
let mut scores = (0,0); 
```
Now, inside the loop, we need to check for the ball position and accordingly update the scores:
```rs

     if ball.circle.x < 0.0 {
         ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS));         
         scores.1 += 1;
     }else if ball.circle.x > screen_width() {
         ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS));        
         scores.0 += 1;
    }
```
What good are scores if you cannot view them? 
## Drawing the scores
Macroquad has functions for drawing text onto the screen. I don't really like the font, so I'll use [Robot-Black](https://fonts.google.com/specimen/Roboto). Create a folder `res` in your project directory (outside `src` but inside your project) and install the ttf version of the font there.
Now let's access it in our main function:
```rs
      let font =  load_ttf_font("./res/Roboto-Black.ttf")
        .await
        .unwrap();
```
Create a new function to draw the text:
```rs

fn draw_scores(font:Font,scores:&(u32,u32)) {
        let text_params =  TextParams {
            font_size:70,
            font,
            ..Default::default()
        };

        draw_text_ex(&format!("{}",scores.0).as_str(),100., 100.,text_params);
        draw_text_ex(&format!("{}",scores.1).as_str(),screen_width()-100., 100.,text_params);
}
```
We take in the font and scores. We assign text_params to a struct which allows us to customize how our text looks.([More Info](https://docs.rs/macroquad/latest/macroquad/text/struct.TextParams.html)).  Now we draw the text using [draw_text_ex](https://docs.rs/macroquad/latest/macroquad/text/fn.draw_text_ex.html) at the left and right side.

Then calling it in our main loop:
```rs
loop {
   ...
   clear_background(); //again, always draw stuff below this
   ...

   draw_scores(font,&scores);
   ...
}
```
And we're done!
Here's the full code:
```rs
use macroquad::prelude::*;
use ::rand::{thread_rng,Rng};

const PADDLE_W:f32 = 20f32;
const PADDLE_H:f32 = 80f32;
const PADDLE_COLOR:Color = DARKBLUE;
const PADDLE_SPEED:f32 = 10f32;

const BALL_RADIUS:f32 = 15f32;
const BALL_COLOR:Color = PINK;
const BALL_SPEED:f32 = 8f32;

fn conf() -> Conf {
      Conf {
      window_title:"Pong".to_owned(),
      window_resizable: false,
      ..Default::default()
    }
}

#[derive(Copy,Clone)]
struct Paddle {
    rect:Rect,
}

#[derive(Copy,Clone)]
struct Ball {
    circle:Circle,
    dir: Vec2
}


impl Paddle {
    fn new(rect:Rect) -> Self {
        Self {
            rect,
        }
    }

    fn movement(&mut self,up:KeyCode,down:KeyCode) {
        if is_key_down(up) {
            self.rect.y -= 1.*PADDLE_SPEED;
        }else if is_key_down(down) {
            self.rect.y += 1.*PADDLE_SPEED;
        }

        if self.rect.y > screen_height()-PADDLE_H{
            self.rect.y = screen_height()-PADDLE_H;
        }else if self.rect.y < 0. {
            self.rect.y = 0.;
        }
        
    }

    fn draw(&self) {
        let r = self.rect;
        draw_rectangle(r.x,r.y,r.w,r.h,PADDLE_COLOR);
    }
}

impl Ball {
    fn new(circle:Circle) ->Self {
        let mut rng = thread_rng();
        let dir_x = rng.gen_range(-1.0..1.);
        let dir_y = rng.gen_range(-1.0..1.);
        Self {
            circle,
            dir:vec2(dir_x,dir_y)
        }
    }

    fn move_ball(&mut self) {
         self.circle.x += self.dir.x * BALL_SPEED;
         self.circle.y += self.dir.y * BALL_SPEED;
        
        if self.circle.y > screen_height()-BALL_RADIUS || self.circle.y < 0.0 {
            self.dir.y = -self.dir.y;
        }
        
    }

    fn collision_with_paddle(&mut self, paddle_1 : &Rect, paddle_2 : &Rect) {
        let ball_rect = Rect::new(self.circle.x,self.circle.y,self.circle.r,self.circle.r);
        if ball_rect.intersect(*paddle_1).is_some() || ball_rect.intersect(*paddle_2).is_some() {
            self.dir.x = -self.dir.x;
        }
    } 


    fn draw(&self) {
        draw_circle(self.circle.x,self.circle.y,self.circle.r,BALL_COLOR);
    }
}

fn draw_scores(font:Font,scores:&(u32,u32)) {
        let text_params =  TextParams {
            font_size:70,
            font,
            ..Default::default()
        };

        draw_text_ex(&format!("{}",scores.0).as_str(),100., 100.,text_params);
        draw_text_ex(&format!("{}",scores.1).as_str(),screen_width()-100., 100.,text_params);
}

#[macroquad::main(conf)]
async fn main() {
  let mut paddle_left = Paddle::new(Rect::new(PADDLE_W,screen_height()/2.,PADDLE_W,PADDLE_H));
   let mut paddle_right = Paddle::new(Rect::new(screen_width()-PADDLE_W*2.,screen_height()/2.,PADDLE_W,PADDLE_H));
   let mut ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS)); 
    let mut scores:(u32,u32)= (0,0);

      let font =  load_ttf_font("./res/Roboto-Black.ttf")
        .await
        .unwrap();
    loop {
     clear_background(SKYBLUE);
     paddle_left.movement(KeyCode::W,KeyCode::S);
     paddle_right.movement(KeyCode::Up, KeyCode::Down);   
     ball.move_ball();
     ball.collision_with_paddle(&paddle_left.rect,&paddle_right.rect);
     ball.draw();
     paddle_left.draw();
     paddle_right.draw();

     if ball.circle.x < 0.0 {
         ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS));         
         scores.1 += 1;
     }else if ball.circle.x > screen_width() {
         ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS));        
         scores.0 += 1;
    }
        draw_scores(font,&scores);
     next_frame().await;
   }
}

```

# Useful Links

- [Macroquad Website](https://macroquad.rs)
- [Macroquad docs.rs](https://docs.rs/macroquad/latest/macroquad/)
- [Macroquad GitHub](https://github.com/not-fl3/macroquad)