Introduction

Pong is one of the oldest games of all time. Pong is the most popular choice for a beginner game developer to start their game development journey. And in this post, we’ll be making it from stratch, specifically using the Rust programming language (no, not the game).

Required Tools

You should also know atleast a little about Rust. If you don’t then you should read the Rust Book which is free and online.

Initializing the project

We’re gonna create the project into a folder called pong-rs using the command:

cargo init pong-rs

Now, for the game framework, we’re gonna use the Macroquad crate. Macroquad is my personal favorite for making simple 2D games.
To use Macroquad, we’re gonna add it to our Cargo.toml file. For this post we’re gonna use version 0.3.

[dependencies]
macroquad = "0.3"

Now, just build the project:

cargo build

Creating a window

Open up src/main.rs in your favorite IDE and replace everything inside it with:

use macroquad::prelude::*;

fn window_config() -> Conf {
    Conf {
        window_title: "Pong-rs".to_string(),
        ..Default::default()
    }
}

#[macroquad::main(window_config)]
async fn main() {
    loop {
        clear_background(BLACK);
        next_frame().await;
    }
}

Now, run the project and you should get a window!

Explanation

Just copy and pasting code wouldn’t get you far in programming, so lets try to understand it.
First off,

use macroquad::prelude::*;

Most crates in Rust has a prelude module which includes all the required stuffs to use the crate and this line of code imports everything from it.

fn window_config() -> Conf {
    Conf {
        window_title: "Pong-rs".to_string(),
        ..Default::default()
    }
}

This function returns a config which Macroquad will use when creating a window. You can see more options here.

#[macroquad::main(window_config)]

This line of code calls a macro. We don’t need to know too much about this macro, just know that it creates a window and initializes the graphics API Macroquad uses. This macro also requires that our main function be async.

async function main() {
    loop {
        ...
    }
}

This block of code starts the main game loop. We’ll put all the game mechanics like moving the paddle and the ball in the loop block.

clear_background(BLACK);

Pretty self-explanatory. Clears the background with the color black.

Note: We can use BLACK directly as we imported it from the use statement earlier.

next_frame().await;

Waits until we can get the next frame we’ll draw into.

Making the paddle

The paddle’ll be a struct so that we can use it for both the left and right paddles.

struct Paddle {
    position: Vec2,
    size: Vec2,
}

The position and size member of the struct is a Vec2 or more commonly called a Vector2. The most basic explanaton of a vector is that it’s a collection of numbers. A vector can be used for either a coordinate or a direction or anything that needs two numbers.
For Macroquad, the coordinate system starts from the top-left with coordinate (0, 0) and ends with the bottom-right with coordinate (screen width, screen height).

Lets create all the methods we’ll use for this project.

impl Paddle {
    fn new(position: Vec2, size: Vec2) -> Paddle {
        todo!();
    }

    fn update(&mut self, speed: f32, up_key: KeyCode, down_key: KeyCode) {
        todo!();
    }

    fn draw(&self) {
        todo!();
    }
}

new

fn new(position: Vec2, size: Vec2) -> Paddle {
    Paddle {
        position,
        size,
    }
}

update

fn update(&mut self, speed: f32, up_key: KeyCode, down_key: KeyCode) {
    if is_key_down(up_key) {
        self.position.y -= speed;
    }
    if is_key_down(down_key) {
        self.position.y += speed;
    }
    self.position.y = self.position.y.clamp(0.0, screen_height() - self.size.y);
}

Explanation

if is_key_down(up_key) {
    self.position.y -= speed;
}

This block of code checks if up_key is being held down. If it is, then we move the paddle’s position upward by subtracting it’s y by speed. We subtract as Macroquad’s y axis is flipped.

if is_key_down(down_key) {
    self.position.y += speed;
}

Same as above but for the down movement.

self.position.y = self.position.y.clamp(0.0, screen_height() - self.size.y);

