RandomMomentania

Godot Pong Tutorial

In this tutorial, we will cover how to make the classic game of Pong, with all of the code in a single file, using a single Godot scene. Let’s jump right in!

Note

This tutorial is written for Godot 3.2.3! You can find the completed project file for this tutorial at the bottom of this page. There are no starter assets for this tutorial, but you can follow along by downloading the project file and deleting the OneFilePong.gd file in the Godot editor!

The first thing we need to do is make a new Godot project. I have called mine “Godot_OneFile_Pong”, but feel free to call yours whatever you want. After you have created the project, in the scene inspector on the left side, you will see a panel that says “Create Root Node”. Select the “User Interface” button:

From there, you will be given a Control node as the root node of the scene. Rename the control node to whatever you want (I named mine “OneFilePong”), and then perform the following steps:

  • Select the root control node and in the top bar in the middle window, the scene view, select the “Layout” tab. This will bring a dropdown of various anchor and layout modes. Select the “Full Rect” mode. This will scale the Control node so it always take the full size of the game window.
  • Create a new ColorRect node and name it “BG”. Select the “Layout” tab and select “Full Rect” again. Set the color of the ColorRect node to whatever color you want your background to be.
  • Make a StaticBody2D node, a ColorRect node, and a CollisionShape2D node. Make the ColorRect node and the CollisionShape2D node children of the StaticBody2D node. Set the color of the ColorRect node to something that will show on the screen, but is not too different than the background, so players can see that it is a boundary.
  • Using the transform tools/handles in the scene view, select the ColorRect node and make the size of the ColorRect node fit the size of the game window. Then select the CollisionShape2D node, make a RectangleShape2D, set its size to half of the size of the ColorRect node, and position it in place. When finished, you should have something like this:
  • Once this is done position the StaticBody2D node so it covers the top of the “BG” ColorRect node. Then copy the StaticBody2D node and place it so it covers the bottom of the “BG” ColorRect node. These StaticBody2D nodes will keep the ball in our game of Pong within the game area. Note that we are leaving the sides open.
  • Create a Label node and call it “Score_Label”. With the Label node selected, press the “Layout” tab and select “Full Rect”. Then, still with the label selected, go to the inspector tab on the right and set the “Align” and “Valign” properties to “Center”. This will place the text in the center of the screen.
  • Next, create a KinematicBody2D node and name it “Paddle_Player”. Make a ColorRect node and a CollisionShape2D node, and set these as children of the KinematicBody2D node. Set the ColorRect node to whatever size and color you want your paddle. I choose 32 pixels wide and 128 pixels high as the “size” (under the “Rect” category) and set my paddle to use a green color. Then select the CollisionShape2D node and make a RectangleShape2D shape, and set its size to half of the ColorRect node’s size. Finally, position the CollisionShape2D over the ColorRect node.
  • Position the “Paddle_Player” node on the left side of the screen to wherever you want the player’s paddle to be.
  • Copy the “Paddle_Player” node, rename it to “Paddle_AI”, and position it to where you want the AI’s paddle to be on the right side. To help with visually knowing which is which, you can select the ColorRect child node in “Paddle_AI” and make it a different color.
  • Finally, copy the “Paddle_AI” and rename it to “Ball”. Set the “Ball” node in the middle of the screen. Then select the ColorRect child node in “Ball” and make it a different size, like a small square. You can also change the color of the ColorRect node to something like White, to make it easier to tell what is the ball.
  • Select the CollisionShape2D child node in “Ball” and on the Shape2D, select the little down arrow on the right side of the shape in the inspector. From the dropdown, select “new RectangleShape2D” and set it’s size to half of the ball ColorRect node. Finally, position the RectangleShape2D over the ColorRect node.
  • We are almost there! Next, make a Position2D node and call it “Ball_Spawn_Pos”. Position this node in the center of the screen.
  • Then, make another Position2D node and call it “AI_Win_Position”. Place this node on the left side of the screen, behind the player paddle. I placed mine just a bit outside of the “BG” ColorRect node.
  • Finally, make a final Position2D node and call it “Player_Win_Position”. Place this node on the right side of the screen, behind the AI paddle. I placed my just a bit outside of the “BG” ColorRect node.

Phew! That is a lot of steps. Here is what your scene tree should look like, with all that completed:

