Tutorial: Pong game in Rust 🦀
Hello, amazing people and welcome back to my blog! Today we’re going to learn how to build a Pong game using the piston engine as well as the OpenGL graphics library.
In the end, we will have a board with 2 paddles, one on the left and one on the right side, and one ball. We’ll also have 2 players who will be able to handle the left and the right paddles with Y and X keys and the up and down arrows.
Let’s build this. 🎾
Dependencies
Toml File
[dependencies]
piston = "0.35.0"
piston2d-graphics = "0.24.0"
pistoncore-glutin_window = "0.43.0"
piston2d-opengl_graphics = "0.50.0"
First, we need the piston engine itself, then we’ll need our piston 2D graphics and we’ll need our pistoncore-glutin_window
and our piston2d-opengl_graphics
.
Tip: When you write the dependencies you can use inside the quotes an asterisk for the version number. Then go to the terminal, typecargo update
and this will update all of your dependencies in thecargo.lock
file. If we go to thelock
file, we can search out the libraries, then copy the number and replace the asterisk back in thetoml
file.
piston = "*"
The reason it’s important to use static versions is just in case the library actually changes. If the syntax changes, then the game will not work properly anymore because we will be behind.
Main rs File
Let’s go to the main.rs file and bring in all of our external libraries and make some imports!
extern crate glutin_window;
extern crate graphics;
extern crate opengl_graphics;
extern crate piston;
I will need the process
the piston::window
so that we can set up our window, the event_loop
to set up our event settings, we'll need our piston::input
for Key, PressEvent
etc and then we'll also need our glutin_window
(this allows us to create an OpenGL window) and opengl_graphics
which contains our GlGraphics, OpenGL
.
use std::process;
use piston::window::WindowSettings;
use piston::event_loop::{EventSettings, Events};
use piston::input::{Button, Key, PressEvent, ReleaseEvent, RenderArgs, RenderEvent, UpdateArgs, UpdateEvent};
use glutin_window::GlutinWindow;
use opengl_graphics::{GlGraphics, OpenGL};
Now we want to create a structure called App
.
- Inside of this, we will have a field called
gl
which will connect to ourGlGraphics
type. Then we'll have our left score as well as the left position and left velocity. This will correspond to the left paddle. - Then we’ll have the right score, the right position, and the right velocity which will correspond to our right paddle. All these are
i32
. - Then we’ll have
ball_x
andball_y
andvelocity_x
andvelocity_y
. All four of these will correspond with our ball.
Tip: If you want to write a more full featured application you can split these off into their own objects.
pub struct App {
gl: GlGraphics,
left_score: i32,
left_pos: i32,
left_vel: i32,
right_score: i32,
right_pos: i32,
right_vel: i32,
ball_x: i32,
ball_y: i32,
vel_x: i32,
vel_y: i32,
}
Next, we want to create an implementation block for our application. Inside of it we need a render method. This method will take in a mutable self
and our arguments (which will be a reference to our render arguments).
- We want to have an import for graphics so that we can easily get to it inside of this function.
- We will also create some constants for the colors of our game. They will be
[f32; 4].
Our background color will be the color in the background of our window. Whereas our foreground color will be the color that we paint our paddles and our ball with. - We’re going to create a variable called
left
which will be a rectangle square. This takes in scalars forx
andy
as well as size and we're just going to make ourx
andy
zero and then we're going to make thesize
50 so that it has some width. - Then we’re going to create a variable called
left_pos
and this will take ourself
from our struct and cast it into anf64
. - We’ll do the same for our
right
andright_pos
. - Last but not least, for our ball, we also want it to be a square. We don’t want it to have any specific
x
andy
values but we want it to be a square of size 10. Then we want to do what we did for our positions forball_x
andball_y
. These are going to cast ouri32
intof64
for the rendering.
impl App {
fn render(&mut self, args: &RenderArgs) {
use graphics::*;
const BACKGROUND: [f32; 4] = [0.0, 0.5, 0.5, 1.0];
const FOREGROUND: [f32; 4] = [0.0, 0.0, 1.0, 1.0];
let left = rectangle::square(0.0, 0.0, 50.0);
let left_pos = self.left_pos as f64;
let right = rectangle::square(0.0, 0.0, 50.0);
let right_pos = self.right_pos as f64;
let ball = rectangle::square(0.0, 0.0, 10.0);
let ball_x = self.ball_x as f64;
let ball_y = self.ball_y as f64;
.
.
.
}
Then we want to call self.gl.draw
and this is a method that's included inside of our opengl_graphics
library. This takes in our args.viewport
and then it takes in a closure which takes in c
and gl
( c
being the context
and gl
being our opengl graphic
renderer.) Inside of this closure we first want to clear
our board and apply the background
. We run this method called clear
which takes in the color
that we want the background to be and then the actual renderer which is the gl
.
We want to create a rectangle. This will be our foreground color. It will be on the left as well and it will have a transform
of -40
and then our left_pos
and the gl
.
Our right paddle things are going to be slightly different on the transform
. We're going to color it with the foreground color and we're still going to put in the right variable but we also need to transform
it by args.width
so the width
of the actual args
from the viewport
as a f64
and then we're going to have -10
.
Now we want to render our ball
with the foreground
color. Let's add c.transform.trans(ball_x, ball_y)
and lastly let's add the gl
as well.
To recap this part: First we are defining our paddles as squares and then we’re using the transform
to stretch our paddles downwards towards the bottom so that they actually are rectangles rather than squares.
self.gl.draw(args.viewport(), |c, gl| {
clear(BACKGROUND, gl);
rectangle(FOREGROUND, left, c.transform.trans(-40.0, left_pos), gl);
rectangle(
FOREGROUND,
right,
c.transform.trans(args.width as f64 - 10.0, right_pos),
gl,
);
rectangle(FOREGROUND, ball, c.transform.trans(ball_x, ball_y), gl);
});
The update method
Now we need to create an update
method. Our update method will be where all of our game logic lies. We're taking in a mutable self
so a mutable version of our struct
and then we're going to take in our UpdateArgs
which are sort of like our render args except made for updating.
Now we’re going to write a few if
statements, buckle up! 🙂
Our first two if statements check to see if our paddles are about to go off the screen.
In the first if statement we’re checking to see if our left paddle’s velocity is == 1
and if self.left_pos < 291
. In other words it's going off the bottom of the screen or we're checking to see if the left velocity == -1
and && self.left_pos >= 1
, in other words it's going off the top of the screen. If that happens then we want to increment the actual paddle so that it comes back onto the screen. So as soon as the paddle goes off the screen just slightly it will be incremented back onto the screen.
We do the same for the right paddle.
fn update(&mut self, _args: &UpdateArgs) {
if (self.left_vel == 1 && self.left_pos < 291)
|| (self.left_vel == -1 && self.left_pos >= 1)
{
self.left_pos += self.left_vel;
}
if (self.right_vel == 1 && self.right_pos < 291)
|| (self.right_vel == -1 && self.right_pos >= 1)
{
self.right_pos += self.right_vel;
}
.
.
.
The next part gives our ball some velocity. We want our ball to be moving in the x
direction always so we increment it with our velocity x
.
self.ball_x += self.vel_x;
Then we want to check to see if the ball has gone off of the right side of the screen, if self.ball_x > 502
. If it is, we want to then reverse its velocity in the x
direction, self.vel_x = -self.vel_x;
Basically, if it's going right and it hits the paddle then it will automatically go to the left. If it's going right and it misses the paddle and goes off the screen then when we reset it. It will automatically be moving towards the left paddle. Then we check to see if ball y self.ball_y < self.right_pos || self.ball_y > self.right_pos + 50
. In other words, we check to see if our ball has gone past our right paddle and if it has, then we increment our left score +1, self.left_score += 1;
We have a little statement that says that if self.left_score >= 5
then we say println!("Left wins!");
and then we process::exit(0);
. If we do go past the right paddle then we want to reset the ball at 256
for its ball_x
and 171
for its ball_y
.
self.ball_x += self.vel_x;
if self.ball_x > 502 {
self.vel_x = -self.vel_x;
if self.ball_y < self.right_pos || self.ball_y > self.right_pos + 50 {
self.left_score += 1;
if self.left_score >= 5 {
println!("Left wins!");
process::exit(0);
}
self.ball_x = 256;
self.ball_y = 171;
}
}
Similarly, we’ll work on the left paddle. You can check the code below for minor differences.
if self.ball_x < 1 {
self.vel_x = -self.vel_x;
if self.ball_y < self.left_pos || self.ball_y > self.left_pos + 50 {
self.right_score += 1;
if self.right_score >= 5 {
println!("Right wins!");
process::exit(0);
}
self.ball_x = 256;
self.ball_y = 171;
}
}
The last thing we want in this method is to allow our ball to bounce off of the top and the bottom of the screen. So we will do self.ball_y += self.vel_y;
. Then we want to check to see if the ball has hit the bottom of the screen or if it has hit the top of the screen. If it has hit either one then we reverse the velocity.
self.ball_y += self.vel_y;
if self.ball_y > 332 || self.ball_y < 1 {
self.vel_y = -self.vel_y;
}
The press method
So now we want to create a method to handle the keys. We will call this method press
. It takes in our immutable self
and the arguments which is a reference to our Button
enum. We will use an if let
binding to destruct our arguments and if the pattern is similar to a reference to Button
keyboard then we want to take the key
out and we want to match
on key
.
- If
Key::Up
→ we want the right paddle's velocity to move -1. - If
Key::Down
→ we want the right velocity to move 1 - Likewise, we’ll code
w
,s
, and any other key_
.
fn press(&mut self, args: &Button) {
if let &Button::Keyboard(key) = args {
match key {
Key::Up => {
self.right_vel = -1;
}
Key::Down => {
self.right_vel = 1;
}
Key::W => {
self.left_vel = -1;
}
Key::S => {
self.left_vel = 1;
}
_ => {}
}
}
}
The release method
Now we also want to have a release method. It will be the same as our press
method except for one minor difference. We're going to run an if let
binding on args
to see if let &Button::Keyboard(key) = args
and if it does then we're going to match
on key
and:
- If
Key::Up
was pressed but let go then we want to set the right paddle's velocity to 0 (to elaborate, if for instance the player is hitting up and then they let go then we want to immediately stop the velocity.) - Likewise, we’ll code
Down
,w
,s
, and any other key_
.
fn release(&mut self, args: &Button) {
if let &Button::Keyboard(key) = args {
match key {
Key::Up => {
self.right_vel = 0;
}
Key::Down => {
self.right_vel = 0;
}
Key::W => {
self.left_vel = 0;
}
Key::S => {
self.left_vel = 0;
}
_ => {}
}
}
}
The main function
Let’s finally build up the window and make the game work! 🤗
We’re going to bind let opengl = OpenGL::V3_2;
, then we're going to say let mut window: GlutinWindow = WindowSettings::new("Pong", [512, 342])
with pong's dimensions at 512 by 342 and then we'll have .exit_on_esc(true)
so that if an individual hits the escape key it will exit the window, then we want to build
the window and we want to unwrap
it so that we can get back the actual value.
fn main() {
let opengl = OpenGL::V3_2;
let mut window: GlutinWindow = WindowSettings::new("Pong", [512, 342])
/* .opengl(opengl) */
.exit_on_esc(true)
.build()
.unwrap();
Now we want to instantiate our app structure and let mu app
equal to App
. Then we're going to have:
- Our
gl
is ourGlGraphics::new(opengl)
so this will be bound to ouropengl
buffer. - We want to set our
left_score
equal to 0, ourleft_pos
equal to 1and theleft_ vel
equal to 0. - Likewise for the right side.
let mut app = App {
gl: GlGraphics::new(opengl),
left_score: 0,
left_pos: 1,
left_vel: 0,
right_score: 0,
right_pos: 1,
right_vel: 0,
ball_x: 0,
ball_y: 0,
vel_x: 1,
vel_y: 1,
};r
Next, we want to create an events
variable, this will let mut events = Events::new(EventSettings::new());
. Now let's create a loop. The loop will continue to iterate through as long as we have a new event. And we finally want to call the functions we created above according to our user's action (key presses).
let mut events = Events::new(EventSettings::new());
while let Some(e) = events.next(&mut window) {
if let Some(r) = e.render_args() {
app.render(&r);
}
if let Some(u) = e.update_args() {
app.update(&u);
}
if let Some(b) = e.press_args() {
app.press(&b);
}
if let Some(b) = e.release_args() {
app.release(&b);
}
}
Run it 🏃♂️
Are you still here? That’s it folks, we made it! Time to run it.
Simply type in your terminal cargo run
and you should see the board. Call a friend and start playing. 🤗
Use the keys w
and s
and your friend the arrows up
and down
. Who is going to win?!
Find the code here:
Happy Rust Coding! 🤞🦀
Originally published at https://eleftheriabatsou.hashnode.dev on May 17, 2024.