I tried text-based development with Godot, a game engine well-suited for collaborative development with LLMs

I tried text-based development with Godot, a game engine well-suited for collaborative development with LLMs

I report on my experience implementing a simple ping pong game using Godot 4.x, which is attracting attention as a game engine well-suited for collaborative development with LLMs. I verified a development approach that minimizes GUI operations through text-based scene files and Python-like GDScript.
2025.07.16

This page has been translated by machine translation. View original

Introduction

In this article, we will implement a simple ping pong game using Godot 4.x, which is gaining attention as a game engine suitable for collaborative development with LLMs. It's a 2D game where the player controls the paddle at the bottom and competes against the CPU.

Game operation image

What is Godot?

Godot is an open-source cross-platform game engine. It is one of the major game development tools alongside Unity and Unreal Engine, and adopts a Python-like scripting language called GDScript. It is characterized by its lightweight engine size and fast startup.

Why Godot is suitable for collaborative development with LLMs

Godot's scene files (.tscn) are written in a human-readable format, making them easy for LLMs to understand and generate.

Example

[node name="Player" type="CharacterBody2D" parent="."]
position = Vector2(180, 650)
script = ExtResource("2_1hdqx")

Additionally, GDScript uses Python-like syntax, which is a language pattern that many LLMs excel at. Development using only a text editor, as in this case, is possible, enabling an interactive development environment with LLMs.

Target audience

  • Developers interested in game development using LLMs
  • Those who want to learn the basic usage of Godot

References

Implementation

First, I downloaded Godot from the official site, launched it, and created a new project.

Create new project

For this project, I selected Compatibility as the renderer, which is said to be "the fastest at rendering simple scenes."

Renderer selection

When the project is created, a folder is generated. I communicated the operational image of the game I wanted to create to the LLM, and created the following files in the folder. For this development, I used Claude Sonnet 4 as the LLM.

Main.tscn
[gd_scene load_steps=9 format=3 uid="uid://bvxqy8qkqwqxr"]

[ext_resource type="Script" path="res://Main.gd" id="1_0hdqx"]
[ext_resource type="Script" path="res://Player.gd" id="2_1hdqx"]
[ext_resource type="Script" path="res://CPU.gd" id="3_2hdqx"]
[ext_resource type="Script" path="res://Ball.gd" id="4_3hdqx"]

[sub_resource type="RectangleShape2D" id="RectangleShape2D_1"]
size = Vector2(80, 10)

[sub_resource type="RectangleShape2D" id="RectangleShape2D_2"]
size = Vector2(80, 10)

[sub_resource type="RectangleShape2D" id="RectangleShape2D_3"]
size = Vector2(16, 16)

[sub_resource type="RectangleShape2D" id="RectangleShape2D_4"]
size = Vector2(20, 720)

[sub_resource type="RectangleShape2D" id="RectangleShape2D_5"]
size = Vector2(360, 20)

[node name="Main" type="Node2D"]
script = ExtResource("1_0hdqx")

[node name="Player" type="CharacterBody2D" parent="."]
position = Vector2(180, 650)
script = ExtResource("2_1hdqx")

