Rust Project: Ping Pong Game

Harsh Vishwakarma
11 min readNov 30, 2021

--

How to make the classic Ping Pong game using Rust programming language.

Ping Pong

Before getting started, let us have a look at the tools you will need for development.

  1. RUST installed on your system.
  2. An IDE. I used VSCode with the RUST analyzer extension.

You can download the Source Code here

Our game will have three main components.

  1. A ball, which will move in two directions. Along the x-axis and y-axis.
  2. Paddles. One is for our player, which we will control using the arrow keys. Another paddle will be the computer.
  3. 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.

[Note: You can use the latest version of the crates.]

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.

For now, your draw.rs file should look like this.

① 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.

With these two additions our draw.rs is complete

④ 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.

Note: We’re importing the draw.rs file we created in the previous steps.

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.

Adding #[derive(Clone, Copy, Debug)] to the top of each of these declarations. It will help you when you try to debug your game. PartialEq will allow us to compare the two directions.

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).

For now, you have added everything you need for the paddle.

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.

Following is our complete implementation 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.

The brains of our game. This file will contain the logic to run our game.

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.

The constructor is tightly coupled. You can fiddle around and make it more flexible as you like.

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.

If game_over is true we will just return without storing the value of the key pressed.

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.

And with this our draw function is complete. Hurray!

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.

Don’t worry about the errors. We will add other update functions in a while

Move the ball

Add another member function named update_ball. It also takes in delta_time.

  1. Find what will be the coordinates of the ball in the next update.
  2. Check if the ball is going to hit the wall.
  3. 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.

Remember, our top wall is MARGIN_TOP distance from the 0,0 coordinates.

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.

using floor() and ceil() to round the numbers before equating

Lastly, update the position of the ball.

Aaand that’s all for your update function

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.

Limit the player movement between MARGIN_TOP and our window height

Add a helper function to convert the user input into our Direction enum

We ignore if any other key is pressed other than Up & Down

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.

Reset score and the ball

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!

Download the Source Code here

Your feedback is most valuable as always :)

--

--

No responses yet