This line of code limits the paddle’s movement. When drawing later on, the rectangle’ll start from it’s top left, and so we need to limit it’s y movement to 0.0 (top) screen_height() - self.size.y (bottom) instead of 0.0 and screen_height().

draw

fn draw(&self) {
    draw_rectangle(self.position.x, self.position.y, self.size.x, self.size.y, WHITE);
}

A pretty self-explanatory function. Draws a rectangle at self.position with size self.size.

Updating our main function

Lets create the left paddle and right paddle.

let paddle_size = vec2(20.0, 60.0);
let speed = 10.0;
let mut left_paddle = Paddle::new(vec2(10.0, (screen_height() - paddle_size.y) / 2.0), paddle_size);
let mut right_paddle = Paddle::new(vec2(screen_width() - 10.0 - paddle_size.x, (screen_height() - paddle_size.y) / 2.0), paddle_size);

Don’t forget to update and draw them in the game loop!

left_paddle.update(speed, KeyCode::W, KeyCode::S);
left_paddle.draw();
right_paddle.update(speed, KeyCode::Up, KeyCode::Down);
right_paddle.draw();

Now run the project and you should have two paddles movable with the W, S, Up, and Down key.

The code so far

use macroquad::prelude::*;

fn window_config() -> Conf {
    Conf {
        window_title: "Pong-rs".to_string(),
        ..Default::default()
    }
}

struct Paddle {
    position: Vec2,
    size: Vec2,
}

impl Paddle {
    fn new(position: Vec2, size: Vec2) -> Paddle {
        Paddle {
            position,
            size,
        }
    }

    fn update(&mut self, speed: f32, up_key: KeyCode, down_key: KeyCode) {
        if is_key_down(up_key) {
            self.position.y -= speed;
        }
        if is_key_down(down_key) {
            self.position.y += speed;
        }
        self.position.y = self.position.y.clamp(0.0, screen_height() - self.size.y);
    }

    fn draw(&self) {
        draw_rectangle(self.position.x, self.position.y, self.size.x, self.size.y, WHITE);
    }
}

#[macroquad::main(window_config)]
async fn main() {
    let paddle_size = vec2(20.0, 60.0);
    let speed = 10.0;
    let mut left_paddle = Paddle::new(vec2(10.0, (screen_height() - paddle_size.y) / 2.0), paddle_size);
    let mut right_paddle = Paddle::new(vec2(screen_width() - 10.0 - paddle_size.x, (screen_height() - paddle_size.y) / 2.0), paddle_size);

    loop {
        clear_background(BLACK);

        left_paddle.update(speed, KeyCode::W, KeyCode::S);
        left_paddle.draw();
        right_paddle.update(speed, KeyCode::Up, KeyCode::Down);
        right_paddle.draw();

        next_frame().await;
    }
}

The ball

Lets also create a struct for the ball.

struct Ball {
    position: Vec2,
    direction: Vec2,
    size: Vec2,
}

Lets also create the needed methods.

impl Ball {
    fn new(position: Vec2, direction: Vec2, size: Vec2) -> Ball {
        todo!();
    }

    fn update(&mut self, speed: f32, left_paddle: &Paddle, right_paddle: &Paddle) {
        todo!();
    }

    fn colliding(&self, paddle: &Paddle) -> bool {
        todo!();
    }

    fn draw(&self) {
        todo!();
    }
}

new

fn new(position: Vec2, direction: Vec2, size: Vec2) -> Ball {
    Ball {
        position,
        direction,
        size,
    }
}

update

fn update(&mut self, speed: f32, left_paddle: &Paddle, right_paddle: &Paddle) {
    if self.colliding(left_paddle) {
        self.direction.x = 1.0;
    }
    if self.colliding(right_paddle) {
        self.direction.x = -1.0;
    }
    if self.position.y <= 0.0 {
        self.direction.y = 1.0;
    }
    if self.position.y >= screen_height() - self.size.y {
        self.direction.y = -1.0;
    }
    self.position += self.direction * speed;
}

