Homemade pong game

Hello, I made a pong game from scratch with the sdl2 bindings and I am looking for feedback on my code.

The code is hosted here, but for simplicity I put the content of the files below:

main.rs

extern crate sdl2;

use crate::entity::*;
use crate::view::*;

use sdl2::pixels::Color;
use sdl2::event::Event;
use sdl2::EventPump;
use sdl2::keyboard::Keycode;

mod entity;
mod view;

const WINDOW_WIDTH: u32 = 800;
const WINDOW_HEIGHT: u32 = 600;
const WINDOW_TITLE: &str = "pong";

const SCREEN_MARGIN: i32 = 10;

const FRAME_DURATION: u32 = 50;

struct FrameEvent;

pub fn main() {
    let sdl_context = sdl2::init().unwrap();
    let video_subsystem = sdl_context.video().unwrap();

    let window = video_subsystem.window(WINDOW_TITLE, WINDOW_WIDTH, WINDOW_HEIGHT)
        .position_centered()
        .build()
        .unwrap();

    let mut canvas = window.into_canvas().build().unwrap();

    game_loop(&sdl_context, &mut canvas);

}

fn game_loop(context: &sdl2::Sdl,
             canvas: &mut sdl2::render::Canvas<sdl2::video::Window>) {

    let mut gs: GameState;
    let mut event_pump = context.event_pump().unwrap();
    let ev = context.event().unwrap();
    ev.register_custom_event::<FrameEvent>().unwrap();

    let timer_subsystem = context.timer().unwrap();
    let _timer = timer_subsystem.add_timer(
        FRAME_DURATION,
        Box::new(|| {
            ev.push_custom_event(FrameEvent).unwrap();
            FRAME_DURATION
        }),
    );
    'game_loop: loop {
        gs = initialize_game_state();
        while !gs.is_game_over && !gs.is_game_restarted {
            handle_game_events(&mut gs, &mut event_pump, canvas);
            handle_ball_out_of_border(&mut gs); 
        }
        if !gs.is_game_restarted {
            break 'game_loop;
        }
    }
}

fn handle_game_events(gs: &mut GameState, event_pump: &mut EventPump, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>){
    let event = event_pump.wait_event();
    if event.is_user_event() {
        handle_collisions(gs);

        gs.ball.update_position();

        update_cpu_racket(gs);

        canvas.set_draw_color(Color::BLACK);
        canvas.clear();
        draw_game(&gs, canvas);
        canvas.present();

    }else {
        match event {
            Event::Quit {..} |
            Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
                gs.is_game_over = true;
            },
            Event::KeyDown { keycode: Some(Keycode::Up), .. } => {
                gs.racket_1.move_up();
            },
            Event::KeyDown { keycode: Some(Keycode::Down), .. } => {
                gs.racket_1.move_down();
            },
            Event::KeyDown { keycode: Some(Keycode::Space), .. } => {
                gs.is_game_restarted = true;
            },
            _ => {}
        }
    }
}

fn handle_collisions(gs: &mut GameState){
    if gs.ball.has_collision_with(&gs.racket_1) {
        let cp = gs.ball.collision_point_with(&gs.racket_1);
        if cp == 0 { gs.ball.direction = Direction::EAST; }
        else if cp > 0 { gs.ball.direction = Direction::SOUTHEAST; }
        else {gs.ball.direction = Direction::NORTHEAST; }
        gs.ball.increase_speed();
    }

    if gs.ball.has_collision_with(&gs.racket_2) {
        let cp = gs.ball.collision_point_with(&gs.racket_2);
        if cp == 0 { gs.ball.direction = Direction::WEST; }
        else if cp > 0 { gs.ball.direction = Direction::SOUTHWEST; }
        else { gs.ball.direction = Direction::NORTHWEST; }
        gs.ball.increase_speed();
    }

    if gs.ball.has_collision_with_ceiling() {
        if gs.ball.direction == Direction::NORTHWEST {
            gs.ball.direction = Direction::SOUTHWEST;
        }else {
            gs.ball.direction = Direction::SOUTHEAST;
        }
    }

    if gs.ball.has_collision_with_floor() {
        if gs.ball.direction == Direction::SOUTHWEST {
            gs.ball.direction = Direction::NORTHWEST;
        }else {
            gs.ball.direction = Direction::NORTHEAST;
        }
    }
}

