Prerequisite: This tutorial is targeted to users who are familiar with basics of Godot engine. If you are in doubt, you can take a look at this basic Godot tutorial before continuing the Pong game tutorial.
Pong is one of the earliest arcade video games, developed by Atari and released in 1972. It’s a two-dimensional sports game that simulates table tennis. The game features two paddles and a ball. Each player controls a paddle in a vertical motion across the left or right side of the screen. The goal is to use the paddle to hit the ball back and forth, aiming to score points by making the ball miss the opponent’s paddle.
Pong’s simple yet engaging gameplay made it a massive hit, leading to the creation of numerous similar games and contributing significantly to the popularity of video games in the early 1970s. Its success laid the foundation for the gaming industry we know today.
In this post, we will create pong in Godot engine 4.3.
Breakdown
- We have to create a ball that follows Newtonian physics. And give it some initial velocity at the start of the game.
- We have to create two paddles (or bats). Both of them are same except both are controlled by different players.
- A level that has instances of paddles and a ball object. Overall game simulation occurs in the level.
What we will do in Godot engine
- A paddle is made as a
CharacterBody2D
node which will go upward or downward via a script. - Ball is made as a
RigidBody2D
, so Newtonian physics will be inherited by it asRigidBody2D
implements 2D Newtonian physics. - Level is made as a scene. In Level scene:
- Ball is instanced and initially given a random impulse and after that, we must control it using the two pedals.
- Two instances of Pedal are created. Both of them are assigned separate player, so they listen to different input events.
- Goals are made behind each player pedal, if ball goes into goal-1, player-2 gets point else player-1 gets point.
- Their scores are written in the score GUI labels.
- Table tennis-like spin in the ball is also present due to nature of
RigidBody2D
.
GitHub Project Link + Assets
You can use all the assets from the GitHub project. Remember that all those assets that are not mine are given credit in a.txt
file under any folder.
Creating a Ball
Ball is a scene with a RigidBody2D
root node. Its gravity_scale
is set to 0.0 so it is not effected by the gravity and thus should float freely in space.
Below the root node, add a sprite with ball texture & a collision shape of a circle. Remember to make ball scene in a separate /Ball/
folder, so it will be neat and modular.
The ball scene overall looks like this:
After you have create ball scene, attach a script Ball.gd
to the root node.
Make sure to connect the body_entered
signal on root node (RigidBody2D
) to a function in script. Overall script code should be this:
Ball.gd:
extends RigidBody2D
func _ready():
apply_central_impulse(Vector2(-200, 0))
func _on_body_entered(body):
if linear_velocity.length() < 232:
apply_central_impulse(linear_velocity.normalized() * 10)
if linear_velocity.normalized().y > 0.95:
apply_central_impulse(Vector2(-50, 0))
Above code just applies impulse on the ball to give it an initial push towards left. Also, in _process()
, we need to make sure that the speed of ball must never decrease too much, so I gave it a small boost if its speed falls below 232
.
Also, if the ball is continually moving up and down (vertically), and is stuck, we will just add a horizontal boost to it so it starts moving slightly horizontal and can then reach the pedals.
Creating a Pedal
Pedal/Racket is a simple scene that has CharacterBody2D
as its root and looks like this:
Polygon2D
is just a node to draw 2D polygon shapes. You can use a rectangle texture. Add a collision shape to the scene as well.
Add script to this scene:
Pedal.gd:
extends CharacterBody2D
@export var speed: float = 2048
@export var player: int = 0 # 0 or 1
@export var custom_color: Color = Color.WHITE
@onready var initial_x_position = position.x
func _ready():
modulate = custom_color
func _physics_process(delta: float) -> void:
velocity -= velocity * delta * 4.0
if player == 0:
if Input.is_action_pressed("ui_up"):
velocity.y -= speed * delta
if Input.is_action_pressed("ui_down"):
velocity.y += speed * delta
if player == 1:
if Input.is_action_pressed("ui_home"):
velocity.y -= speed * delta
if Input.is_action_pressed("ui_end"):
velocity.y += speed * delta
velocity = round(velocity * 30.0) / 30.0
move_and_slide()
position.x = initial_x_position
In above code, we have two options for player
variable, either it is 0
or 1
. If player is 0
, it only listens to arrow up
or arrow down
keys for moving up and down. And if it is 1
, it listens to home
and end
keys.
initial_x_position
variable hold the pedal’s initial position. The reason to ad this is that the ball when colliding with pedal pushes the pedal from its original position. Since its x-axis position must remain at same place and only y-axis movement is allowed, I stored its initial position so when rigid-body collides with it and tries to push it, its original position can be overridden and it stays at same position.
Apart from that, the code is just assigning values to y-component of velocity. The following snippet of code is used to make the velocity a bit jagged/pixelated so it feels like older games:
velocity = round(velocity * 30.0) / 30.0
Whenever we spawn a pedal, we just have to assign its player as 0 or 1 and it will work and be able to push the moving rigid-bodies.
Making the Pong Level
Create Level
as a scene under a folder named Level
.
Replicating Pedals
We have to create two instances of pedals.
Spawning the Ball
Ball can be added directly to the scene tree, but we want to spawn new ball every time old one is deleted after having a goal. So, for now, we just have to add a logic to spawn a ball on game start. And spawn ball every time existing ball is found to be null
.
Add a script to the level as Level.gd:
extends Node2D
@export var ball: PackedScene
var current_ball: Node2D
func _ready():
# instantiate ball in middle of screen
var ball_instance = ball.instantiate()
add_child(ball_instance)
ball_instance.global_position = Vector2(
get_viewport().size.x / 4,
get_viewport().size.y / 4
)
current_ball = ball_instance
func _process(delta):
if current_ball == null:
var ball_instance = ball.instantiate()
add_child(ball_instance)
ball_instance.global_position = Vector2(
get_viewport().size.x / 4,
get_viewport().size.y / 4
)
current_ball = ball_instance
Make sure to assign the ball scene in the inspector; as @export
keyword in this script has added a slot for assigning a ball scene in editor’s inspector.
in above code, it spawns a ball in the middle of the screen (by getting viewport width/height and dividing it to get 1/4 of its values).
Adding the Goals & Side Barriers
We need to prevent the ball from crossing the screen in vertical y-axis direction. And we need to make the horizontal ends of screen pass-able but passing through them must be registered as a goal for a player.
So we must add StaticBody2D
on up and down ends of screen. And Area2D
nodes of left and right ends of screen. This is how the setup looks like:
Notice the nodes named Goal1
and Goal2
. They have signals connected for body_entered
. Whenever a body (i.e. our ball) enters Goal2
, right-side player gets a point, and when ball hits the Goal1
area, left-side player gets a point. In code, thus we have to create 2 more variables for storing the scores and we must implement the two signals that we connected earlier:
Level.gd:
var left_score = 0 # Player 1 score
var right_score = 0 # Player 2 score
# _proces(delta) and _ready() are here...
func _on_goal_1_body_exited(body):
left_score += 1
current_ball.queue_free()
current_ball = null
func _on_goal_2_body_exited(body):
right_score += 1
current_ball.queue_free()
current_ball = null
Adding Score Label
In our scene, we want to visualize the scores of the player. So we must add some nodes to represent the labels text.
I added a CanvasLayer
to separate GUI from gameplay. And then added a Control
node below it, and two Label
nodes below that control node. Setup looks like this (with GUI labels added):
In above setup, I applied a pixel-art font on the labels to make them look good. Font name is 04b03 Font. It is shipped with the same GitHub repo I linked.
And in Level.gd script, replace the two functions so UI labels are updated with the score values whenever the score changes:
func _on_goal_1_body_exited(body):
left_score += 1
$CanvasLayer/Control/Label1.text = "Left: " + str(left_score)
current_ball.queue_free()
current_ball = null
func _on_goal_2_body_exited(body):
right_score += 1
$CanvasLayer/Control/Label2.text = "Right: " + str(right_score)
current_ball.queue_free()
current_ball = null
Thank you for reading <3