A simple function so I’m just gonna gloss over it. Basically, checks if is colliding with a paddle or a wall, and if it is then flip the ball’s direction.

colliding

fn colliding(&self, paddle: &Paddle) -> bool {
    return
        (self.position.x + self.size.x > paddle.position.x && self.position.x < paddle.position.x + paddle.size.x) &&
        (self.position.y + self.size.y > paddle.position.y && self.position.y < paddle.position.y + paddle.size.y);
}

draw

fn draw(&self) {
    draw_rectangle(self.position.x, self.position.y, self.size.x, self.size.y, WHITE);
}

Updating our main function

use macroquad::rand::gen_range;

let ball_size = vec2(20.0, 20.0);
let mut ball = Ball::new((vec2(screen_width(), screen_height()) - ball_size) / 2.0, vec2(gen_range(0, 2) as f32, gen_range(0, 2) as f32) * 2.0 - 1.0, ball_size);

We create the ball with a random direction. Macroquad has a built-in random number generator so we’ll use that. We generate a number that is either 0 or 1 (gen_range takes an inclusive start and an exclusive end) then multiply it by 2 to get 0 or 2 and then subtract 1 to get either -1 or 1 so that we don’t get a 0.

Lets update them in the game loop.

ball.update(speed / 2.0, &left_paddle, &right_paddle);
ball.draw();

We move the ball half as fast as the paddle so that the players actually have a chance of playing.

The code so far

use macroquad::prelude::*;

fn window_config() -> Conf {
    Conf {
        window_title: "Pong-rs".to_string(),
        ..Default::default()
    }
}

struct Paddle {
    position: Vec2,
    size: Vec2,
}

impl Paddle {
    fn new(position: Vec2, size: Vec2) -> Paddle {
        Paddle {
            position,
            size,
        }
    }

    fn update(&mut self, speed: f32, up_key: KeyCode, down_key: KeyCode) {
        if is_key_down(up_key) {
            self.position.y -= speed;
        }
        if is_key_down(down_key) {
            self.position.y += speed;
        }
        self.position.y = self.position.y.clamp(0.0, screen_height() - self.size.y);
    }

    fn draw(&self) {
        draw_rectangle(self.position.x, self.position.y, self.size.x, self.size.y, WHITE);
    }
}

struct Ball {
    position: Vec2,
    direction: Vec2,
    size: Vec2,
}

impl Ball {
    fn new(position: Vec2, direction: Vec2, size: Vec2) -> Ball {
        Ball {
            position,
            direction,
            size,
        }
    }
    
    fn update(&mut self, speed: f32, left_paddle: &Paddle, right_paddle: &Paddle) {
        if self.colliding(left_paddle) {
            self.direction.x = 1.0;
        }
        if self.colliding(right_paddle) {
            self.direction.x = -1.0;
        }
        if self.position.y <= 0.0 {
            self.direction.y = 1.0;
        }
        if self.position.y >= screen_height() - self.size.y {
            self.direction.y = -1.0;
        }
        self.position += self.direction * speed;
    }
    
    fn colliding(&self, paddle: &Paddle) -> bool {
        return
            (self.position.x + self.size.x > paddle.position.x && self.position.x < paddle.position.x + paddle.size.x) &&
            (self.position.y + self.size.y > paddle.position.y && self.position.y < paddle.position.y + paddle.size.y);
    }
    
    fn draw(&self) {
        draw_rectangle(self.position.x, self.position.y, self.size.x, self.size.y, WHITE);
    }
}