fn handle_ball_out_of_border(gs: &mut GameState){
    if gs.ball.pos_x < 0 {
        gs.score_p2 += 1;
        println!("p2 scored!, total: {}-{}", gs.score_p1, gs.score_p2);
        gs.reset_positions();
    }
    else if gs.ball.pos_x > WINDOW_WIDTH as i32 {
        gs.score_p1 += 1;
        println!("p1 score!, total: {}-{}", gs.score_p1, gs.score_p2);
        gs.reset_positions();
    }
}

fn update_cpu_racket(gs: &mut GameState) {
    if gs.ball.direction == Direction::SOUTH ||
        gs.ball.direction == Direction::SOUTHWEST ||
        gs.ball.direction == Direction::SOUTHEAST {
            gs.racket_2.pos_y += gs.racket_2.speed;
        }
    if gs.ball.direction == Direction::NORTH ||
        gs.ball.direction == Direction::NORTHEAST ||
        gs.ball.direction == Direction::NORTHWEST {
            gs.racket_2.pos_y -= gs.racket_2.speed;
        }
}

entity.rs

use crate::WINDOW_WIDTH;
use crate::WINDOW_HEIGHT;
use crate::SCREEN_MARGIN;

// Speed of the ball in terms of pixels/frame.
const BALL_INIT_SPEED: i32 = 10;
const BALL_MAX_SPEED: i32 = 15;

// Set the racket speed as 90% of the ball speed.
const RACKET_SPEED: i32 = ((BALL_INIT_SPEED as f32) * 0.9) as i32;
const RACKET_WIDTH: u32 = 10;
const RACKET_HEIGHT: u32 = WINDOW_HEIGHT / 8;

use sdl2::pixels::Color;
use rand::random;

#[derive(Copy, Clone, PartialEq)]
pub enum Direction {
    NORTH,
    SOUTH,
    EAST,
    WEST,
    NORTHWEST,
    NORTHEAST,
    SOUTHWEST,
    SOUTHEAST,
}

pub struct GameState {
    pub ball: Ball,
    pub racket_1: Racket,
    pub racket_2: Racket,
    pub is_game_over: bool,
    pub is_game_restarted: bool,
    pub score_p1: u32,
    pub score_p2: u32,
}

impl GameState {
    pub fn reset_positions(&mut self){
        self.ball.pos_x = (WINDOW_WIDTH / 2) as i32;
        self.ball.pos_y = (WINDOW_HEIGHT / 2) as i32;
        self.ball.speed = BALL_INIT_SPEED;
        self.racket_1.pos_y = (WINDOW_HEIGHT / 2 - self.racket_1.height / 2) as i32;
        self.racket_2.pos_y = (WINDOW_HEIGHT / 2 - self.racket_2.height / 2) as i32;

        if random::<u8>() % 2 == 0 {
            self.ball.direction = Direction::EAST;
        }else {
            self.ball.direction = Direction::WEST;
        }
    }
}

#[derive(Copy, Clone)]
pub struct Ball {
    pub pos_x: i32,
    pub pos_y: i32,
    pub radius: i32,
    pub direction: Direction,
    pub speed: i32, // the number of pixels/frame
    pub color: sdl2::pixels::Color,
}

impl Ball {
    pub fn update_position(&mut self) {
        match self.direction {
            Direction::NORTH => { self.pos_y -= self.speed; }
            Direction::SOUTH => { self.pos_y += self.speed; }
            Direction::WEST => { self.pos_x -= self.speed; }
            Direction::EAST => { self.pos_x += self.speed; }
            Direction::NORTHEAST => {
                self.pos_x += self.speed;
                self.pos_y -= self.speed;
            }
            Direction::NORTHWEST => {
                self.pos_x -= self.speed;
                self.pos_y -= self.speed;
            }
            Direction::SOUTHEAST => {
                self.pos_x += self.speed;
                self.pos_y += self.speed;
            }
            Direction::SOUTHWEST => {
                self.pos_x -= self.speed;
                self.pos_y += self.speed;
            }
        }
    }