[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"]
shape = SubResource("RectangleShape2D_1")

[node name="ColorRect" type="ColorRect" parent="Player"]
offset_left = -40.0
offset_top = -5.0
offset_right = 40.0
offset_bottom = 5.0
color = Color(1, 1, 1, 1)

[node name="CPU" type="CharacterBody2D" parent="."]
position = Vector2(180, 70)
script = ExtResource("3_2hdqx")

[node name="CollisionShape2D" type="CollisionShape2D" parent="CPU"]
shape = SubResource("RectangleShape2D_2")

[node name="ColorRect" type="ColorRect" parent="CPU"]
offset_left = -40.0
offset_top = -5.0
offset_right = 40.0
offset_bottom = 5.0
color = Color(1, 1, 1, 1)

[node name="Ball" type="CharacterBody2D" parent="."]
position = Vector2(180, 360)
script = ExtResource("4_3hdqx")

[node name="CollisionShape2D" type="CollisionShape2D" parent="Ball"]
shape = SubResource("RectangleShape2D_3")

[node name="ColorRect" type="ColorRect" parent="Ball"]
offset_left = -8.0
offset_top = -8.0
offset_right = 8.0
offset_bottom = 8.0
color = Color(1, 1, 1, 1)

[node name="Walls" type="StaticBody2D" parent="."]

[node name="LeftWall" type="CollisionShape2D" parent="Walls"]
position = Vector2(-10, 360)
shape = SubResource("RectangleShape2D_4")

[node name="RightWall" type="CollisionShape2D" parent="Walls"]
position = Vector2(370, 360)
shape = SubResource("RectangleShape2D_4")

[node name="TopWall" type="CollisionShape2D" parent="Walls"]
position = Vector2(180, -10)
shape = SubResource("RectangleShape2D_5")

[node name="BottomWall" type="CollisionShape2D" parent="Walls"]
position = Vector2(180, 730)
shape = SubResource("RectangleShape2D_5")
Main.gd
extends Node2D

# Game settings
const SCREEN_WIDTH = 360
const SCREEN_HEIGHT = 720

# Score
var player_score = 0
var cpu_score = 0

# Game object references
@onready var player = $Player
@onready var cpu = $CPU
@onready var ball = $Ball

func _ready():
	# Screen size settings
	get_window().size = Vector2i(SCREEN_WIDTH, SCREEN_HEIGHT)
	get_window().position = Vector2i(100, 100)  # Adjust window position

	# Connect ball's score update signal
	ball.goal_scored.connect(_on_goal_scored)

	print("Pingpong game started!")
	print("Use A/D or Left/Right arrows to move your paddle")

func _on_goal_scored(scorer):
	if scorer == "player":
		player_score += 1
		print("Player scores! Score: Player ", player_score, " - CPU ", cpu_score)
	elif scorer == "cpu":
		cpu_score += 1
		print("CPU scores! Score: Player ", player_score, " - CPU ", cpu_score)

	# Game end check (first to 5 points)
	if player_score >= 5:
		print("Player wins!")
		_reset_game()
	elif cpu_score >= 5:
		print("CPU wins!")
		_reset_game()

func _reset_game():
	# Reset score
	player_score = 0
	cpu_score = 0

	# Reset ball position
	ball.reset_position()

	print("Game reset! New game started.")

func _input(event):
	# End game with ESC key
	if event.is_action_pressed("ui_cancel"):
		get_tree().quit()
Player.gd
extends CharacterBody2D

# Paddle settings
const SPEED = 300.0
const PADDLE_WIDTH = 80.0
const SCREEN_WIDTH = 360

# Collision shape settings
@onready var collision_shape = $CollisionShape2D

func _ready():
	# Set up paddle collision shape
	var shape = RectangleShape2D.new()
	shape.size = Vector2(PADDLE_WIDTH, 10)
	collision_shape.shape = shape

func _physics_process(delta):
	# Get input
	var input_direction = 0

	# Process keyboard input
	if Input.is_action_pressed("ui_left") or Input.is_action_pressed("move_left"):
		input_direction = -1
	elif Input.is_action_pressed("ui_right") or Input.is_action_pressed("move_right"):
		input_direction = 1

	# Set velocity
	velocity.x = input_direction * SPEED
	velocity.y = 0  # Restrict movement on Y axis

	# Execute movement
	move_and_slide()

	# Restrict at screen boundaries
	_clamp_to_screen()

func _clamp_to_screen():
	# Restrict paddle from going off-screen
	var half_width = PADDLE_WIDTH / 2
	position.x = clamp(position.x, half_width, SCREEN_WIDTH - half_width)

# Input map settings (other than actions automatically created in project settings)
func _input(event):
	# Movement with A/D keys (additional input options)
	pass
CPU.gd
extends CharacterBody2D

# CPU paddle settings
const SPEED = 200.0  # Set slightly slower than player
const PADDLE_WIDTH = 80.0
const SCREEN_WIDTH = 360
const REACTION_ZONE = 360.0  # Reacts when the ball is above this Y coordinate

# AI settings
const PREDICTION_FACTOR = 0.8  # Response level to ball's predicted movement
const DEAD_ZONE = 10.0  # Don't move paddle within this range

# Game object references
@onready var collision_shape = $CollisionShape2D
var ball_node: CharacterBody2D

func _ready():
	# Set up paddle collision shape
	var shape = RectangleShape2D.new()
	shape.size = Vector2(PADDLE_WIDTH, 10)
	collision_shape.shape = shape

	# Get reference to ball node
	ball_node = get_parent().get_node("Ball")

func _physics_process(delta):
	if not ball_node:
		return

	# Get ball position and velocity
	var ball_position = ball_node.global_position
	var ball_velocity = ball_node.velocity

	# Only react when ball is moving toward CPU side
	if ball_position.y < REACTION_ZONE and ball_velocity.y < 0:
		_move_towards_ball(ball_position, ball_velocity, delta)
	else:
		# Return to center when ball is far away
		_move_to_center(delta)

	# Execute movement
	move_and_slide()

	# Restrict at screen boundaries
	_clamp_to_screen()

func _move_towards_ball(ball_pos: Vector2, ball_vel: Vector2, delta: float):
	# Calculate ball's predicted position
	var time_to_reach = abs((position.y - ball_pos.y) / ball_vel.y) if ball_vel.y != 0 else 0
	var predicted_x = ball_pos.x + ball_vel.x * time_to_reach * PREDICTION_FACTOR

	# Calculate difference between paddle center and ball's predicted position
	var target_x = predicted_x
	var distance = target_x - position.x

	# Don't move within dead zone
	if abs(distance) < DEAD_ZONE:
		velocity.x = 0
		return

	# Move toward target
	var direction = sign(distance)
	velocity.x = direction * SPEED
	velocity.y = 0

func _move_to_center(delta: float):
	# Move toward screen center
	var center_x = SCREEN_WIDTH / 2
	var distance = center_x - position.x

	if abs(distance) < DEAD_ZONE:
		velocity.x = 0
		return

	var direction = sign(distance)
	velocity.x = direction * SPEED * 0.5  # Return to center more slowly
	velocity.y = 0

func _clamp_to_screen():
	# Restrict paddle from going off-screen
	var half_width = PADDLE_WIDTH / 2
	position.x = clamp(position.x, half_width, SCREEN_WIDTH - half_width)
Ball.gd
extends CharacterBody2D

# Ball settings
const INITIAL_SPEED = 250.0
const MAX_SPEED = 400.0
const SPEED_INCREMENT = 20.0  # Speed increases each time ball hits paddle
const BALL_SIZE = 16.0

# Screen settings
const SCREEN_WIDTH = 360
const SCREEN_HEIGHT = 720

# Game state
var current_speed = INITIAL_SPEED
var is_serving = true

# Signals
signal goal_scored(scorer)

# Game object references
@onready var collision_shape = $CollisionShape2D

func _ready():
	# Set up ball collision shape
	var shape = RectangleShape2D.new()
	shape.size = Vector2(BALL_SIZE, BALL_SIZE)
	collision_shape.shape = shape

	# Initial serve
	_serve()

func _physics_process(delta):
	if is_serving:
		return

	# Execute movement
	var collision = move_and_slide()

	# Process collision
	if get_slide_collision_count() > 0:
		_handle_collision()

	# Check goals
	_check_goals()

func _serve():
	# Initial settings for serve
	is_serving = true
	current_speed = INITIAL_SPEED
	position = Vector2(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2)

	# Serve in random direction (either up or down)
	var angle = randf_range(-PI/4, PI/4)  # Range from -45 to 45 degrees
	if randf() > 0.5:
		angle += PI  # 50% chance of going downward

	velocity = Vector2(cos(angle), sin(angle)) * current_speed
	is_serving = false

	print("Ball served with velocity: ", velocity)

func _handle_collision():
	var collision = get_slide_collision(0)
	var collider = collision.get_collider()

	if collider:
		var collision_normal = collision.get_normal()

		# Collision with paddle
		if collider.name == "Player" or collider.name == "CPU":
			_handle_paddle_collision(collider, collision_normal)

		# Collision with wall
		elif collider.name == "Walls":
			_handle_wall_collision(collision_normal)

func _handle_paddle_collision(paddle, normal: Vector2):
	# Calculate relative position from paddle center to ball
	var paddle_center = paddle.global_position.x
	var ball_center = global_position.x
	var relative_intersect = (ball_center - paddle_center) / 40.0  # Normalize by paddle half-width

	# Calculate reflection angle (adjust angle based on relative position)
	var bounce_angle = relative_intersect * PI/3  # Maximum 60 degree angle

	# Increase speed
	current_speed = min(current_speed + SPEED_INCREMENT, MAX_SPEED)

	# Set new velocity vector
	if paddle.name == "Player":
		# Player paddle (bottom to top)
		velocity = Vector2(sin(bounce_angle), -cos(bounce_angle)) * current_speed
	else:
		# CPU paddle (top to bottom)
		velocity = Vector2(sin(bounce_angle), cos(bounce_angle)) * current_speed

	print("Paddle hit! New speed: ", current_speed)

func _handle_wall_collision(normal: Vector2):
	# Reflection with wall (left and right walls only)
	if abs(normal.x) > 0.5:  # Left or right wall
		velocity.x = -velocity.x
		print("Wall bounce!")

func _check_goals():
	# Upper goal (player scores)
	if position.y < -20:
		goal_scored.emit("player")
		_serve()

	# Lower goal (CPU scores)
	elif position.y > SCREEN_HEIGHT + 20:
		goal_scored.emit("cpu")
		_serve()

func reset_position():
	# Initialize position when resetting game
	_serve()

# Display velocity for debugging
func _input(event):
	if event.is_action_pressed("ui_accept"):  # Space key
		print("Ball position: ", position, " Velocity: ", velocity, " Speed: ", current_speed)

I also modified project.godot as follows:

project.godot
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
;   [section] ; section goes between []
;   param=value ; assign values to parameters

config_version=5

[application]

config/name="simple-pong"
run/main_scene="res://Main.tscn"

[display]

window/size/viewport_width=360
window/size/viewport_height=720
window/size/resizable=false

[input]

move_left={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"echo":false,"script":null)
]
}
move_right={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"echo":false,"script":null)
]
}

