Hero image!

Pong clone in Godot

September 20, 2023 - Drygast
Basics Game Dev GDScript Godot

To reiterate from my previous godot related post (hello world). I'm no expert with the Godot Engine; I'm just documenting my journey as I learn here. My experience is mostly trial-and-error with lots of research along the way. Through this "log", I hope to share what I've learned so far and provide a starting point for others who may be in a similar situation.

If you want to try a HTML version of this project before reading all of this, you can find it HERE.

First actual godot game project

In this post I will create and setup the project in Godot Engine v4.1.1, import som graphics assets and write basic code in GDScript to complete and play a clone of the classic pong game. If things do not work when downloading this project, please check what version of the engine you are using. Things will change over time and some changes will break previous implementations.

Start with opening the Godot Engine and create a new project.

Create New Project

My folder was not empty since I had already cloned my almost empty git repo here.

Folder is not empty

Almost empty folder except for the basic git things.

Git and readme.md already in the folder

Next, create a new scene. I used the name "main" since I think that makes sense.

Create New Scene
Create New Scene Options

After a little bit of searching I found that one version of pong had the width and height of 858x525 pixels, so I used that as my default viewport settings.

Viewport Size

Next, I added the 3 images that I had prepared earlier. They were really basic images of the ball, paddles and the center divider

Add images

Next I created 4 Sprite2D nodes to act as the actual objects that I want to display on screen.

Create 2D Sprite nodes

Next I added som textures (the images I imported earlier) to the nodes.

Add texture to the nodes
New texture from image

After setting some default values for the objects positions, this is what the viewport in the editor looked like.

Viewport
 

At this time I thought it would be a good idea to run the project just to make sure it worked. Simply hit the F5 key and this is what you should see:

Running the project for the first time

I hade to change the default background color to black instead of the default gray.

Setting the background color

I also needed som text to display the score for each player. I simply added 2 Label nodes.

Create New Labels for the score

I changed the default color and size of these Label nodes.

Change color and font size for the labels

I also had to align the text to the center of the Label. This allowed me to expand the actual Label boundaries to fill the entire left and right side knowing that the score will always be aligned in the middle.

Align the text to center within the label

Let's add som code

Right click the main node in the scene and select "Attach Script".

Add script

Make sure to change the path of the new script file to "res://src/main.gd" so that the file is stored in the src directory.

Change path of script

After the script has been created, this is the default content if the main.gd file:

extends Node2D


# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass

First thing I reacted to was the 'pass' keyword that I did not recognise. A quick google later and here is what it does:
The pass keyword does nothing except preventing the compiler from showing the empty function soft error.
So for now, let's just ignore it.

The next thing I wanted to do was to move the paddle just to feel like there was some sort of interaction with the game. So with some minor setup of the input-map and a few small changes to the code, we can now alter the position of the left paddle.

Input map

extends Node2D

# Member variables
var paddleMoveSpeed = 250

# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	if Input.is_action_pressed("player_up"):
		get_node("paddle-left").position.y -= paddleMoveSpeed * delta
	if Input.is_action_pressed("player_down"):
		get_node("paddle-left").position.y += paddleMoveSpeed * delta

Now, I wanted to write some info about every single change I made to the code, but honestly I think I will just dump the final script instead and make sure that it has sufficient comments within it. Hopefully that is enough to explain what is being done and how things work. So here goes:

extends Node2D

# Member variables
var screen_size
var paddleMoveSpeed = 250
var paddleWidth = 8;
var paddleHeight = 32;
var ballMoveSpeed = 200
var ballMoveDirection = Vector2(1.0, 0.0)
var playerScore = 0
var computerScore = 0

# Called when the node enters the scene tree for the first time.
func _ready():
	# Grab and save the size of the current viewport for coming calculations
	screen_size = get_viewport_rect().size
	resetGame()

# Used to set default variables and positions
func resetGame():
	# Score as Integers
	playerScore = 0
	computerScore = 0
	updateScore()
	resetBallPosition()

# Reposition the ball to the center of the screen
func resetBallPosition():
	get_node("ball").position = screen_size / 2;
	# We could use X and Y separatly, but the above works for both
	# get_node("ball").position.x = screen_size.x / 2;
	ballMoveDirection = Vector2(1.0, 0.0)

# Write score and convert from Integer to String
func updateScore():
	get_node("score-left").text = str(playerScore)
	get_node("score-right").text = str(computerScore)

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	# Move player paddle if input key pressed
	if Input.is_action_pressed("player_up"):
		get_node("paddle-left").position.y -= paddleMoveSpeed * delta
	if Input.is_action_pressed("player_down"):
		get_node("paddle-left").position.y += paddleMoveSpeed * delta
	
	# Get the current position of the ball and do calculations with it
	# to figure out where it is supposed to be in the next frame
	var ballPosition = get_node("ball").position
	ballPosition += ballMoveDirection * ballMoveSpeed * delta
	
	# Check if the ball is outside screen bounds right and left
	if (ballPosition.x > screen_size.x):
		playerScore += 1
		updateScore()
		resetBallPosition()
		return
	if (ballPosition.x < 0):
		computerScore += 1
		updateScore()
		resetBallPosition()
		return
	
	# Bounce top or bottom boundaries
	if (ballPosition.y >= screen_size.y) or (ballPosition.y <= 0):
		ballMoveDirection.y = -ballMoveDirection.y
	
	# Grab the left paddle position
	var paddleLeftPosition = get_node("paddle-left").position
	
	# Make sure that the paddle does not go outside the screen
	if(paddleLeftPosition.y < (paddleHeight/2)):
		get_node("paddle-left").position.y = (paddleHeight/2)
	if(paddleLeftPosition.y > (screen_size.y - (paddleHeight/2))):
		get_node("paddle-left").position.y = screen_size.y - (paddleHeight/2)
	
	# Create a temporary hitbox for the two paddles
	var paddleLeftRect = Rect2(paddleLeftPosition.x, paddleLeftPosition.y-(paddleHeight/2), paddleWidth, paddleHeight)
	var paddleRightRect = Rect2(get_node("paddle-right").position.x, get_node("paddle-right").position.y - (paddleHeight / 2), paddleWidth, paddleHeight)
	
	# Check if projected position of the ball is inside on of the paddles
	if ((paddleLeftRect.has_point(ballPosition) and ballMoveDirection.x < 0) or (paddleRightRect.has_point(ballPosition) and ballMoveDirection.x > 0)):
		ballMoveDirection.x = -ballMoveDirection.x
		ballMoveDirection.y = randf()*2.0 - 1
		ballMoveDirection = ballMoveDirection.normalized()
	
	# Move the computer player paddle (simple chase Y position thing)
	if(get_node("paddle-right").position.y > ballPosition.y):
		get_node("paddle-right").position.y -= (paddleMoveSpeed/2) * delta
	if(get_node("paddle-right").position.y < ballPosition.y):
		get_node("paddle-right").position.y += (paddleMoveSpeed/2) * delta
	
	# Actually move the ball
	get_node("ball").position = ballPosition

And thats it! Save the files, run the project by pressing the F5 key and you're done. You could export the project in a few different ways, but I'll ignore that for the moment (except for exporting to HTML and publish a playable version to my site HERE).

Export project

Final thoughts

So that was pretty easy. Still having problems with the GDScript syntax and formatting, but I guess that will be easier in time. There's also the fact that I just dumped all code into one file that I'm not usually a fan of, but for a project this small, I'll let it slide. Overall - I liked the experience and will start thinking about my next project straight away.

You can find a playable version of this project HERE, and the Azure repo with the code HERE.