    pub fn has_collision_with(self, racket: &Racket) -> bool {

        let y_collision = self.pos_y + self.radius >= racket.pos_y && self.pos_y - self.radius <= racket.pos_y + racket.height as i32;
        let x_left_collision = self.pos_x + self.radius >= racket.pos_x && self.pos_x + self.radius <= racket.pos_x + racket.width as i32;
        let x_right_collision = self.pos_x - self.radius <= racket.pos_x + racket.width as i32 && self.pos_x - self.radius >= racket.pos_x;

        return  x_left_collision && y_collision || x_right_collision && y_collision;
    }

    pub fn collision_point_with(self, racket: &Racket) -> i32 {
        if self.pos_y == racket.pos_y + racket.height as i32 / 2 {
            return 0;
        }
        return if self.pos_y > racket.pos_y + racket.height as i32 / 2  { 1 } else { - 1 };
    }

    pub fn has_collision_with_ceiling(self) -> bool {
        return self.pos_y - self.radius <= 0;
    }

    pub fn has_collision_with_floor(self) -> bool {
        return self.pos_y + self.radius >= WINDOW_HEIGHT as i32;
    }

    pub fn increase_speed(&mut self){
        if self.speed < BALL_MAX_SPEED {
            self.speed += 1;
        }
    }
}

pub struct Racket {
    pub pos_x: i32,
    pub pos_y: i32,
    pub height: u32,
    pub width: u32,
    pub speed: i32,
    pub color: sdl2::pixels::Color,
}

impl Racket {
    pub fn move_up(&mut self){
        if self.pos_y > 0 {
            self.pos_y = self.pos_y - self.speed;
        }
    }

    pub fn move_down(&mut self){
        if self.pos_y < (WINDOW_HEIGHT - self.height) as i32 {
            self.pos_y = self.pos_y + self.speed;
        }
    }
}

pub fn initialize_game_state() -> GameState {
    return GameState {
        ball: initialize_ball(
            WINDOW_WIDTH as i32 / 2,
            WINDOW_HEIGHT as i32 / 2,
            10,
            Direction::EAST,
            BALL_INIT_SPEED,
            Color::RGB(255,140,0),
        ),
        racket_1: initialize_racket(
            SCREEN_MARGIN,
            (WINDOW_HEIGHT / 2  - RACKET_HEIGHT / 2) as i32,
            RACKET_HEIGHT,
            RACKET_WIDTH,
            RACKET_SPEED,
            Color::WHITE
        ),
        racket_2: initialize_racket(
            WINDOW_WIDTH as i32 - SCREEN_MARGIN - RACKET_WIDTH as i32,
            (WINDOW_HEIGHT / 2  - RACKET_HEIGHT / 2) as i32,
            RACKET_HEIGHT,
            RACKET_WIDTH,
            RACKET_SPEED,
            Color::WHITE
        ),
        is_game_over: false,
        is_game_restarted: false,
        score_p1: 0,
        score_p2: 0,
    };
}

pub fn initialize_racket(x: i32, y: i32, h: u32, w: u32, s: i32, c: Color) -> Racket {
    return Racket {
        pos_x: x,
        pos_y: y,
        height: h,
        width: w,
        speed: s,
        color: c,
    };
}

pub fn initialize_ball(x: i32, y: i32, r: i32, d: Direction, s: i32, c: Color) -> Ball {
    return Ball {
        pos_x: x,
        pos_y: y,
        radius: r,
        direction: d,
        speed: s,
        color: c,
    };
}

view.rs

use crate::sdl2::gfx::primitives::DrawRenderer;

use crate::entity::GameState;
use crate::entity::Ball;
use crate::entity::Racket;
use crate::WINDOW_WIDTH;
use crate::WINDOW_HEIGHT;

use std::path::Path;