And here is what the finished scene view looks like. Note that I added a custom font from Kenney fonts to my Label, but this step is entirely optional.

Note

If you are lost on how to make the scene setup, please download the finished project at the bottom of this tutorial! It has a complete layout of everything I wrote above (including the optional font change).

Writing the code

Alright, now all that is left is writing the code, and our game of Pong is good to go! Believe it or not, all of the code we need for this game of Pong is just a tiny bit over 100 lines of code, including empty space! Additionally, all of the code is in a single file. To me, this really shows how game engines, like Godot, have made rapid game development so much faster and easier.

Select the root Control node (“OneFilePong” or whatever you called yours) in the scene view and right click it. From the dropdown, select “Attach Script”. Leave the settings at their default and press the “Create” button.

In the code editor, add the following lines of code:

Let’s go over what this code does!

Tip

You can copy and paste the code into Godot. If you do, in the script editor press the “edit” tab at the top left of the screen, and then press “Convert Indents to Tabs”. That way, you can have the Godot editor help show code indentation on the left size of the text editor!

First, let’s take a look at all of the class variables we have defined and what they will do. The class variables are lines 4 through 21, and they are variables we can access from any of the functions in the code.

  • AI_SPEED – This is a float that defines how fast the AI paddle will be able to move up and down when it tries to block the ball.
  • PLAYER_SPEED – This is a float that defines how fast the player paddle will be able to move up and down.
  • ball_speed – This is a float that defines how fast the ball will move around in the scene. Note that it is not a constant, and this is because we will make the ball get faster the more it is bounced from player to player.
  • BALL_SPEED_GAME_START – This is a float that defines how fast the ball moves when it is spawned into the game.
  • BALL_SPEED_PLAYER_BOUNCE_INCREASE – This is a float that defines how much speed is added to the ball when it bounces off the player or AI paddle.
  • ball_direction – This is a Vector2 that will hold the direction that the ball is moving towards. Whatever direction this Vector2 points to, the ball will move towards.
  • ball_spawn_pos – This is a Position2D node that we will use to get the position when the ball is spawned and respawned.
  • ball_pos_player_score – This is a float that will hold the X axis value that the ball has to be over for the player to score. Because the player is on the left side, this value will be a X position on the right.
  • ball_pos_ai_score – This is a float that will hold the X axis value that the ball has to be over for the AI to score. Because the AI is on the right side, this value will be a X position on the left.
  • body_ball – This is a KinematicBody2D node for the ball. Using this, we will be able to move the ball, bounce it off walls and the paddles, and increase the score when necessary.
  • body_player – This is a KinematicBody2D node for the player. Using this, we will be able to move the player’s paddle around.
  • body_ai – This is a KinematicBody2D node for the AI. Using this, we will be able to write code to move the AI’s paddle around to attempt to block the ball.
  • score_player – This is an integer that will hold how many times the player has scored.
  • score_ai – This is an integer that will hold how many times the AI has scored.
  • score_label – This is a Label node that we will use to display the scores to the player.

That is quite a few variables, but they are everything we will need for the entire game of pong. Most of them are, hopefully, fairly self explanatory as to what they are for.

Next, let’s look at the _ready function.

_ready function

On lines 25 to 29, all we are doing is getting the various nodes from the scene and assigning them to their respective class variables. This is so we can use these nodes later through the class variables. We could get these nodes each time we need them, but there would be a slight performance cost of doing so.

Tip

You may see some Godot tutorial using the following syntax for getting nodes: $NodeNameHere instead of get_node("NodeNameHere"). both do exactly the same thing, but $ is shorthand for the get_node function.

On line 31, we get the “AI_Win_Position” node’s global X position (global_position.x) and assign it to ball_pos_ai_score. On line 32, we do the same thing but for “Player_Win_Position” and ball_pos_player_score. We do this so we can use the Position2D’s positions to know when the ball has passed a point where a score can be given to either the AI or Player. We could have defined these positions in code, but using the “AI_Win_Position” and “Player_Win_Position” nodes allows us to have any sized game area without needing to adjust the code.

On line 34 we call randomize. This gets a new seed for the random number generator using the system time of the computer. This makes the random number generator mostly random for each play through of the game, and we want this because we are going to be using the random number generator for setting which direction the ball starts going towards.

