Pong game in Rust
I built a game of Pong using the piston engine as well as the OpenGL graphics library.
Tutorial here.
Watch me playing it here.
Run it
cargo run
Posted on May 17, 2024
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. š¾
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 our GlGraphics
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
and ball_y
and velocity_x
and velocity_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 for x
and y
as well as size and we're just going to make our x
and y
zero and then we're going to make the size
50 so that it has some width.
Then we're going to create a variable called left_pos
and this will take our self
from our struct and cast it into an f64
.
We'll do the same for our right
and right_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
and y
values but we want it to be a square of size 10. Then we want to do what we did for our positions for ball_x
and ball_y
. These are going to cast our i32
into f64
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
.
Then 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);
});
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;
Then 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;
}
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;
}
_ => {}
}
}
}
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;
}
_ => {}
}
}
}
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 our GlGraphics::new(opengl)
so this will be bound to our opengl
buffer.
We want to set our left_score
equal to 0, our left_pos
equal to 1and the left_ vel
equal to 0.
Likewise fo the right side.
Our ball_x
and ball_y
will be 0 with its vel_x
and vel_y
being 1.
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,
};
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);
}
}
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:
I built a game of Pong using the piston engine as well as the OpenGL graphics library.
Tutorial here.
Watch me playing it here.
cargo run
Happy Rust Coding! š¤š¦
š Hello, I'm Eleftheria, Community Manager, developer, public speaker, and content creator.
š„° If you liked this article, consider sharing it.
Posted on May 17, 2024
Sign up to receive the latest update from our blog.