use sdl2::pixels::Color;
use sdl2::rect::Point;
use sdl2::rect::Rect;

const HALFWAY_LINE_DASHES: i32 = 20;

fn draw_racket(racket: &Racket, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>){
    canvas.set_draw_color(racket.color);
    let rectangle: Rect = Rect::new(racket.pos_x, racket.pos_y, racket.width, racket.height);
    let r = canvas.fill_rect(rectangle);
    if r.is_err() {
        panic!("racket could not be drawn");
    }
}

fn draw_ball(ball: &Ball, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>){
    canvas.set_draw_color(ball.color);
    let r = canvas.filled_circle(ball.pos_x as i16, ball.pos_y as i16, ball.radius as i16, ball.color);
    if r.is_err() {
        panic!("ball could not be drawn!");
    }
}

pub fn draw_halfway_line(canvas: &mut sdl2::render::Canvas<sdl2::video::Window>){
    canvas.set_draw_color(Color::WHITE);
    let middle_x = (WINDOW_WIDTH / 2) as i32 - 2;
    let dash_length = WINDOW_HEIGHT as i32 / (HALFWAY_LINE_DASHES * 2);
    let margin_top = dash_length / 2;
    for i in 0..(HALFWAY_LINE_DASHES * 2) {
        if i % 2 == 0 {
            let p1 = Point::new(middle_x, margin_top + i * dash_length);
            let p2 = Point::new(middle_x, margin_top + i * dash_length + dash_length);
            let r = canvas.draw_line(p1, p2);
            if r.is_err() {
                panic!("Attempted to draw halfway line dash from {} to {}.",
                       margin_top + i * dash_length,
                       margin_top + i * dash_length + dash_length);
            }
        }
    }
}

pub fn draw_score(gs: &GameState, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>){
    let ttf_context = sdl2::ttf::init().expect("SDL TTF initialization failed");
    let texture_creator = canvas.texture_creator();

    let schluber_font_path: &Path = Path::new("font/schluber/Schluber.ttf");

    let result_load_font = ttf_context.load_font(schluber_font_path, 128);
    if result_load_font.is_err() {
        panic!("Problem loading font {}", schluber_font_path.display());
    }

    let font = result_load_font.unwrap();
    let surface_p1 = font
        .render(&format!("{}", gs.score_p1))
        .blended(Color::WHITE)
        .unwrap();
    let surface_p2 = font
        .render(&format!("{}", gs.score_p2))
        .blended(Color::WHITE)
        .unwrap();

    let rect_width: u32 = WINDOW_WIDTH / 12;
    let rect_height: u32 = WINDOW_HEIGHT / 10;
    let font_rect_p1 = Rect::new((WINDOW_WIDTH / 4 - rect_width / 2) as i32, rect_height as i32, rect_width, rect_height);
    let font_rect_p2 = Rect::new((WINDOW_WIDTH * 3 / 4 - rect_width / 2) as i32, rect_height as i32, rect_width, rect_height);
    let texture_p1 = texture_creator.create_texture_from_surface(&surface_p1).unwrap();
    let texture_p2 = texture_creator.create_texture_from_surface(&surface_p2).unwrap();

    canvas.copy(&texture_p1, None, font_rect_p1).unwrap();
    canvas.copy(&texture_p2, None, font_rect_p2).unwrap();

}

pub fn draw_game(gs: &GameState, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>){
    draw_halfway_line(canvas);
    draw_score(&gs, canvas);
    draw_ball(&gs.ball, canvas);
    draw_racket(&gs.racket_1, canvas);
    draw_racket(&gs.racket_2, canvas);
}

So basically, main.rs contains the main function, the game loop and the event handling. entity.rs is for the game elements and their associated behavior (the ball, the rackets...). And finally, view.rs contains the functions for drawing on the screen.

I consider myself as a rust beginner, so I would like to have your opinion on my code. Is there any way you would improve the syntax of what I wrote or the way I organized things ?

I understand that it takes time to look at it and to write constructive criticism, so even a small tip is appreciated!

Thank you!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.