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 gaining 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. This is a 2D game where the player controls the paddle at the bottom and plays against the CPU.

Game demonstration 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 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.

Create new project

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

Renderer selection

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.

simple-pingpong initial implementation

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.

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

# 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.

Share this article

FacebookHatena blogX