[rendering]

renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"

Testing and fixes

After completing the implementation, I started the game by pressing the F5 key in the Godot editor.

simple-pingpong initial implementation

There were some issues with the operation:

  • The CPU moves forward each time it hits the ball
  • The ball gets stuck on the wall and doesn't come off
  • The game doesn't end when you lose
  • The ball speed is too slow

After repeated dialogue with the LLM and making several code revisions, these issues were resolved.

simple-pingpong after fixes

Corrected Code

Main.tscn
[gd_scene load_steps=10 format=3 uid="uid://cilbg8jwrfbyr"]

[ext_resource type="Script" uid="uid://efgbiogmat67" path="res://Main.gd" id="1_0hdqx"]
[ext_resource type="Script" uid="uid://d348gu2f10sit" path="res://Player.gd" id="2_1hdqx"]
[ext_resource type="Script" uid="uid://c6m0g0dnrbvff" path="res://CPU.gd" id="3_2hdqx"]
[ext_resource type="Script" uid="uid://tbmexcoveiia" path="res://Ball.gd" id="4_3hdqx"]

[sub_resource type="RectangleShape2D" id="RectangleShape2D_1"]
size = Vector2(80, 10)

[sub_resource type="RectangleShape2D" id="RectangleShape2D_2"]
size = Vector2(80, 10)