#[macroquad::main(window_config)]
async fn main() {
    let paddle_size = vec2(20.0, 60.0);
    let speed = 10.0;
    let mut left_paddle = Paddle::new(vec2(10.0, (screen_height() - paddle_size.y) / 2.0), paddle_size);
    let mut right_paddle = Paddle::new(vec2(screen_width() - 10.0 - paddle_size.x, (screen_height() - paddle_size.y) / 2.0), paddle_size);

    let ball_size = vec2(20.0, 20.0);
    let mut ball = Ball::new((vec2(screen_width(), screen_height()) - ball_size) / 2.0, vec2(gen_range(0, 2) as f32, gen_range(0, 2) as f32) * 2.0 - 1.0, ball_size);

    loop {
        clear_background(BLACK);

        left_paddle.update(speed, KeyCode::W, KeyCode::S);
        left_paddle.draw();
        right_paddle.update(speed, KeyCode::Up, KeyCode::Down);
        right_paddle.draw();

        ball.update(speed / 2.0, &left_paddle, &right_paddle);
        ball.draw();

        next_frame().await;
    }
}

Adding the scores

The last thing we need to finish this project is the scores. Add this code to your main function.

let mut left_score = 0;
let mut right_score = 0;

Now, lets draw them.

let text_measurement = measure_text(&left_score.to_string(), None, 64, 1.0);
draw_text(&left_score.to_string(), (screen_width() - text_measurement.width) / 2.0 - 100.0, 50.0, 64.0, WHITE);
let text_measurement = measure_text(&right_score.to_string(), None, 64, 1.0);
draw_text(&right_score.to_string(), (screen_width() - text_measurement.width) / 2.0 + 100.0, 50.0, 64.0, WHITE);

The measure_text function returns a TextDimensions which makes it easy to center a text.

Lets update the score if the ball goes outside of the screen.

if ball.position.x <= 0.0 {
    right_score += 1;
    left_paddle = Paddle::new(vec2(10.0, (screen_height() - paddle_size.y) / 2.0), paddle_size);
    right_paddle = Paddle::new(vec2(screen_width() - 10.0 - paddle_size.x, (screen_height() - paddle_size.y) / 2.0), paddle_size);
    ball = Ball::new((vec2(screen_width(), screen_height()) - ball_size) / 2.0, vec2(gen_range(0, 2) as f32, gen_range(0, 2) as f32) * 2.0 - 1.0, ball_size);
}
if ball.position.x >= screen_width() - ball.size.x {
    left_score += 1;
    left_paddle = Paddle::new(vec2(10.0, (screen_height() - paddle_size.y) / 2.0), paddle_size);
    right_paddle = Paddle::new(vec2(screen_width() - 10.0 - paddle_size.x, (screen_height() - paddle_size.y) / 2.0), paddle_size);
    ball = Ball::new((vec2(screen_width(), screen_height()) - ball_size) / 2.0, vec2(gen_range(0, 2) as f32, gen_range(0, 2) as f32) * 2.0 - 1.0, ball_size);
}

We check if the ball goes outside of the screen, and if it does, we update the score and reset all the paddles and the ball.

And the project is done!

The finished code

use macroquad::prelude::*;

fn window_config() -> Conf {
    Conf {
        window_title: "Pong-rs".to_string(),
        ..Default::default()
    }
}

struct Paddle {
    position: Vec2,
    size: Vec2,
}

impl Paddle {
    fn new(position: Vec2, size: Vec2) -> Paddle {
        Paddle {
            position,
            size,
        }
    }

    fn update(&mut self, speed: f32, up_key: KeyCode, down_key: KeyCode) {
        if is_key_down(up_key) {
            self.position.y -= speed;
        }
        if is_key_down(down_key) {
            self.position.y += speed;
        }
        self.position.y = self.position.y.clamp(0.0, screen_height() - self.size.y);
    }

    fn draw(&self) {
        draw_rectangle(self.position.x, self.position.y, self.size.x, self.size.y, WHITE);
    }
}

struct Ball {
    position: Vec2,
    direction: Vec2,
    size: Vec2,
}

impl Ball {
    fn new(position: Vec2, direction: Vec2, size: Vec2) -> Ball {
        Ball {
            position,
            direction,
            size,
        }
    }
    
