Psycho Pong (part 1)

Who says first article, says first game. And who says first game (on this blog) says Pong... Finally, something that will look a little bit like it.

The concept of Pong comes from.......a long time ago.

However, there is something to have fun with, in particular by modifying it a little. Pong is very nice, but either you have to find another player, or play against a wall (boring), or code an AI.

We will see a little different gameplay this time: the same player plays both paddles at the same time. I admit I even want to make one with four simultaneous paddles...

I will here use Rust and macroquad and I will try to show you that Rust is not that difficult (in many cases), but on the contrary, it can bring you a lot of things.

Macroquad is quite easy to use and removes, in my opinion, a lot of roughness from Rust. In addition, it is light and minimalist, and "cerise sur le gâteau", clearly cross-platform.

I will place myself from a beginner's point of view in Rust.

Project setup

After creating project with cargo new --bin psycho_pong, we add the dependencies in Cargo.toml

[dependencies]
macroquad = "0.3.24"

Then, we modify our main.rs file with the classical Macorquad's main function.

use macroquad::prelude::*;

const WIDTH: i32 = 480;
const HEIGHT: i32 = 480;

#[macroquad::main(window_conf)]
async fn main() {
    // Game initialization are here
    loop {
        // Update logic
        // Handle input
        // Draw stuff

        clear_background(BLACK);
        next_frame().await;
    }
}

fn window_conf() -> Conf {
    Conf {
        window_title: "Psycho Pong".to_owned(),
        window_width: WIDTH,
        window_height: HEIGHT,
        fullscreen: false,
        ..Default::default()
    }
}

Don't be afraid, Rust's async and await are just used to solve the cross platform main loop organization, nothing else.

At this point , it's not really fancy: if we compile this, we get a nice little window, black, empty, very empty, sadly empty.

Our first two paddles

It's time to add a little more visual: a paddle

Basically, we can imagine a paddle like a simple rectangle and define it like a structure:

struct Paddle {
    x: f32,     // x position
    y: f32,     // y position
    w: f32,     // width (the x-axis size )
    h: f32      // height (the y-axis size)
}

And use the macroquad's draw_rectangle() function to draw each paddle, and add an update(paddle_one: Paddle, paddle_two: Paddle) function like this in the main loop:

async fn main() {
    let mut paddle_one = Paddle{
        x: 0.5 * (WIDTH - 100) as f32,
        y: 5.0,
        w: 100.0,
        h: 20.0};

    let mut paddle_two = Paddle{
        x: 0.5 * (WIDTH - 100) as f32, 
        y: HEIGHT as f32 - 25.0, 
        w: 100.0,
        h: 20.0};

    loop {

        // Handle input for the first paddle
        if is_key_down(KeyCode::Left) {
            paddle_one.x -= 4.0;
        }
        else if is_key_down(KeyCode::Right) {
            paddle_one.x += 4.0;
        }

        // Handle input for the second paddle
        if is_key_down(KeyCode::A) {
            paddle_two.x -= 4.0;
        }
        else if is_key_down(KeyCode::D) {
            paddle_two.x += 4.0;
        }

        clear_background(BLACK);  // Scene cleanup

        // draw each paddle
        draw_rectangle(
            paddle_one.x,
            paddle_one.y,
            paddle_one.w,
            paddle_one.h,
            WHITE);
        draw_rectangle(
            paddle_two.x,
            paddle_two.y,
            paddle_two.w,
            paddle_two.h,
            WHITE);

        next_frame().await;
    }
}

So we have come to this...

First two paddles

Clearly, it's not a very elegant route, and which will quickly become tedious when it comes to managing four paddles. In addition, the movement of the paddles lacks flexibility.

Some generalizations for our four paddles

Good. We will try to generalize this, by implementing some functionality to the Paddle structure. At the same time, we will now create four paddles. One on each edge of the screen

First, we can generalize a little more the creation of the paddles according to their position on the screen. For this, let's create an enum describing the position of the paddle and use it for the creation:


// Our enum that discribe the paddle's position
enum PaddleType {
    Top,
    Down,
    Left,
    Right,
}

// And our paddle structure
struct Paddle {
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    name: PaddleType,
}
impl Paddle {
    fn new(name: PaddleType) -> Paddle {
        match name {
            PaddleType::Top => Paddle {
                x: 0.5 * (WIDTH - 100) as f32,
                y: 5.0,
                w: 100.0,
                h: 20.0,
                name,
            },
            PaddleType::Down => Paddle {
                x: 0.5 * (WIDTH - 100) as f32,
                y: HEIGHT as f32 - 25.0,
                w: 100.0,
                h: 20.0,
                name,
            },
            PaddleType::Left => Paddle {
                x: 5.0,
                y: 0.5 * (HEIGHT - 100) as f32,
                w: 20.0,
                h: 100.0,
                name,
            },
            PaddleType::Right => Paddle {
                x: WIDTH as f32 - 25.0,
                y: 0.5 * (HEIGHT - 100) as f32,
                w: 20.0,
                h: 100.0,
                name,
            },
        }
    }
}

And now, if we create four paddle, and draw them with :