[sub_resource type="RectangleShape2D" id="RectangleShape2D_3"]
size = Vector2(16, 16)

[sub_resource type="RectangleShape2D" id="RectangleShape2D_4"]
size = Vector2(20, 720)

[sub_resource type="RectangleShape2D" id="RectangleShape2D_5"]
size = Vector2(360, 20)

[node name="Main" type="Node2D"]
script = ExtResource("1_0hdqx")

[node name="Player" type="CharacterBody2D" parent="."]
position = Vector2(180, 650)
script = ExtResource("2_1hdqx")

[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"]
shape = SubResource("RectangleShape2D_1")

[node name="ColorRect" type="ColorRect" parent="Player"]
offset_left = -40.0
offset_top = -5.0
offset_right = 40.0
offset_bottom = 5.0

[node name="CPU" type="CharacterBody2D" parent="."]
position = Vector2(180, 70)
script = ExtResource("3_2hdqx")

[node name="CollisionShape2D" type="CollisionShape2D" parent="CPU"]
shape = SubResource("RectangleShape2D_2")

[node name="ColorRect" type="ColorRect" parent="CPU"]
offset_left = -40.0
offset_top = -5.0
offset_right = 40.0
offset_bottom = 5.0

[node name="Ball" type="CharacterBody2D" parent="."]
position = Vector2(180, 360)
script = ExtResource("4_3hdqx")

[node name="CollisionShape2D" type="CollisionShape2D" parent="Ball"]
shape = SubResource("RectangleShape2D_3")

[node name="ColorRect" type="ColorRect" parent="Ball"]
offset_left = -8.0
offset_top = -8.0
offset_right = 8.0
offset_bottom = 8.0

[node name="Walls" type="StaticBody2D" parent="."]

[node name="LeftWall" type="CollisionShape2D" parent="Walls"]
position = Vector2(-10, 360)
shape = SubResource("RectangleShape2D_4")

[node name="RightWall" type="CollisionShape2D" parent="Walls"]
position = Vector2(370, 360)
shape = SubResource("RectangleShape2D_4")

[node name="TopWall" type="CollisionShape2D" parent="Walls"]
position = Vector2(180, -10)
shape = SubResource("RectangleShape2D_5")

[node name="BottomWall" type="CollisionShape2D" parent="Walls"]
position = Vector2(180, 730)
shape = SubResource("RectangleShape2D_5")
Main.gd
extends Node2D

# Game settings
const SCREEN_WIDTH = 360
const SCREEN_HEIGHT = 720
const WINNING_SCORE = 5

# Scores
var player_score = 0
var cpu_score = 0
var game_over = false

# Game object references
@onready var player = $Player
@onready var cpu = $CPU
@onready var ball = $Ball

func _ready():
	# Set screen size
	get_window().size = Vector2i(SCREEN_WIDTH, SCREEN_HEIGHT)
	get_window().position = Vector2i(100, 100)

	# Connect ball score update signal
	ball.goal_scored.connect(_on_goal_scored)

	print("Pingpong game started!")
	print("Use Arrow Keys to move your paddle")
	print("First to ", WINNING_SCORE, " points wins!")

func _on_goal_scored(scorer):
	print("Goal scored by: ", scorer)

	if game_over:
		print("Game is already over, ignoring goal")
		return

	if scorer == "player":
		player_score += 1
		print("Player scores! Score: Player ", player_score, " - CPU ", cpu_score)
	elif scorer == "cpu":
		cpu_score += 1
		print("CPU scores! Score: Player ", player_score, " - CPU ", cpu_score)

	# Check for game over
	if player_score >= WINNING_SCORE:
		print("=== PLAYER WINS! ===")
		_end_game()
	elif cpu_score >= WINNING_SCORE:
		print("=== CPU WINS! ===")
		_end_game()

func _end_game():
	game_over = true
	# Stop the ball
	ball.velocity = Vector2.ZERO
	ball.is_serving = true

	print("Game Over! Press SPACE to restart or ESC to quit")

func _reset_game():
	print("Resetting game...")

	# Reset game state
	game_over = false
	player_score = 0
	cpu_score = 0

	# Reset object positions
	player.position = Vector2(180, 650)
	cpu.position = Vector2(180, 70)
	ball.reset_position()

	print("Game reset! New game started.")

func _input(event):
	# ESC key to quit game
	if event.is_action_pressed("ui_cancel"):
		print("Quitting game...")
		get_tree().quit()

	# Space key to restart
	if event.is_action_pressed("ui_accept"):
		if game_over:
			_reset_game()
		else:
			print("Current score: Player ", player_score, " - CPU ", cpu_score)

	# R key to restart (for debugging)
	if Input.is_key_pressed(KEY_R):
		_reset_game()

# Debug information display
func _process(delta):
	if Input.is_action_just_pressed("ui_select"):  # Tab key
		print("Ball position: ", ball.position)
		print("Ball velocity: ", ball.velocity)
		print("Game over: ", game_over)
		print("Is serving: ", ball.is_serving)
Player.gd
extends CharacterBody2D

# Paddle settings
const SPEED = 300.0
const PADDLE_WIDTH = 80.0
const SCREEN_WIDTH = 360

# Game object references
@onready var collision_shape = $CollisionShape2D

func _ready():
	# Set up paddle collision shape
	var shape = RectangleShape2D.new()
	shape.size = Vector2(PADDLE_WIDTH, 10)
	collision_shape.shape = shape

func _physics_process(delta):
	# Get input
	var input_direction = 0

	# Process keyboard input
	if Input.is_action_pressed("ui_left"):
		input_direction = -1
	elif Input.is_action_pressed("ui_right"):
		input_direction = 1

	# Update position directly
	if input_direction != 0:
		position.x += input_direction * SPEED * delta

	# Limit at screen boundaries
	_clamp_to_screen()

	# Fix Y coordinate
	position.y = 650

func _clamp_to_screen():
	# Restrict paddle from going off-screen
	var half_width = PADDLE_WIDTH / 2
	position.x = clamp(position.x, half_width, SCREEN_WIDTH - half_width)
CPU.gd
extends CharacterBody2D

# CPU paddle settings
const SPEED = 220.0
const PADDLE_WIDTH = 80.0
const SCREEN_WIDTH = 360
const REACTION_ZONE = 400.0  # React to ball above this Y coordinate

# AI settings
const PREDICTION_FACTOR = 0.7  # Ball prediction accuracy
const DEAD_ZONE = 15.0  # Don't move within this range
const CENTER_RETURN_SPEED = 0.4  # Center return speed factor

# Game object references
@onready var collision_shape = $CollisionShape2D
var ball_node: Node2D
var target_x = 0.0

func _ready():
	# Set up paddle collision shape
	var shape = RectangleShape2D.new()
	shape.size = Vector2(PADDLE_WIDTH, 10)
	collision_shape.shape = shape

	# Get reference to ball node
	ball_node = get_parent().get_node("Ball")
	target_x = SCREEN_WIDTH / 2  # Initial target is center

func _physics_process(delta):
	if not ball_node:
		return

	# Wait in center when ball is serving
	if ball_node.is_serving:
		_move_to_center(delta)
	else:
		# Get ball state
		var ball_position = ball_node.position
		var ball_direction = ball_node.ball_direction
		var ball_speed = ball_node.current_speed

		# Only actively respond when ball is moving toward CPU
		if ball_position.y < REACTION_ZONE and ball_direction.y < 0:
			_track_ball(ball_position, ball_direction, ball_speed, delta)
		else:
			# Return to center when ball is far
			_move_to_center(delta)

	# Execute movement
	_execute_movement(delta)

	# Limit at screen boundaries
	_clamp_to_screen()

	# Fix Y coordinate (prevent forward movement)
	position.y = 70

func _track_ball(ball_pos: Vector2, ball_dir: Vector2, ball_speed: float, delta: float):
	# Calculate predicted ball position
	var time_to_cpu = 0.0
	if ball_dir.y != 0:
		time_to_cpu = abs((position.y - ball_pos.y) / (ball_dir.y * ball_speed))

	# Calculate predicted position
	var predicted_x = ball_pos.x + ball_dir.x * ball_speed * time_to_cpu * PREDICTION_FACTOR

	# Limit to screen boundaries
	predicted_x = clamp(predicted_x, PADDLE_WIDTH/2, SCREEN_WIDTH - PADDLE_WIDTH/2)

	target_x = predicted_x

	# Debug info
	if randf() < 0.01:  # 1% chance to output (prevent spam)
		print("CPU tracking - Ball pos: ", ball_pos, " Predicted: ", predicted_x, " Target: ", target_x)

func _move_to_center(delta: float):
	# Set screen center as target
	target_x = SCREEN_WIDTH / 2

func _execute_movement(delta: float):
	# Calculate difference between current and target position
	var distance = target_x - position.x

	# Don't move within dead zone
	if abs(distance) < DEAD_ZONE:
		return

	# Determine movement direction and speed
	var direction = sign(distance)
	var move_speed = SPEED

	# Move slower when returning to center
	if target_x == SCREEN_WIDTH / 2:
		move_speed *= CENTER_RETURN_SPEED

	# Update position directly (don't use physics engine)
	position.x += direction * move_speed * delta

func _clamp_to_screen():
	# Restrict paddle from going off-screen
	var half_width = PADDLE_WIDTH / 2
	position.x = clamp(position.x, half_width, SCREEN_WIDTH - half_width)

# For debugging
func _input(event):
	if event.is_action_pressed("ui_select"):  # Tab key
		if ball_node:
			print("CPU Debug:")
			print("  Position: ", position.x)
			print("  Target: ", target_x)
			print("  Ball pos: ", ball_node.position)
			print("  Ball dir: ", ball_node.ball_direction)
			print("  Ball serving: ", ball_node.is_serving)
Ball.gd
extends CharacterBody2D

# Ball settings
const INITIAL_SPEED = 350.0
const MAX_SPEED = 500.0
const SPEED_INCREMENT = 25.0
const BALL_SIZE = 16.0

# Screen settings
const SCREEN_WIDTH = 360
const SCREEN_HEIGHT = 720

# Game state
var current_speed = INITIAL_SPEED
var ball_direction = Vector2.ZERO
var is_serving = false

# Signals
signal goal_scored(scorer)

# Game object references
@onready var collision_shape = $CollisionShape2D

func _ready():
	# Set up ball collision shape
	var shape = RectangleShape2D.new()
	shape.size = Vector2(BALL_SIZE, BALL_SIZE)
	collision_shape.shape = shape

	# Initial serve
	_serve()

func _physics_process(delta):
	if is_serving:
		return

	# Move ball manually
	position += ball_direction * current_speed * delta

	# Check boundaries
	_check_boundaries()

	# Check paddle collisions
	_check_paddle_collisions()

func _serve():
	is_serving = true
	current_speed = INITIAL_SPEED
	position = Vector2(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2)

	# Serve after 1 second
	await get_tree().create_timer(1.0).timeout

	# Serve in random direction
	var angle = randf_range(-PI/6, PI/6)  # -30 to 30 degrees
	if randf() > 0.5:
		angle += PI  # 50% chance to go downward

	ball_direction = Vector2(cos(angle), sin(angle))
	is_serving = false

	print("Ball served! Direction: ", ball_direction)

func _check_boundaries():
	# Reflect off left/right walls
	if position.x <= BALL_SIZE / 2:
		position.x = BALL_SIZE / 2
		ball_direction.x = abs(ball_direction.x)  # Go right
		print("Left wall bounce")
	elif position.x >= SCREEN_WIDTH - BALL_SIZE / 2:
		position.x = SCREEN_WIDTH - BALL_SIZE / 2
		ball_direction.x = -abs(ball_direction.x)  # Go left
		print("Right wall bounce")

	# Goal detection
	if position.y <= -BALL_SIZE:
		print("Player scores! Ball Y: ", position.y)
		goal_scored.emit("player")
		_serve()
	elif position.y >= SCREEN_HEIGHT + BALL_SIZE:
		print("CPU scores! Ball Y: ", position.y)
		goal_scored.emit("cpu")
		_serve()

func _check_paddle_collisions():
	# Player paddle collision
	var player = get_parent().get_node("Player")
	if _is_colliding_with_paddle(player):
		_handle_paddle_hit(player)

	# CPU paddle collision
	var cpu = get_parent().get_node("CPU")
	if _is_colliding_with_paddle(cpu):
		_handle_paddle_hit(cpu)

func _is_colliding_with_paddle(paddle) -> bool:
	var paddle_pos = paddle.position
	var paddle_size = Vector2(80, 10)  # Paddle size

	# Simple rectangle collision detection
	return (position.x + BALL_SIZE/2 > paddle_pos.x - paddle_size.x/2 and
			position.x - BALL_SIZE/2 < paddle_pos.x + paddle_size.x/2 and
			position.y + BALL_SIZE/2 > paddle_pos.y - paddle_size.y/2 and
			position.y - BALL_SIZE/2 < paddle_pos.y + paddle_size.y/2)

func _handle_paddle_hit(paddle):
	# Calculate relative position from paddle center
	var relative_x = (position.x - paddle.position.x) / 40.0  # Normalize
	relative_x = clamp(relative_x, -1.0, 1.0)

	# Calculate reflection angle
	var bounce_angle = relative_x * PI/4  # Max 45 degrees

	# Increase speed
	current_speed = min(current_speed + SPEED_INCREMENT, MAX_SPEED)

	# Set new direction
	if paddle.name == "Player":
		# Player paddle - reflect upward
		ball_direction = Vector2(sin(bounce_angle), -cos(bounce_angle))
		position.y = paddle.position.y - 10  # Move away from paddle
	else:
		# CPU paddle - reflect downward
		ball_direction = Vector2(sin(bounce_angle), cos(bounce_angle))
		position.y = paddle.position.y + 10  # Move away from paddle

	print("Paddle hit! Speed: ", current_speed, " Direction: ", ball_direction)

func reset_position():
	_serve()

Summary

Through this ping pong game development, I was able to experience how Godot's text-based development approach is extremely well-suited for collaboration with LLMs. Since everything from scene files to scripts can be completed in a text editor, code generated by LLMs can be utilized as-is, leading to expected improvements in development efficiency. UI and effects can be expanded using similar methods, making it possible to apply this approach to more sophisticated game development as well.

Share this article

FacebookHatena blogX

Related articles