
I tried text-based development with Godot, a game engine well-suited for collaborative development with LLMs
This article was published more than one year ago. Please be aware that the information may be outdated.
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. This is a 2D game where the player controls the paddle at the bottom and plays 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 it employs GDScript, its own Python-like scripting language. 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 has a Python-like syntax, which is a language pattern many LLMs excel at. Development using only a text editor is possible, as in this case, allowing you to build an interactive development environment with LLMs.
Target audience
- Developers interested in game development using LLMs
- Those who want to learn the basics of Godot
References
Implementation
First, I downloaded and launched Godot from the official site and created a new project.

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

After creating the project, a folder is created. 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():
# Set screen size
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):
# Exit 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 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 (apart from 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 a bit slower than player
const PADDLE_WIDTH = 80.0
const SCREEN_WIDTH = 360
const REACTION_ZONE = 360.0 # React when 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 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
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
_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 predicted ball 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 predicted ball 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 center of screen
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 a bit slower
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 with each paddle hit
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
# Signal
signal goal_scored(scorer)
# Game object references
@onready var collision_shape = $CollisionShape2D
func _ready():
# Set 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()
# Handle collision
if get_slide_collision_count() > 0:
_handle_collision()
# Check for goals
_check_goals()
func _serve():
# Initial setup 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 degrees 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 off 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():
# Reset position when game is reset
_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 Fixing
After completing the implementation, I started the game in the Godot editor by pressing the F5 key.

There were the following issues with the operation:
- CPU advances every time it hits the ball
- Ball gets stuck against the wall and doesn't move away
- Game doesn't end even when you lose
- Ball speed is too slow
After iterative dialogue with the LLM and several code revisions, I resolved the above issues.

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
# Score
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 the score update signal to the ball
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 if game ended
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()
# Display debug info
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 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
# Restrict at screen boundaries
_clamp_to_screen()
# Fix Y coordinate
position.y = 650
func _clamp_to_screen():
# Prevent paddle from going offscreen
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 # Respond 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 # Speed coefficient for returning to center
# Game object references
@onready var collision_shape = $CollisionShape2D
var ball_node: Node2D
var target_x = 0.0
func _ready():
# Set paddle collision shape
var shape = RectangleShape2D.new()
shape.size = Vector2(PADDLE_WIDTH, 10)
collision_shape.shape = shape
# Get ball node reference
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 if 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 respond actively when ball is moving towards CPU side
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 away
_move_to_center(delta)
# Execute actual movement
_execute_movement(delta)
# Restrict at screen boundaries
_clamp_to_screen()
# Fix Y coordinate (prevent advancing)
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 prediction position
var predicted_x = ball_pos.x + ball_dir.x * ball_speed * time_to_cpu * PREDICTION_FACTOR
# Restrict within 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 center of screen as target
target_x = SCREEN_WIDTH / 2
func _execute_movement(delta: float):
# Calculate difference between current position and target
var distance = target_x - position.x
# Don't move if within dead zone
if abs(distance) < DEAD_ZONE:
return
# Determine movement direction and speed
var direction = sign(distance)
var move_speed = SPEED
# Slower when returning to center
if target_x == SCREEN_WIDTH / 2:
move_speed *= CENTER_RETURN_SPEED
# Update position directly (not using physics engine)
position.x += direction * move_speed * delta
func _clamp_to_screen():
# Prevent paddle from going offscreen
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
# Signal
signal goal_scored(scorer)
# Game object references
@onready var collision_shape = $CollisionShape2D
func _ready():
# Set 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
# Manually move ball
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 for downward direction
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) # Rightward
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) # Leftward
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 rectangular 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 # Normalized
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 the development of this ping-pong game, I've experienced how Godot's text-based development approach is exceptionally well-suited for collaboration with LLMs. Being able to complete everything from scene files to scripts entirely in a text editor allows direct utilization of LLM-generated code, potentially improving development efficiency. The same approach can be applied to expand UI elements and effects, making it possible to develop more sophisticated games using similar methods.