Finally, on line 36 we call a function named spawn_ball and on line 37 we call a function named update_score_label. Both of these functions are what we will be covering next, and they do as their name implies: The first spawns the ball and the second updates the score label to reflect the current score.

spawn_ball function

The first line of code in the spawn_ball function, line 41, sets the speed of the ball back to BALL_SPEED_GAME_START. The next line sets the ball’s global position to the global position of the Position2D stored in ball_spawn_pos.

Tip

You might be wondering why we are using global_position instead of just position. This is because position is the offset of the node relative to its parent, while global_position is the offset of the node relative to the scene origin.

For this project, we could use either since everything is parented to the same root node and that root node is not moving, but generally it is a good habit to use global_position when you are wanting to position something since the position is the same regardless of what the node is parented to.

Next we make a local variable called ball_horizontal direction and set it to 1. Then we check to see if the randf function, which returns a random float in the range of 0 to 1, is more than or equal to 0.5. If it is, we set ball_horizontal_direction to -1.

Finally, the last bit of code in the function is line 46, where we set the ball_direction variable. We set ball_direction to a new Vector2, with the X value being ball_horizontal_direction and the Y value being a random number between -1 and 1 using rand_range. We then normalize the vector, so it has a length/magnitude of 1.

update_score_label function

This function is really simple, as all we are doing is changing the text property to a string composed of the score_player float (converted to a string using the str function), adding " | " so there is a visual separator, and finally adding a string composed of the score_ai float.

This will make the label in the middle of the pong arena always reflect the current score of the game, and we can update this label by simply calling the update_score_label function.

_physics_process function

All we are doing here is calling three functions: process_ball_movement, process_ai_movement, and process_player_movement. These functions are responsible for moving the ball, the AI, and allowing the player to move their paddle.

The _physics_process function is called once every physics frame, meaning that all three of those functions listed will be called routinely. delta, the only argument passed-in and the argument we are passing, is the amount of time (in seconds) that has passed since the last _physics_process call. delta allows us to write code that runs independent of the frames per second (FPS) of the computer, instead giving us a method to write code that is dependent on real-world time instead.

_process_ball_movement function

Of all of the code, this is the most complicated. That said, let’s walk through it.

First, we tell the ball to move in the physics world and return its collision info, if there was a collision, by using the move_and_collide function. We are passing a single argument to the move_and_collide function, and that is the direction we want the ball to move towards. The direction we want the ball to move towards is ball_direction, but we want it to move at ball_speed speed, so we multiply and finally we multiply by delta to make it FPS independent. We take the collision data returned by the move_and_collide function and place it in a local variable called ball_collision_info.

Next, line 61, we check to see if there was a collision by checking to make sure the ball_collision_info variable is not null and that the normal (the “bounce” of the surface the ball collided with) is not zero.

If the ball did collide with something, we move on to line 62, which checks to see if the normal has an X value of 0. If it does, then that means that whatever the ball collided with left it with a “bounce” that is purely vertical. In this case, we know that it has to be one of the boundaries on the top or bottom of the screen. When this happens, all we do is we set the ball_direction.y value to the ball_collision_info.normal.y value, which makes the ball “bounce” off the wall.

If the ball collided with something but it was not a purely vertical collision, we move to line 64, which checks to see if the collision was purely horizontal instead.

If the collision was purely horizontal, we then check to see if the collider, what the ball hit, is equal to either the AI or player paddle on line 65.

If the ball did collide with one of the paddles, we first set the ball’s horizontal direction, ball_direction.x, to the normal.x of the collision so it bounces away on the horizontal axis. We then make the ball bounce away on the Y axis based on its position relative to the center of the paddle by getting the direction from the ball to the collider/paddle the ball collided with. We do this using the direction_to function, which returns a Vector2 pointing from the paddle to the ball (for line 67). We set the ball_direction.y to the direction vector we calculated on line 67, which makes the ball bounce up if it hits the top of the paddle and down if it hits the bottom. On line 68 we normalize the ball_direction so it has a magnitude of 1, and finally on line 69 we increase the speed of the ball, ball_speed, by BALL_SPEED_PLAYER_BOUNCE_INCREASE so the ball moves faster.

On line 70, an else statement, is called when the ball has collided on a horizontal surface but it is not the player or the AI paddle. If this occurs, we just set the ball_direction.x to the normal.x, so it bounces away from the surface.

