Introduction
In this short tutorial, learn how to make a two-player pong game in Rust.
We will be using Macroquad for this.
Prerequisites
Having Rust installed on your machine.
Some basic knowledge of Rust (structs, tuples, async/await, loops and random numbers etc.).
If you want to learn some basics of Macroquad, click here
Getting Started
Create a new Rust project:
cargo new pong
Add Macroquad and Rand Crate:
Either add it in your Cargo.toml
file:
[dependencies]
macroquad = "0.3.25"
rand = "0.8.5"
# these were the latest versions on the time of making this post
Or add it using cargo add
:
cargo add macroquad
cargo add rand
Blank Window
In your src/main.rs
file, add:
use macroquad::prelude::*;
#[macroquad::main("Pong")]
async fn main() {
loop {
clear_background(SKYBLUE);
next_frame().await;
}
}
This sets up a window with sky-blue as the background color.
Click here for some explanation of the code.
If you wanted more control over the window properties:
//snip
fn conf() -> Conf {
Conf {
window_title:"Pong".to_owned(),
...
}
}
#[macroquad::main(conf)]
Specify custom properties to Conf struct. For this tutorial, I'll set window_resizeable
to false.
Creating Paddles
Let's start by making a Paddle
struct.
//snip
#[derive(Copy,Clone)]
struct Paddle {
rect:Rect,
}
The Rect has these fields:
- The X-Coordinate
- The Y-Coordinate
- The width of the rectangle
- The height of the rectangle This will make it easier for us to manage the position and size of the paddles. Speaking of size, let's create some constants for the height and width of the paddles and also their color:
// snip
const PADDLE_W:f32 = 20f32;
const PADDLE_H:f32 = 80f32;
const PADDLE_COLOR:Color = DARKBLUE;
Implementations for Paddle struct:
// snip
impl Paddle {
fn new(rect:Rect) -> Self {
Self {
rect,
}
}
fn draw(&self) {
let r = self.rect;
draw_rectangle(r.x,r.y,r.w,r.h,PADDLE_COLOR);
}
}
The new
method to create a new instance of Paddle
and draw
function to draw the paddle onto the screen.
the draw_rectangle function takes the x and y position and width and height (all of f32 type) and also the color to draw.
screen_width()
and screen_height()
return the width and height of the screen respectively. We assign the x and y values to be half of the width and height of the screen so that the paddle is at the center of the window.
Coordinate system of Macroquad, where the black rectangle is the screen:
In your main function, create a paddle and draw it on to the screen:
//snip
async fn main() {
let paddle = Paddle::new(Rect::new(screen_width()/2.,screen_height()/2.,PADDLE_W,PADDLE_H));
loop {
clear_background(SKYBLUE);
paddle.draw();
next_frame().await();
}
}
The resulting code should give:
And the code:
use macroquad::prelude::*;
const PADDLE_W:f32 = 20f32;
const PADDLE_H:f32 = 80f32;
const PADDLE_COLOR:Color = DARKBLUE;
fn conf() -> Conf {
Conf {
window_title:"Pong".to_owned(),
window_resizable: false,
..Default::default()
}
}
#[derive(Copy,Clone)]
struct Paddle {
rect:Rect,
}
impl Paddle {
fn new(rect:Rect) -> Self {
Self {
rect,
}
}
fn draw(&self) {
let r = self.rect;
draw_rectangle(r.x,r.y,r.w,r.h,PADDLE_COLOR);
}
}
#[macroquad::main("Pong")]
async fn main() {
let paddle = Paddle::new(Rect::new(screen_width()/2.,screen_height()/2.,PADDLE_W,PADDLE_H));
loop {
clear_background(SKYBLUE);
paddle.draw(); // Make sure its after the clear_background function
// else it cannot be seen
next_frame().await;
}
}
Now let's make two paddles, one for the right side and another for the left. Modify your main
function:
//don't worru about the unused mut warning,we'll use them later.
let mut paddle_left = Paddle::new(Rect::new(PADDLE_W,screen_height()/2.,PADDLE_W,PADDLE_H));
let mut paddle_right = Paddle::new(Rect::new(screen_width()-PADDLE_W*2.,screen_height()/2.,PADDLE_W,PADDLE_H));
loop {
clear_background(SKYBLUE);
paddle_left.draw();
paddle_right.draw();
next_frame().await;
}
Creating two new paddles for the left and the right and aligning them properly, and drawing them on the screen
Output:
Movement
Create a new constant for the paddle speed:
const PADDLE_SPEED:f32 = 10f32;
In your Paddle struct, create a new implmentation for movement.
impl Paddle {
...
fn movement(&mut self,up:KeyCode,down:KeyCode) {
if is_key_down(up) {
self.rect.y -= 1.*PADDLE_SPEED;
}else if is_key_down(down) {
self.rect.y += 1.*PADDLE_SPEED;
}
}
...
}
We need to change the position (x and y coordinates) of the rect, so we get a mutable refernce. Since we have two paddles with two different keys for movement, we get the keys through the parameters.
`is_key_down
function takes a KeyCode and returns true if the key is being held down.
If the key to move up is held down, then we move the paddle up by negating the y coordinate multiplied with the paddle speed.
Same logic to move down, but now we add to the y coordinate.
In your main function loop:
rs
//snip
...
paddle_left.movement(KeyCode::W,KeyCode::S);
paddle_right.movement(KeyCode::Up, KeyCode::Down);
paddle_left.draw();
paddle_right.draw();
...
And now you should be able to move the paddles using the keys!
Here's the whole code till now:
rs
use macroquad::prelude::*;
const PADDLE_W:f32 = 20f32;
const PADDLE_H:f32 = 80f32;
const PADDLE_COLOR:Color = DARKBLUE;
const PADDLE_SPEED:f32 = 10f32;
fn conf() -> Conf {
Conf {
window_title:"Pong".to_owned(),
window_resizable: false,
..Default::default()
}
}
#[derive(Copy,Clone)]
struct Paddle {
rect:Rect,
}
impl Paddle {
fn new(rect:Rect) -> Self {
Self {
rect,
}
}
fn movement(&mut self,up:KeyCode,down:KeyCode) {
if is_key_down(up) {
self.rect.y -= 1.*PADDLE_SPEED;
}else if is_key_down(down) {
self.rect.y += 1.*PADDLE_SPEED;
}
}
fn draw(&self) {
let r = self.rect;
draw_rectangle(r.x,r.y,r.w,r.h,PADDLE_COLOR);
}
}
#[macroquad::main(conf)]
async fn main() {
let mut paddle_left = Paddle::new(Rect::new(PADDLE_W,screen_height()/2.,PADDLE_W,PADDLE_H));
let mut paddle_right = Paddle::new(Rect::new(screen_width()-PADDLE_W*2.,screen_height()/2.,PADDLE_W,PADDLE_H));
loop {
clear_background(SKYBLUE);
paddle_left.movement(KeyCode::W,KeyCode::S);
paddle_right.movement(KeyCode::Up, KeyCode::Down);
paddle_left.draw();
paddle_right.draw();
next_frame().await;
}
}
But you'll probably note that the paddles can go out of the screen.
To restrict the paddles into the screen, add this in your movement
implementation:
rs
if self.rect.y > screen_height()-PADDLE_H{
self.rect.y = screen_height()-PADDLE_H;
}else if self.rect.y < 0. {
self.rect.y = 0.;
}
Just some simple logic.
If the paddle is gone above screen (less than 0) then we bring it back to the screen (0). If its below the screen (screen_height()-PADDLE_H) then get it back to the screen.
Adding the Ball
Create some constants for the ball:
rs
const BALL_RADIUS:f32 = 15f32;
const BALL_COLOR:Color = PINK;
const BALL_SPEED:f32 = 8f32;
Create the Ball
struct
rs
#[derive(Copy,Clone)]
struct Ball {
circle:Circle,
dir: Vec2 // direction or velocity of the ball
}
dir
is the velocity or the direction towards which the ball should travel.
Implementations for the ball:
rs
impl Ball {
fn new(circle:Circle) ->Self {
Self {
circle,
dir:vec2(0.,1.) // for now, we'll use these
// we'll add random values later
}
}
fn draw(&self) {
draw_circle(self.circle.x,self.circle.y,self.circle.r,BALL_COLOR);
}
}
Add the ball to the game:
rs
async fn main() {
...
let mut ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS));
loop {
clear_background(SKYBLUE); //MAKE SURE: all the drawing
// happens below this function
...
ball.draw();
...
}
}
And on running it:
Great! Now to make it move, we simple add the dir value (x and y) multiplied by the speed to the balls's position.
Add another function to the ball implementation:
rs
fn move_ball(&mut self) {
self.circle.x += self.dir.x * BALL_SPEED;
self.circle.y += self.dir.y * BALL_SPEED;
}
And move the ball in your loop:
rs
//snip
loop {
...
ball.move_ball();
...
}
Now the ball moves...out of the window. But this has an easy fix!
In your move_ball
function:
rs
...
if self.circle.y > screen_height()-BALL_RADIUS || self.circle.y < 0.0 {
self.dir.y = -self.dir.y;
}
...
We invert the sign of dir.y
so that it goes the other way. Now if you run this you can see that the ball moves up and down the screen. We aren't doing this with the x
value because we will use that for scoring system.
Random dir value
Import rand
crate and some of its items:
rs
use ::crate::{thread_rng,Rng};
In your new function for ball, make these changes:
rs
let mut rng = thread_rng();
let dir_x = rng.gen_range(-1.0..1.);
let dir_y = rng.gen_range(-1.0..1.);
Self {
circle,
dir:vec2(dir_x,dir_y)
}
Done!
The code so far:
rs
use macroquad::prelude::*;
use ::rand::{thread_rng,Rng};
const PADDLE_W:f32 = 20f32;
const PADDLE_H:f32 = 80f32;
const PADDLE_COLOR:Color = DARKBLUE;
const PADDLE_SPEED:f32 = 10f32;
const BALL_RADIUS:f32 = 15f32;
const BALL_COLOR:Color = PINK;
const BALL_SPEED:f32 = 8f32;
fn conf() -> Conf {
Conf {
window_title:"Pong".to_owned(),
window_resizable: false,
..Default::default()
}
}
#[derive(Copy,Clone)]
struct Paddle {
rect:Rect,
}
#[derive(Copy,Clone)]
struct Ball {
circle:Circle,
dir: Vec2
}
impl Paddle {
fn new(rect:Rect) -> Self {
Self {
rect,
}
}
fn movement(&mut self,up:KeyCode,down:KeyCode) {
if is_key_down(up) {
self.rect.y -= 1.*PADDLE_SPEED;
}else if is_key_down(down) {
self.rect.y += 1.*PADDLE_SPEED;
}
if self.rect.y > screen_height()-PADDLE_H{
self.rect.y = screen_height()-PADDLE_H;
}else if self.rect.y < 0. {
self.rect.y = 0.;
}
}
fn draw(&self) {
let r = self.rect;
draw_rectangle(r.x,r.y,r.w,r.h,PADDLE_COLOR);
}
}
impl Ball {
fn new(circle:Circle) ->Self {
let mut rng = thread_rng();
let dir_x = rng.gen_range(-1.0..1.);
let dir_y = rng.gen_range(-1.0..1.);
Self {
circle,
dir:vec2(dir_x,dir_y)
}
}
fn move_ball(&mut self) {
self.circle.x += self.dir.x * BALL_SPEED;
self.circle.y += self.dir.y * BALL_SPEED;
if self.circle.y > screen_height()-BALL_RADIUS || self.circle.y < 0.0 {
self.dir.y = -self.dir.y;
}
}
fn draw(&self) {
draw_circle(self.circle.x,self.circle.y,self.circle.r,BALL_COLOR);
}
}
#[macroquad::main(conf)]
async fn main() {
let mut paddle_left = Paddle::new(Rect::new(PADDLE_W,screen_height()/2.,PADDLE_W,PADDLE_H));
let mut paddle_right = Paddle::new(Rect::new(screen_width()-PADDLE_W*2.,screen_height()/2.,PADDLE_W,PADDLE_H));
let mut ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS));
loop {
clear_background(SKYBLUE);
paddle_left.movement(KeyCode::W,KeyCode::S);
paddle_right.movement(KeyCode::Up, KeyCode::Down);
ball.move_ball();
ball.draw();
paddle_left.draw();
paddle_right.draw();
next_frame().await;
}
}
Collisions!
Now we'll add ball-to-paddle collisions.
Create a new implementation in your ball struct:
rust
fn collision_with_paddle(&mut self, paddle_1 : &Rect, paddle_2 : &Rect) {
let ball_rect = Rect::new(self.circle.x,self.circle.y,self.circle.r,self.circle.r);
if ball_rect.intersect(*paddle_1).is_some() || ball_rect.intersect(*paddle_2).is_some() {
self.dir.x = -self.dir.x;
}
}
We take in two refernces of paddles. Then we create a rect of the ball because the rect
has a method intersect
which allows us to see if a rect is touching or intersecting another rect. We pass the x and y as coordinates and radius as the height and width (more of a square than a rectangle then).
Then we check if any one of the paddles intersect with the ball and then invert the sign of the dir.x value. Easy!
Call the method in your loop:
rs
loop {
...
ball.collision_with_paddles(&paddle_left.rect,&paddle_right.rect);
...
}
Run it!
And now you can play pong until one of you loses the ball.
Ball respawn and scores
Create a scores tuple variable in your main function (outside the loop):
rs
let mut scores = (0,0);
Now, inside the loop, we need to check for the ball position and accordingly update the scores:
rs
if ball.circle.x < 0.0 {
ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS));
scores.1 += 1;
}else if ball.circle.x > screen_width() {
ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS));
scores.0 += 1;
}
What good are scores if you cannot view them?
Drawing the scores
Macroquad has functions for drawing text onto the screen. I don't really like the font, so I'll use Robot-Black. Create a folder res
in your project directory (outside src
but inside your project) and install the ttf version of the font there.
Now let's access it in our main function:
rs
let font = load_ttf_font("./res/Roboto-Black.ttf")
.await
.unwrap();
Create a new function to draw the text:
rs
fn draw_scores(font:Font,scores:&(u32,u32)) {
let text_params = TextParams {
font_size:70,
font,
..Default::default()
};
draw_text_ex(&format!("{}",scores.0).as_str(),100., 100.,text_params);
draw_text_ex(&format!("{}",scores.1).as_str(),screen_width()-100., 100.,text_params);
}
We take in the font and scores. We assign text_params to a struct which allows us to customize how our text looks.(More Info). Now we draw the text using draw_text_ex at the left and right side.
Then calling it in our main loop:
rs
loop {
...
clear_background(); //again, always draw stuff below this
...
draw_scores(font,&scores);
...
}
And we're done!
Here's the full code:
rs
use macroquad::prelude::*;
use ::rand::{thread_rng,Rng};
const PADDLE_W:f32 = 20f32;
const PADDLE_H:f32 = 80f32;
const PADDLE_COLOR:Color = DARKBLUE;
const PADDLE_SPEED:f32 = 10f32;
const BALL_RADIUS:f32 = 15f32;
const BALL_COLOR:Color = PINK;
const BALL_SPEED:f32 = 8f32;
fn conf() -> Conf {
Conf {
window_title:"Pong".to_owned(),
window_resizable: false,
..Default::default()
}
}
#[derive(Copy,Clone)]
struct Paddle {
rect:Rect,
}
#[derive(Copy,Clone)]
struct Ball {
circle:Circle,
dir: Vec2
}
impl Paddle {
fn new(rect:Rect) -> Self {
Self {
rect,
}
}
fn movement(&mut self,up:KeyCode,down:KeyCode) {
if is_key_down(up) {
self.rect.y -= 1.*PADDLE_SPEED;
}else if is_key_down(down) {
self.rect.y += 1.*PADDLE_SPEED;
}
if self.rect.y > screen_height()-PADDLE_H{
self.rect.y = screen_height()-PADDLE_H;
}else if self.rect.y < 0. {
self.rect.y = 0.;
}
}
fn draw(&self) {
let r = self.rect;
draw_rectangle(r.x,r.y,r.w,r.h,PADDLE_COLOR);
}
}
impl Ball {
fn new(circle:Circle) ->Self {
let mut rng = thread_rng();
let dir_x = rng.gen_range(-1.0..1.);
let dir_y = rng.gen_range(-1.0..1.);
Self {
circle,
dir:vec2(dir_x,dir_y)
}
}
fn move_ball(&mut self) {
self.circle.x += self.dir.x * BALL_SPEED;
self.circle.y += self.dir.y * BALL_SPEED;
if self.circle.y > screen_height()-BALL_RADIUS || self.circle.y < 0.0 {
self.dir.y = -self.dir.y;
}
}
fn collision_with_paddle(&mut self, paddle_1 : &Rect, paddle_2 : &Rect) {
let ball_rect = Rect::new(self.circle.x,self.circle.y,self.circle.r,self.circle.r);
if ball_rect.intersect(*paddle_1).is_some() || ball_rect.intersect(*paddle_2).is_some() {
self.dir.x = -self.dir.x;
}
}
fn draw(&self) {
draw_circle(self.circle.x,self.circle.y,self.circle.r,BALL_COLOR);
}
}
fn draw_scores(font:Font,scores:&(u32,u32)) {
let text_params = TextParams {
font_size:70,
font,
..Default::default()
};
draw_text_ex(&format!("{}",scores.0).as_str(),100., 100.,text_params);
draw_text_ex(&format!("{}",scores.1).as_str(),screen_width()-100., 100.,text_params);
}
#[macroquad::main(conf)]
async fn main() {
let mut paddle_left = Paddle::new(Rect::new(PADDLE_W,screen_height()/2.,PADDLE_W,PADDLE_H));
let mut paddle_right = Paddle::new(Rect::new(screen_width()-PADDLE_W*2.,screen_height()/2.,PADDLE_W,PADDLE_H));
let mut ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS));
let mut scores:(u32,u32)= (0,0);
let font = load_ttf_font("./res/Roboto-Black.ttf")
.await
.unwrap();
loop {
clear_background(SKYBLUE);
paddle_left.movement(KeyCode::W,KeyCode::S);
paddle_right.movement(KeyCode::Up, KeyCode::Down);
ball.move_ball();
ball.collision_with_paddle(&paddle_left.rect,&paddle_right.rect);
ball.draw();
paddle_left.draw();
paddle_right.draw();
if ball.circle.x < 0.0 {
ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS));
scores.1 += 1;
}else if ball.circle.x > screen_width() {
ball = Ball::new(Circle::new(screen_width()/2.,screen_height()/2.,BALL_RADIUS));
scores.0 += 1;
}
draw_scores(font,&scores);
next_frame().await;
}
}
Top comments (0)