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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
extends Control const AI_SPEED : float = 280.0; const PLAYER_SPEED : float = 280.0; var ball_speed : float = 300.0; const BALL_SPEED_GAME_START : float = 300.0; const BALL_SPEED_PLAYER_BOUNCE_INCREASE : float = 20.0; var ball_direction : Vector2 = Vector2.ZERO; var ball_spawn_pos : Position2D = null; var ball_pos_player_score : float = 0.0; var ball_pos_ai_score : float = 0.0; var body_ball : KinematicBody2D; var body_player : KinematicBody2D; var body_ai : KinematicBody2D; var score_player : int = 0; var score_ai : int = 0; var score_label : Label = null; func _ready(): ball_spawn_pos = get_node("Ball_Spawn_Pos"); body_ball = get_node("Ball"); body_player = get_node("Paddle_Player"); body_ai = get_node("Paddle_AI"); score_label = get_node("Score_Label"); ball_pos_ai_score = get_node("AI_Win_Position").global_position.x; ball_pos_player_score = get_node("Player_Win_Position").global_position.x; randomize(); spawn_ball(); update_score_label(); func spawn_ball(): ball_speed = BALL_SPEED_GAME_START; body_ball.global_position = ball_spawn_pos.global_position; var ball_horizontal_direction = 1; if (randf() >= 0.5): ball_horizontal_direction = -1; ball_direction = Vector2(ball_horizontal_direction, rand_range(-1, 1)).normalized(); func update_score_label(): score_label.text = str(score_player) + " | " + str(score_ai); func _physics_process(delta): process_ball_movement(delta); process_ai_movement(delta); process_player_movement(delta); func process_ball_movement(delta): var ball_collision_info = body_ball.move_and_collide(ball_direction * ball_speed * delta); if (ball_collision_info != null and ball_collision_info.normal != Vector2.ZERO): if (ball_collision_info.normal.x == 0): ball_direction.y = ball_collision_info.normal.y; elif (ball_collision_info.normal.y == 0): if (ball_collision_info.collider == body_ai or ball_collision_info.collider == body_player): ball_direction.x = ball_collision_info.normal.x; ball_direction.y = ball_collision_info.collider.global_position.direction_to(body_ball.global_position).y; ball_direction = ball_direction.normalized(); ball_speed += BALL_SPEED_PLAYER_BOUNCE_INCREASE; else: ball_direction.x = ball_collision_info.normal.x; else: ball_direction = ball_collision_info.normal; if (body_ball.global_position.x < ball_pos_ai_score): score_ai += 1; update_score_label(); spawn_ball(); elif (body_ball.global_position.x > ball_pos_player_score): score_player += 1; update_score_label(); spawn_ball(); func process_ai_movement(delta): var ai_to_ball_dir = body_ai.global_position.direction_to(body_ball.global_position); ai_to_ball_dir.x = 0; if (ai_to_ball_dir.y > 0.4 or ai_to_ball_dir.y < 0.4): if (ai_to_ball_dir > 0): ai_to_ball_dir.y = 1; else: ai_to_ball_dir.y = -1; # warning-ignore:return_value_discarded body_ai.move_and_collide(ai_to_ball_dir * AI_SPEED * delta); func process_player_movement(delta): if (Input.is_action_pressed("ui_up")): # warning-ignore:return_value_discarded body_player.move_and_collide(Vector2.UP * PLAYER_SPEED * delta); elif (Input.is_action_pressed("ui_down")): # warning-ignore:return_value_discarded body_player.move_and_collide(Vector2.DOWN * PLAYER_SPEED * delta); |
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!
1 thought on “Godot Pong Tutorial”
Comments are closed.