A Simple Pong Clone

Games are fun!

Ping Pong

We have been playing with the minifb framebuffer Crate for rendering graphs on the screen, but you know what else gets rendered to the screen? That's right, games!

This is a very simple Pong clone that gives you 3 lives and uses the left and right cursor keys to move the bat. I can not be bothered to sort putting the lives and score on the screen... I might update in a Part 2. You get to see your score in the terminal when the game exits.

Cargo.toml:

[package]
name = "game_loop"
version = "0.1.0"
edition = "2021"

[dependencies]
minifb = "0.27.0"
                

main.rs

// simple pong like game with 3 lives
// using minifb to render game space
// by maths.earth

extern crate minifb;
use minifb::{Key, Window, WindowOptions};
use std::time::{Duration, Instant};

// Constants for window dimensions and frame timing
const WINDOW_WIDTH: usize = 800;
const WINDOW_HEIGHT: usize = 600;
const FRAME_TARGET_TIME: u64 = 16; // ~60 FPS
const PAUSE_DURATION: Duration = Duration::from_secs(2);

struct GameObject {
    x: f32,
    y: f32,
    width: f32,
    height: f32,
    vel_x: f32,
    vel_y: f32,
}

struct Game {
    window: Window,
    ball: GameObject,
    paddle: GameObject,
    last_frame_time: Instant,
    game_is_running: bool,
    lives: i32,
    score: i32,
    is_paused: bool,
    pause_start: Option<Instant>,
    ball_reset_pending: bool,
}

impl Game {
    fn new() -> Self {
        let window = Window::new(
            "Game Window",
            WINDOW_WIDTH,
            WINDOW_HEIGHT,
            WindowOptions::default(),
        )
        .unwrap_or_else(|e| {
            panic!("Error creating window: {}", e);
        });

        let ball = GameObject {
            x: 20.0,
            y: 20.0,
            width: 15.0,
            height: 15.0,
            vel_x: 300.0,
            vel_y: 300.0,
        };

        let paddle = GameObject {
            width: 100.0,
            height: 20.0,
            x: (WINDOW_WIDTH as f32 / 2.0) - 50.0,
            y: WINDOW_HEIGHT as f32 - 40.0,
            vel_x: 0.0,
            vel_y: 0.0,
        };

        Game {
            window,
            ball,
            paddle,
            last_frame_time: Instant::now(),
            game_is_running: true,
            lives: 3,
            score: 0,
            is_paused: false,
            pause_start: None,
            ball_reset_pending: false,
        }
    }

    fn process_input(&mut self) {
        // Handle input for exiting the game
        if self.window.is_key_down(Key::Escape) {
            self.game_is_running = false;
        }

        // Handle paddle movement input
        if !self.is_paused {
            if self.window.is_key_down(Key::Left) {
                self.paddle.vel_x = -400.0;
            } else if self.window.is_key_down(Key::Right) {
                self.paddle.vel_x = 400.0;
            } else {
                self.paddle.vel_x = 0.0;
            }
        }
    }

    fn update(&mut self) {
        // Handle pause state
        if self.is_paused {
            if let Some(start) = self.pause_start {
                if start.elapsed() >= PAUSE_DURATION {
                    self.is_paused = false;
                    self.pause_start = None;
                    self.ball_reset_pending = true;
                    self.reset_ball();
                } else {
                    return;
                }
            }
        }

        // Ensure ball reset is handled before updating positions
        if self.ball_reset_pending {
            self.ball_reset_pending = false;
            self.last_frame_time = Instant::now(); // Reset the frame time to avoid large delta time
            return;
        }

        // Calculate delta time for consistent movement
        let current_time = Instant::now();
        let delta_time = (current_time - self.last_frame_time).as_secs_f32();
        self.last_frame_time = current_time;

        // Update ball and paddle positions
        self.ball.x += self.ball.vel_x * delta_time;
        self.ball.y += self.ball.vel_y * delta_time;
        self.paddle.x += self.paddle.vel_x * delta_time;

        // Handle ball collision with window boundaries
        if self.ball.x <= 0.0 || self.ball.x + self.ball.width >= WINDOW_WIDTH as f32 {
            self.ball.vel_x = -self.ball.vel_x;
        }

        if self.ball.y <= 0.0 {
            self.ball.vel_y = -self.ball.vel_y;
        }

        // Handle ball collision with paddle
        if self.ball.y + self.ball.height >= self.paddle.y
            && self.ball.x + self.ball.width >= self.paddle.x
            && self.ball.x <= self.paddle.x + self.paddle.width
        {
            self.ball.vel_y = -self.ball.vel_y;
            self.score += 1;
        }

        // Prevent paddle from moving out of window boundaries
        if self.paddle.x <= 0.0 {
            self.paddle.x = 0.0;
        }

        if self.paddle.x >= WINDOW_WIDTH as f32 - self.paddle.width {
            self.paddle.x = WINDOW_WIDTH as f32 - self.paddle.width;
        }

        // Handle ball falling out of window (losing a life)
        if self.ball.y + self.ball.height > WINDOW_HEIGHT as f32 {
            self.lives -= 1;
            if self.lives > 0 {
                self.is_paused = true;
                self.pause_start = Some(Instant::now());
                // Move ball to a safe position off-screen before pausing
                self.ball.x = WINDOW_WIDTH as f32 / 2.0 - self.ball.width / 2.0;
                self.ball.y = WINDOW_HEIGHT as f32 / 2.0 - self.ball.height / 2.0;
                self.ball.vel_x = 0.0;
                self.ball.vel_y = 0.0;
            } else {
                self.game_is_running = false;
            }
        }
    }

    fn reset_ball(&mut self) {
        // Reset ball position and velocity
        self.ball.x = WINDOW_WIDTH as f32 / 2.0 - self.ball.width / 2.0;
        self.ball.y = WINDOW_HEIGHT as f32 / 2.0 - self.ball.height / 2.0;
        self.ball.vel_x = 300.0;
        self.ball.vel_y = 300.0;
    }

    fn render(&mut self, buffer: &mut [u32]) {
        // Clear the screen
        for i in buffer.iter_mut() {
            *i = 0;
        }

        // Render ball
        for y in 0..self.ball.height as usize {
            for x in 0..self.ball.width as usize {
                let index = (self.ball.y as usize + y) * WINDOW_WIDTH + (self.ball.x as usize + x);
                if index < buffer.len() {
                    buffer[index] = 0xFFFFFFFF;
                }
            }
        }

        // Render paddle
        for y in 0..self.paddle.height as usize {
            for x in 0..self.paddle.width as usize {
                let index = (self.paddle.y as usize + y) * WINDOW_WIDTH + (self.paddle.x as usize + x);
                if index < buffer.len() {
                    buffer[index] = 0xFFFFFFFF;
                }
            }
        }

        // Update window with buffer
        self.window.update_with_buffer(&buffer, WINDOW_WIDTH, WINDOW_HEIGHT).unwrap();
    }
}

fn main() {
    let mut game = Game::new();
    let mut buffer: Vec<u32> = vec![0; WINDOW_WIDTH * WINDOW_HEIGHT];

    // Main game loop
    while game.game_is_running && game.window.is_open() {
        game.process_input();
        game.update();
        game.render(&mut buffer);
        std::thread::sleep(Duration::from_millis(FRAME_TARGET_TIME));
    }

    println!("Game Over! Lives remaining: {}", game.lives);
    println!("Final Score: {}", game.score);
}
                

Mission Accomplished

And there we have it, a very simple Pong clone for you to play and improve on!