async fn main() {
    let mut paddle_one = Paddle::new(PaddleType::Top);
    let mut paddle_two = Paddle::new(PaddleType::Down);
    let mut paddle_three = Paddle::new(PaddleType::Left);
    let mut paddle_four = Paddle::new(PaddleType::Right);

    loop {
        // .....
        clear_background(BLACK);
        draw_rectangle(
            paddle_one.x,
            paddle_one.y,
            paddle_one.w,
            paddle_one.h,
            WHITE,
        );
        draw_rectangle(
            paddle_two.x,
            paddle_two.y,
            paddle_two.w,
            paddle_two.h,
            WHITE,
        );
        draw_rectangle(
            paddle_three.x,
            paddle_three.y,
            paddle_three.w,
            paddle_three.h,
            WHITE,
        );
        draw_rectangle(
            paddle_four.x,
            paddle_four.y,
            paddle_four.w,
            paddle_four.h,
            WHITE,
        );
        next_frame().await
    }
}

As you can see, it's a bit long... And we don't want to read code that just repeats itself. Draw...draw...draw...draw. Some code rewriting will do us the greatest good.

So let's replace all that horrible code with this:

async fn main() {
    // We create our four paddles, on each side of the screen,
    // stored in an Array
    let mut paddle_list: [Paddle; 4] = [
        Paddle::new(PaddleType::Top),
        Paddle::new(PaddleType::Down),
        Paddle::new(PaddleType::Left),
        Paddle::new(PaddleType::Right),
    ];
    
    loop {
        clear_background(BLACK);

        // And we can iterate over our list
        for paddle in paddle_list.iter() {
            draw_rectangle(
                paddle.x,
                paddle.y,
                paddle.w,
                paddle.h,
                WHITE);
        }

        next_frame().await;
    }
}

It's still more readable and pleasant to see now, we have greatly reduced the previous boring repetitions. Now it's time to move it's paddle, each with the controls assigned to it. As much before we had two paddles, now we have four.

To handle the input command, we can use a classic method

// inputs for paddle one
if is_key_down(KeyCode::Left) { /* Do something */ } 
else if is_key_down(KeyCode::Right) { /* Do something */ }

// inputs for paddle two
if is_key_down(KeyCode::A) { /* Do something */ } 
else if is_key_down(KeyCode::D) { /* Do something */ }

// inputs for paddle three
if is_key_down(KeyCode::W) { /* Do something */ } 
else if is_key_down(KeyCode::S) { /* Do something */ }

// inputs for paddle four
if is_key_down(KeyCode::Up) { /* Do something */ }
else if is_key_down(KeyCode::Down) { /* Do something */ }

But let's try another way. First we create a function that return the direction of motion (1.0 or -1.0) given specific KeyCode inputs

pub fn get_direction(key_a: KeyCode, key_b:KeyCode) -> f32 {
    let left = if is_key_down(key_a) {1.0} else {0.0};
    let right = if is_key_down(key_b) {1.0} else {0.0};

    right - left
}

and use it to update the position of each paddle. But wait !! Which update function ?!? In impl Paddle{} of course...

fn update(&mut self, direction: f32) {
    //The movements are different
    //when the paddles are vertical or horizontal
    match self.name {
        PaddleType::Left | PaddleType::Right => {self.y += 4.0 * direction},
        PaddleType::Down | PaddleType::Top => {self.x += 4.0 * direction}
    }

    // And the paddle need to stay in the screen
    if self.x < 0.0 {
        self.x = 0.0;
    }
    else if self.x > WIDTH as f32 - self.w {
        self.x = WIDTH as f32 - self.w;
    }

    if self.y < 0.0 {
        self.y = 0.0;
    }
    else if self.y > HEIGHT as f32 - self.h {
        self.y = HEIGHT as f32 - self.h;
    }

}

And we can use this update function in our main loop

paddle_list[0].update(get_direction(KeyCode::Left, KeyCode::Right));
paddle_list[1].update(get_direction(KeyCode::A, KeyCode::D));
paddle_list[2].update(get_direction(KeyCode::W, KeyCode::S));
paddle_list[3].update(get_direction(KeyCode::Up, KeyCode::Down));

As we have seen before, we can avoid some repetitions by using an iterator. But in this case, we need mutability because pressing a key will modify the position of our paddle (and therefore modify certain data specific to each paddle), so we have to use .iter_mut() instead of .iter().

So, in our main loop, we can replace the old four paddles update code with this one

for paddle in paddle_list.iter_mut() {
    paddle.update(get_direction(paddle.command.0, paddle.command.1));
}

And now we have our four paddles that can move around while staying on screen...

Of course, this is just a suggestion, one way, and there are plenty of other ways to deal with this situation.

Et Voilà !

You can test (macroquad allows to generate wasm) now >> here <<.

You can also download the source file of this article here

The next step will therefore be to add a ball and try to play, each hand directing two paddles, which may give us some headaches

Command

  • paddle one (top) : Arrow left / Arrow right
  • paddle two (bottom) : A / D (or Q / D for azerty keyboard)
  • paddle three (right) : Arrow up / Arrow down
  • paddle four (left) : W / S (or Z / S for azerty keyboard)

1408 Words

2022-10-18