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!
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.
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:
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;
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;
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();
score_label.text = str(score_player) + " | " + str(score_ai);
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;
ball_direction.x = ball_collision_info.normal.x;
ball_direction = ball_collision_info.normal;
if (body_ball.global_position.x < ball_pos_ai_score):
score_ai += 1;
elif (body_ball.global_position.x > ball_pos_player_score):
score_player += 1;
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;
ai_to_ball_dir.y = -1;
body_ai.move_and_collide(ai_to_ball_dir * AI_SPEED * delta);
body_player.move_and_collide(Vector2.UP * PLAYER_SPEED * delta);
body_player.move_and_collide(Vector2.DOWN * PLAYER_SPEED * delta);
Let’s go over what this code does!
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
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.
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.
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
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.
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
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
All we are doing here is calling three functions:
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
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.
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
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_PLAYER_BOUNCE_INCREASE so the ball moves faster.
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.
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.
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
update_score_label, and finally call
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
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.
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.
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
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!