
I tried text-based development with Godot, a game engine well-suited for collaborative development with LLMs
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.

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.

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

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.

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.

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.

