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!