Finally, on line 72, we have an else statement that will only occur if the collision normal returned is both on the X and Y axis. If this happens, we set the ball_direction to the normal. Note that this event should not occur, but it has been added just to cover all bases.

With that, we have handled all of the movement for the ball! The last bits of code in process_ball_movement are just for handling score.

On line 75, we check to see if the ball’s global position on the X axis is less than the X position we have stored in ball_pos_ai_score. If it is, then we know that the AI has gotten the ball past the player and has scored a point. First, we increase score_ai by 1, then we update the score label using the update_score_label function we wrote earlier, and finally we call spawn_ball to respawn the ball at the start.

On line 79, we have an elif satement that does much the same thing, but for the player instead of the AI. First, it checks to see if the ball has a X coordinate greater than the X coordinate stored in ball_pose_player_score. If this occurs, then we add one to score_player, call update_score_label, and finally call spawn_ball.

process_ai_movement function

For our Pong AI, we are keeping it super simple. First, we get the direction from the AI paddle to the ball using the direction_to function. This will return us a Vector2 with a magnitude of 1 that points from the AI paddle to the ball, and we store this in a local variable called ai_to_ball_dir.

Next, line 87, we cancel out the X coordinate of ai_to_ball_dir by setting it to zero. This now gives us a Vector2 that points from the AI paddle to the ball, but only on the Y axis.

Next, we check to see if the Y axis value for ai_to_ball_dir is over 0.4 or -0.4, and if it is, we set ai_to_ball_dir.y to 1 or -1 respectfully. We do this so the AI paddle moves at full speed if the ball is moderately far above or below. Without this, the paddle would always move smoothly to try and block the ball, leading to cases where it does not arrive in time. This code makes the AI a little harder to beat, in addition to making it act more like a real player.

Finally, on line 94, we move the AI paddle using the move_and_collide function, passing the ai_to_ball_dir as the direction, multiply by AI_SPEED for speed, and multiplying by delta to make it FPS independent.

Note

If you are wondering what # warning-ignore:return_value_discarded is for, it is to disable a GDScript warning that tells us that we are not using the return value from the move_and_collide function. For the AI paddle, we have no reason to process the collision data, unlike the ball where we do, so we just ignore whatever collision data (or not) is returned. By adding the line of code at 93, it removes a warning that otherwise would be printed simply for not storing the result of the move_and_collide function.

process_player_movement function

The last function of the code! Thankfully, it is also a super simple function.

First, we check to see if the ui_up action is being pressed/held using the Input.is_action_pressed function. The Input.is_action_pressed function will return true if the key(s) assigned to the input action are pressed and held, and will return false if the key is not being held/pressed.

Tip

The ui_up and ui_down actions are default, Godot input actions that are included in every project! They are required input actions that are used with UI elements, and since they are in every project, we are taking advantage of that to move the player paddle here. The up and down arrow-keys are bound to ui_up and ui_down respectfully.

If the ui_up action is pressed, we move the player paddle, body_player, using the move_and_collide function. We pass the Vector2.UP constant as the direction, multiply by PLAYER_SPEED for speed, and finally multiply by delta to make it FPS independent.

We do the same thing for ui_down, but instead of using Vector2.UP constant as the direction, we use Vector2.DOWN.

Final Notes

With that, we have finished our game of Pong! Go ahead and give the project a try by clicking the play arrow at the top right of the screen. It may ask you to choose the default scene for the project, in which case chose the OneFilePong.tscn (or whatever you have called your scene) as the main scene.

I hope you enjoyed this tutorial! It is a shorter tutorial, but I wanted to make it to help show how easily Godot allows for making a game in just a short amount of time and with hardly any code. Additionally, with the exception of the font used, there was no graphics assets needed either!

There are many places you could take this. For example, you could add a 2 player mode by making the AI paddle controllable, could add additional geometry to the level, add power ups, and more!

Tip

You can find the download links for this project on the RandomMomentania download page!

Credits to Kenney for the font used in the project! Go check out the amazing CC0 assets on Kenney’s website right here: https://www.kenney.nl/

If you have any feedback, questions, or concerns, please let us know either through the contact page, through one of our contract pages, or through the Godot Community Forums (@TwistedTwigleg). Thanks for reading!