Rust Project: Ping Pong Game
How to make the classic Ping Pong game using Rust programming language.
Before getting started, let us have a look at the tools you will need for development.
- RUST installed on your system.
- An IDE. I used VSCode with the RUST analyzer extension.
Our game will have three main components.
- A ball, which will move in two directions. Along the x-axis and y-axis.
- Paddles. One is for our player, which we will control using the arrow keys. Another paddle will be the computer.
- Walls. A boundary within which our ball will either bounce and change the direction or make a score.
Let’s get started.
Create a new project using cargo new pong --bin
. I have used the name pong. Feel free to use whatever you like. The output should look similar.
You will be using a couple of crates for this project.
Our first crate is piston_window
. “Piston” is a modular game engine written in Rust. You can check out examples and instructions on how to use it from the GitHub page. You will use this to draw a window and other visual parts of our game.
And the second crate is find_folder
. This is required for locating our assets file.
Now, add these two dependencies to our project.
Splitting our project into modules will allow you to manage it easily.
Now create a new file draw.rs at the (root) /src
folder where your main.rs is present.
draw.rs
Let’s move to our draw.rs file. The imports we want to make in our draw file are from piston_window. Here you will add functions to help draw our boundary walls and glyphs for displaying the score.
① The first thing you want to do is create a block size constant. Our blocks will scale up 10 pixels or at least by a factor of 10. You can play around with these numbers.
② Next, you want to create a helper function called to_coord
which will take in a game coordinate and return it after scaling it up.
③ Now, let’s take a look at another helper function draw_block
which will draw a block. This function will take in a color, x coordinate, y coordinate, a context, and a mutable graphics 2d buffer. Convert the game coordiates and then pass it to the rectangle
function of piston_window
.
④ Create another function draw_rectangle
which is just a slight modification on our draw_block
function. We are also passing the width and the height.
⑤ And lastly, draw_text
allows us to draw a text at a given coordinate. You will essentially use this for displaying our score and other text.
paddle.rs
Now, let’s create a file called paddle.rs. In this file, you are going to tie most of the logic needed to play the game.
So, here are our imports and constants.
The next thing you want to do is, create an enum for direction. So, this will tell us the direction the paddle will move along with how our keyboard input interacts with the paddle. We want our paddle to go up and down only.
Also, you will create a struct named paddle
, which will interact with our keyboard inputs and strike the ball.
Now for the implementation of the paddle.
① The first one is an associated function new
. It will return a type Self
and is accessible via the structure’s namespace.
② The draw
function contains the logic to render our paddles on the window. It is simply drawing a rectangle of width 1px and height specified by the size
parameter of the paddle. The function takes in a reference to a context and a mutable reference to graphics 2d.
③ The slide
function takes in the input from keyboard in the form of direction. min_y
and max_y
parameters make sure the paddle stays inside the window. The top left corner is (0,0), the bottom right corner is (max_x, max_y). Thus the position of our paddle must be within the range (0,0) and (max_x, max_y).
ball.rs
Create a new file ball.rs. Our ball will simply be a dot on the screen. But since we are scaling the graphics it will show up nicely as a square block. Our ball will have a coordinate and velocity in the x-y plane.
Let’s add a new()
and draw()
function just as you did for our paddle. You will add a few functions to modify the position and velocity of the ball.
① set_position
is a member function. It takes a mutable reference to the self and coordinates for the x and y positions. You will call this once per frame update.
② get_next_location
returns the coordinate for the next update. We first calculate the distance by multiplying speed into the delta_time
. (delta_time is the time elapsed since the last time update was called). Simply put, we are using the formula distance = speed x time.
③ flip_velocity_y
as the name suggests it will reverse the velocity. Every time it hit the top or bottom wall, you will call this to change the direction of our ball. And similarly, you will call flip_velocity_x
to change the direction of our ball when it hit the paddle.
Other functions have the implementation as their name suggests.
game.rs
Create a new file game.rs. Add the following imports and constants at the top of it.
Create a new structure named Game. This struct will manage the state of our game.
① Now, you need to add an instance of a paddle for the player
to our game’s struct and instantiate it in the constructor.
② You will also need another instance of a paddle for the ai. Let’s name this enemy
.
③ Add a variable named ball
to manage the state of the ball.
④ The width
and height
defines the size of the game window.
⑥game_over
holds a boolean value that keeps track of whether you can still play or you lost.
⑦ waiting_time
is the time we will wait before restarting the game when the player loses.
⑧ ai_response_time
and ai_update_time
are used by our ai logic to make the game playable. There is no point in playing an unbeatable game.
①⓪ The variable active_key
will contain the key pressed by the user.
①① And, the score
will hold the player’s score.
The implementation of the Game.
You now have all the parts you need for the game. Let’s add some logic to take input from the user. Update the position of the ball, player, and enemy. Detect the collisions with the walls and the paddles.
Our player paddle will be on the right. We are placing it 3 pixels (scaled by a factor of 10) before the right wall and 5 pixels from the top. The enemy paddle is 3 pixels from the left and 9 pixels from the top.
(you defined
MARGIN_TOP
at the beginning with the imports leaving some space to display the game score at the top)
Initially, the waiting_time
is 0.0. You will see its usage in a short while.
Handling keyboard events
Let’s quickly add two functions to set and un-set our active_key
variable. active_key
will keep track of the key pressed by the user.
active_key
is an optional variable. Its data type is Key
from the piston_window
library. Whenever the user presses a key, you will store the key in this variable. And when the user lifts his finger you will un-set this variable.
Rendering graphics
Let’s add a member function to draw our components. This function will take in context, mutable graphics 2d, and mutable Glyphs for drawing the text.
Call the draw function of the player and enemy instances. The player paddle will be on the right, and the enemy will be on the left side.
Next, draw the ball. You will render this only if the game is not yet over.
Now, draw four rectangles that will act as our boundary walls. Our playable area will lie within these four rectangles.
You now have the major components you need to see your gameplay. Now call the draw_text
function from the draw.rs file and display it on the top. And the last thing in our draw function is to draw a red screen when the game is over. Add the following functions next.
Game loop
The terminal-based programs execute top to bottom through the main function, pausing for user input. Games won’t stop or pause whenever the player wants to press a key. For games to operate smoothly, they run a game loop. Each pass through the loop, you will call the update
function.
Add a new member function update
that will take in delta_time
. delta_time
is the time between two subsequent update function calls.
Earlier on the top of the file, you declared a constant MOVING_PERIOD
. MOVING_PERIOD
will determine the speed of your game.
In each pass, you will increase the waiting_period
by delta_time. And when wating_period
is greater than the MOVING_PERIOD
you will perform the necessary updates for the gameplay. Afterward, reset the waiting_period
and this will continue. And if the player loses, your game will be over. Then we will restart the gameplay.
Move the ball
Add another member function named update_ball
. It also takes in delta_time
.
- Find what will be the coordinates of the ball in the next update.
- Check if the ball is going to hit the wall.
- If the ball hits the wall on the player’s side, the game is over. Otherwise, it hits the enemy’s side. In that case, We will increase our score by 1.
If the game is over, you don’t need to update anything else. Just add a return statement. Else, add a check to see if the ball hit the vertical wall. If yes, then flip the vertical velocity of the ball and continue.
Collision Detection
Next, check if the player was able to hit the ball. If yes, then flip the horizontal direction of the ball and increase its velocity. Also, increasing the velocity will make it more difficult for the player to hit the ball now. And, same goes for AI as well.
Lastly, update the position of the ball.
Update the player
This one is pretty simple. update_player takes in an optional direction(Up or Down). If the user has pressed the Up arrow key, slide the paddle upwards 1 px, and if the user has pressed the Down arrow key move it down 1 px.
Add a helper function to convert the user input into our Direction
enum
AI
The AI can also move in vertical directions only.
First, add a delay using the ai_update_time and ai_response_time. If the y-coordinate of the ball is lesser than the y-coordinate of the paddle, move the paddle upwards. Else, if the y-coordinate of the ball is greater than the y-coordinate of the paddle, move the paddle downwards.
Restart
The last remaining part is a function to reset the game when the player loses. Add the following function to complete the game.rs file.
Now, come back to our main.rs. Add the following imports and constants to the main.rs file.
Make modifications to the main function now.
Declare height and width for your window. Next, create a PistonWindow
instance. This contains everything required for controlling window, graphics, event loop. The constructor for PistonWindow
takes in a title for the window and size. I’m using “Ping Pong” as title for the game. Also, make sure to scale the size of the window as you did for other graphics elements.
When set to true
exist_on_esc
. You can just quit by pressing esc key.
Assets
Next, you need to add some fonts to display the score. Create a folder in the root directory of the project and name it assets.
You can use any font you like. I have used FiraSans-Regular.ttf
. You can find it here. Download it and move it to the assets folder you just created.
Use the find_folder
library to locate the assets. Add the following code to your main.rs.
Now, create a new Game object and start the game loop by calling the next function from the PistonWindow.
window.next()
returns an iterator with an optional value of type Event. You implemented key_pressed
and key_released
functions a while back in the game.rs file. Now is the time to call those functions.
Call press_args
on the event variable. If it returns a value, that means the user has pressed some key. Call the key_pressed
function from the game.rs file. Similarly, implement the key_release
from the game.rs for release_args
event.
Now, call the draw_2d
function from the PistonWindow. The closure will give you a reference to context, mutable graphics 2d, and a device. Inside the closure, first, clear the background. Next, you will call your draw function from game.rs
. Calling game.draw
will render your components on the window.
The last piece of the puzzle. Call the event.update
function. Inside the closure for this update function, you will make a call to the game.update
function you wrote. And with this, your game is complete.
All that is left for you to do is open the terminal and run cargo run
.
Now play!
Your feedback is most valuable as always :)