    fn update(&mut self, speed: f32, left_paddle: &Paddle, right_paddle: &Paddle) {
        if self.colliding(left_paddle) {
            self.direction.x = 1.0;
        }
        if self.colliding(right_paddle) {
            self.direction.x = -1.0;
        }
        if self.position.y <= 0.0 {
            self.direction.y = 1.0;
        }
        if self.position.y >= screen_height() - self.size.y {
            self.direction.y = -1.0;
        }
        self.position += self.direction * speed;
    }
    
    fn colliding(&self, paddle: &Paddle) -> bool {
        return
            (self.position.x + self.size.x > paddle.position.x && self.position.x < paddle.position.x + paddle.size.x) &&
            (self.position.y + self.size.y > paddle.position.y && self.position.y < paddle.position.y + paddle.size.y);
    }
    
    fn draw(&self) {
        draw_rectangle(self.position.x, self.position.y, self.size.x, self.size.y, WHITE);
    }
}

#[macroquad::main(window_config)]
async fn main() {
    let paddle_size = vec2(20.0, 60.0);
    let speed = 10.0;
    let mut left_paddle = Paddle::new(vec2(10.0, (screen_height() - paddle_size.y) / 2.0), paddle_size);
    let mut right_paddle = Paddle::new(vec2(screen_width() - 10.0 - paddle_size.x, (screen_height() - paddle_size.y) / 2.0), paddle_size);

    use macroquad::rand::gen_range;

    let ball_size = vec2(20.0, 20.0);
    let mut ball = Ball::new((vec2(screen_width(), screen_height()) - ball_size) / 2.0, vec2(gen_range(0, 1) as f32, gen_range(0, 1) as f32) * 2.0 - 1.0, ball_size);

    let mut left_score = 0;
    let mut right_score = 0;

    loop {
        clear_background(BLACK);

        left_paddle.update(speed, KeyCode::W, KeyCode::S);
        left_paddle.draw();
        right_paddle.update(speed, KeyCode::Up, KeyCode::Down);
        right_paddle.draw();

        ball.update(speed / 2.0, &left_paddle, &right_paddle);
        ball.draw();

        let text_measurement = measure_text(&left_score.to_string(), None, 64, 1.0);
        draw_text(&left_score.to_string(), (screen_width() - text_measurement.width) / 2.0 - 100.0, 50.0, 64.0, WHITE);
        let text_measurement = measure_text(&right_score.to_string(), None, 64, 1.0);
        draw_text(&right_score.to_string(), (screen_width() - text_measurement.width) / 2.0 + 100.0, 50.0, 64.0, WHITE);

        if ball.position.x <= 0.0 {
            right_score += 1;
            left_paddle = Paddle::new(vec2(10.0, (screen_height() - paddle_size.y) / 2.0), paddle_size);
            right_paddle = Paddle::new(vec2(screen_width() - 10.0 - paddle_size.x, (screen_height() - paddle_size.y) / 2.0), paddle_size);
            ball = Ball::new((vec2(screen_width(), screen_height()) - ball_size) / 2.0, vec2(gen_range(0, 2) as f32, gen_range(0, 2) as f32) * 2.0 - 1.0, ball_size);
        }
        if ball.position.x >= screen_width() - ball.size.x {
            left_score += 1;
            left_paddle = Paddle::new(vec2(10.0, (screen_height() - paddle_size.y) / 2.0), paddle_size);
            right_paddle = Paddle::new(vec2(screen_width() - 10.0 - paddle_size.x, (screen_height() - paddle_size.y) / 2.0), paddle_size);
            ball = Ball::new((vec2(screen_width(), screen_height()) - ball_size) / 2.0, vec2(gen_range(0, 2) as f32, gen_range(0, 2) as f32) * 2.0 - 1.0, ball_size);
        }

        next_frame().await;
    }
}

Ending off

Congratulations! You’ve just made Pong from scratch without a game engine in Rust! That wasn’t so hard now was it? If you want to delve deeper into making games from scratch you should make another simple game. My recommendations would be Flappy Bird, Breakout, Space Invaders, and other Atari games since most of those are simple.
Anyways hope you had fun reading this post and I apologize if I made any mistake. Goodbye!