GD Script Basics
Learn the fundamentals of Godot's built-in scripting language
What is GD Script?
Role: GD Script is Godot's primary scripting language designed specifically for game development.
Why use it: It's tightly integrated with Godot's engine, has Python-like readable syntax, and is optimized for game development patterns.
When to use: For all Godot projects - from small prototypes to full commercial games.
# Basic script structure
extends Node
func _ready():
print("Script is ready!")
Quick Start
Create your first script by right-clicking any node â "Attach Script". Godot will automatically set up the basic structure for you.
Coding with AI
Leverage AI tools to accelerate your GD Script learning and development
AI-Assisted Development
Role: Use AI as a coding assistant to generate, explain, and debug code.
Why use it: Speeds up development, helps learn new concepts, and provides alternative solutions.
When to use: When stuck on a problem, learning new patterns, or generating boilerplate code.
Effective AI Prompt Example
"Write a GD Script for a player character that moves with WASD, jumps with spacebar, and has double jump capability. Include comments explaining each part."
Important Note
Always test AI-generated code thoroughly. AI can make mistakes or provide outdated information. Use it as a learning aid, not a replacement for understanding fundamentals.
Structure & Formatting
Organize your code for readability and maintainability
Proper Script Structure
Role: Organizes code logically for better readability and maintenance.
Why use: Makes code easier to understand, debug, and collaborate on.
When to use: Always! Good structure should be habitual from your first script.
# Recommended structure (top to bottom):
# 1. Extends declaration
extends CharacterBody2D
# 2. Class name (optional)
class_name Player
# 3. Signals
signal health_changed
signal died
# 4. Constants
const SPEED: float = 300.0
const JUMP_FORCE: float = -400.0
# 5. Variables
var health: int = 100
var is_alive: bool = true
# 6. Built-in functions
func _ready():
initialize_player()
func _process(delta: float):
update_movement(delta)
# 7. Custom functions (public first)
func take_damage(amount: int):
health -= amount
# 8. Private functions (start with _)
func _initialize_player():
print("Player initialized")
Conditions & Logic
Control the flow of your game with conditional statements
If-Elif-Else Statements
Role: Make decisions in your code based on conditions.
Why use: Essential for game logic - checking player state, game rules, AI decisions.
When to use: Whenever you need to check a condition and execute different code based on the result.
func check_player_state():
if health <= 0:
die()
elif health < 20:
show_low_health_warning()
play_hurt_sound()
elif health < 50:
play_hurt_sound()
else:
# Health is above 50 - player is fine
pass
Match Statements (Switch Alternative)
Role: Clean alternative to multiple if-elif statements when checking a single value.
Why use: More readable than long if-elif chains, especially for states or enums.
When to use: When checking a single variable against multiple possible values.
enum {STATE_IDLE, STATE_WALK, STATE_RUN, STATE_JUMP}
var player_state = STATE_IDLE
func handle_state():
match player_state:
STATE_IDLE:
play_idle_animation()
velocity = Vector2.ZERO
STATE_WALK:
play_walk_animation()
velocity = move_direction * walk_speed
STATE_RUN:
play_run_animation()
velocity = move_direction * run_speed
STATE_JUMP:
play_jump_animation()
velocity.y += gravity
_: # Default case (optional)
print("Unknown state!")
Debugging Techniques
Find and fix errors in your GD Script code
Print Debugging
Role: Simple way to track variable values and execution flow.
Why use: Quickest way to understand what's happening in your code.
When to use: When something isn't working and you need to see values at runtime.
func calculate_damage(base_damage: int, multiplier: float):
print("DEBUG: calculate_damage called")
print("DEBUG: base_damage = ", base_damage)
print("DEBUG: multiplier = ", multiplier)
var final_damage = base_damage * multiplier
print("DEBUG: final_damage = ", final_damage)
return final_damage
Breakpoints in Godot Editor
Role: Pause execution to inspect variables at specific points.
Why use: More powerful than print statements - can inspect complex objects.
When to use: For complex bugs where you need to examine multiple variables at once.
How to use breakpoints:
- Click in the gutter next to line numbers to set a red breakpoint dot
- Run your scene in Debug mode (F5 or Debug â Start Debugging)
- When execution hits the breakpoint, it pauses
- Use the Debugger panel to inspect variables
- Use Step Over (F10), Step Into (F11) to continue execution line by line
Extends Keyword
Understanding inheritance and node types
What Does "Extends" Do?
Role: Defines what type of node your script controls and establishes inheritance.
Why use: Gives your script access to all the properties and methods of the parent class.
When to use: Always! Every GD Script must extend something (usually a Node type).
# Common extends examples:
extends Node # Basic node, no special features
extends Node2D # 2D node with position, rotation, scale
extends Sprite2D # 2D sprite with texture
extends CharacterBody2D # 2D character with physics movement
extends CharacterBody3D # 3D character with physics
extends Control # UI element
extends AnimationPlayer # Animation controller
# With class_name for reusable components:
extends Node
class_name HealthComponent # Now can be reused anywhere
Best Practice
Always use the most specific node type for your needs. If you're making a player character, use extends CharacterBody2D or extends CharacterBody3D instead of just extends Node to get built-in movement physics.
Variables & Types
Store and manipulate data in your game
Basic Data Types
Role: Define what kind of data a variable can hold.
Why use: Type safety prevents errors and improves performance.
When to use: Always specify types for important variables (optional for simple scripts).
| Type | Description | Example | When to Use |
|---|---|---|---|
int |
Whole numbers | 42, -5, 0 |
Counters, scores, health |
float |
Decimal numbers | 3.14, -0.5, 1.0 |
Positions, timers, percentages |
bool |
True/False values | true, false |
Flags, states, conditions |
String |
Text | "Hello", "Player1" |
Names, messages, IDs |
Vector2 |
2D coordinates | Vector2(10, 20) |
2D positions, velocities |
Vector3 |
3D coordinates | Vector3(1, 2, 3) |
3D positions, rotations |
Advanced Data Structures
# Array - ordered list
var items: Array = ["sword", "shield", "potion"]
items.append("bow") # Add item
var first_item = items[0] # Get first item
# Dictionary - key-value pairs
var player_stats: Dictionary = {
"health": 100,
"mana": 50,
"level": 5
}
var player_health = player_stats["health"] # Access value
player_stats["exp"] = 1200 # Add new key-value
# Enum - named constants
enum ItemType {WEAPON, ARMOR, POTION, KEY}
var current_item = ItemType.WEAPON
# Typed array (Godot 4+)
var int_array: Array[int] = [1, 2, 3]
var string_array: Array[String] = ["a", "b"]
Declaring Variables
Different ways to create and initialize variables
Variable Declaration Methods
Role: Create variables with different scopes and initialization times.
Why use different methods: Each has specific use cases for optimization and organization.
| Declaration | Description | When to Use | Example |
|---|---|---|---|
var |
Regular variable | Most common case | var score = 0 |
onready var |
Initialize after node is ready | When you need to reference other nodes | onready var sprite = $Sprite2D |
export var |
Expose to editor | Make variables editable in Inspector | export var speed = 300.0 |
const |
Constant value | Values that never change | const GRAVITY = 980.0 |
@export (Godot 4) |
Type-safe export | Modern export with type hints | @export var health: int = 100 |
Practical Examples
# Basic variable declaration
var player_name: String = "Hero"
var score: int = 0
var game_over: bool = false
# onready - get references after node is ready
onready var animation_player = $AnimationPlayer
onready var sprite = get_node("Sprite2D")
# export - make editable in Inspector
export var move_speed: float = 300.0
export var jump_height: float = 500.0
export(String) var character_name = "Player"
# constants for values that never change
const MAX_HEALTH: int = 100
const PI: float = 3.14159
const TILE_SIZE: int = 64
# Godot 4 style exports with annotations
# @export var health: int = 100
# @export_range(0, 100) var mana: int = 50
# @export_file("*.png") var texture_path: String
Variable Naming Convention
Use snake_case for variables and functions: player_health, calculate_damage(). Use PascalCase for class names: PlayerController. Constants should be UPPER_SNAKE_CASE: MAX_SPEED.
Functions Basics
Create reusable blocks of code
What are Functions?
Role: Encapsulate code that performs a specific task.
Why use: Reuse code, improve readability, organize logic.
When to use: Any time you have code that does a specific job that might be reused.
# Basic function without parameters
func say_hello():
print("Hello, World!")
# Function with parameters
func greet_player(player_name: String):
print("Hello, " + player_name + "!")
# Function with return value
func add_numbers(a: int, b: int) -> int:
return a + b
# Calling functions
say_hello() # Prints: Hello, World!
greet_player("Alex") # Prints: Hello, Alex!
var result = add_numbers(5, 3) # result = 8
Function Parameters Deep Dive
# Default parameter values
func attack(damage: int = 10, critical: bool = false):
var final_damage = damage
if critical:
final_damage *= 2
apply_damage(final_damage)
# Can call with different numbers of arguments
attack() # Uses defaults: damage=10, critical=false
attack(20) # damage=20, critical=false
attack(15, true) # damage=15, critical=true
# Variable number of arguments
func sum_numbers(...numbers):
var total = 0
for num in numbers:
total += num
return total
var total = sum_numbers(1, 2, 3, 4, 5) # total = 15
Inbuilt Functions
Essential Godot engine functions you need to know
Core Built-in Functions
Role: Functions automatically called by Godot engine at specific times.
Why use: They form the backbone of your game's update loop and event handling.
When to use: Godot calls them automatically - you override them to add your logic.
| Function | Called When | Use For | Example |
|---|---|---|---|
_ready() |
Node enters scene tree | Initialization, getting node references | Setup variables, connect signals |
_process(delta) |
Every frame (60 FPS target) | Non-physics updates | Animations, UI updates, input |
_physics_process(delta) |
Fixed time step (60 FPS) | Physics calculations | Movement, collisions, forces |
_input(event) |
Any input event occurs | Handle all input | Keyboard, mouse, gamepad |
_unhandled_input(event) |
Input not handled by GUI | Game-specific input | Character movement, actions |
Practical Implementation
extends CharacterBody2D
var speed: float = 300.0
var jump_force: float = -400.0
var gravity: float = 980.0
# Called when node enters the scene tree
func _ready():
print("Player controller initialized")
# Setup initial state
position = Vector2(100, 100)
# Called every physics frame (60 FPS fixed)
func _physics_process(delta: float):
# Apply gravity
if not is_on_floor():
velocity.y += gravity * delta
# Handle movement
var input_direction = Input.get_axis("move_left", "move_right")
velocity.x = input_direction * speed
# Handle jumping
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_force
# Move the character
move_and_slide()
# Called every frame (variable FPS)
func _process(delta: float):
# Update animations based on velocity
update_animations()
# Handle input events
func _input(event: InputEvent):
if event is InputEventKey:
if event.pressed and event.keycode == KEY_ESCAPE:
show_pause_menu()
move_and_slide() Function
Role: The most important function for 2D/3D character movement. It moves the character while handling collisions with walls, floors, and slopes automatically.
Key features: Automatic collision resolution, slope handling, snap to floor, platform detection.
Always use in _physics_process() for smooth, frame-rate independent movement.
Custom Functions
Create your own reusable code blocks
Why Create Custom Functions?
Role: Organize your code into logical, reusable units.
Benefits:
- Reusability: Write once, use many times
- Readability: Descriptive names explain what code does
- Maintainability: Fix bugs in one place
- Testing: Easier to test isolated functions
# Example: Health system functions
# Public function - can be called from other scripts
func take_damage(amount: int):
if is_invincible:
return
health -= amount
_update_health_display()
_check_death()
if health > 0:
_start_invincibility()
# Private function - starts with underscore, internal use only
func _update_health_display():
health_bar.value = health
health_label.text = "Health: " + str(health)
# Private function with return value
func _check_death() -> bool:
if health <= 0:
die()
return true
return false
# Function with multiple parameters and default values
func heal(amount: int, play_sound: bool = true, show_effect: bool = true):
health = min(health + amount, max_health)
_update_health_display()
if play_sound:
play_heal_sound()
if show_effect:
spawn_heal_effect()
Function Design Principles
Single Responsibility Principle
Each function should do one thing well:
# BAD: Does too much
func handle_player_input_and_move_and_animate():
# ... messy code that does everything ...
# GOOD: Separate responsibilities
func handle_input():
# Get input direction
func apply_movement():
# Move character based on input
func update_animation():
# Update animations based on state
Descriptive Names
Function names should clearly describe what they do:
# BAD: Vague name
func do_stuff():
# What stuff?
# GOOD: Clear, descriptive name
func calculate_player_experience_for_level(level: int) -> int:
return level * level * 100
Practice Exercise
Create a function called calculate_damage that takes:
- Base damage (int)
- Attack multiplier (float, default 1.0)
- Is critical hit (bool, default false)
The function should return final damage. Critical hits should double the damage. Test it with different inputs.
Indentation Rules
Proper code formatting for readability
Why Indentation Matters
Role: Defines code blocks and structure (like braces in other languages).
Why it's critical: GD Script uses indentation to determine what code belongs to which block. Wrong indentation = syntax errors.
Rule: Always use 4 spaces per indentation level (not tabs).
# CORRECT: 4 spaces per level
func example_function():
# Level 1: 4 spaces
if condition:
# Level 2: 8 spaces
do_something()
for i in range(10):
# Level 3: 12 spaces
print(i)
else:
# Back to level 2: 8 spaces
do_something_else()
# WRONG: Mixed indentation (will cause errors)
func bad_function():
if condition: # Missing indentation!
do_something()
another_thing() # Wrong: 7 spaces
yet_another() # Back to 4 - inconsistent!
Common Indentation Patterns
# Function definition
func my_function():
# Function body indented
print("Hello")
# If statements
if condition:
# True branch
do_this()
else:
# False branch
do_that()
# Loops
for item in items:
# Loop body
process_item(item)
# Nested structures
for enemy in enemies:
if enemy.is_alive:
if enemy.can_see_player:
enemy.attack()
else:
enemy.patrol()
else:
enemy.remove()
Godot Editor Help
The Godot editor automatically handles indentation for you. When you press Enter after a colon, it automatically adds 4 spaces. Use Tab to indent and Shift+Tab to unindent selected lines.
Critical Error: IndentationError
If you see "IndentationError: unexpected indent" or similar, it means your indentation is inconsistent. Check for:
- Mixing tabs and spaces (use only spaces)
- Wrong number of spaces (always use multiples of 4)
- Missing indentation after colon
Signals & Methods
Godot's event system for communication between nodes
What are Signals?
Role: Godot's version of events - allow nodes to communicate without tight coupling.
Why use: Decouple code, make reusable components, follow Godot's scene system philosophy.
Analogy: Like a radio broadcast - sender emits signal, receivers tune in if interested.
# Define signals at top of script
extends Node
signal health_changed(new_health)
signal player_died
signal item_collected(item_name, amount)
# Emit signals when events happen
func take_damage(amount: int):
health -= amount
emit_signal("health_changed", health)
if health <= 0:
emit_signal("player_died")
# Connect to signals in another script
func _ready():
# Connect using code
player_node.connect("health_changed", _on_player_health_changed)
player_node.connect("player_died", _on_player_died)
# Handler functions (called when signal is emitted)
func _on_player_health_changed(new_health: int):
health_bar.value = new_health
func _on_player_died():
show_game_over_screen()
Connecting Signals in Editor
Easier way: Use Godot Editor's visual signal connection tool.
Steps to connect signals visually:
- Select the node that emits the signal
- Go to Node tab (right side of editor)
- Double-click the signal you want to connect
- Select target node and method
- Godot automatically creates the connection and handler function stub
# Godot automatically creates this when connecting in editor:
func _on_Button_pressed():
print("Button was pressed!")
# Signal with parameters - Godot includes them automatically
func _on_Player_health_changed(new_health):
print("Player health is now: ", new_health)
Methods vs Signals
| Aspect | Methods (Direct Calls) | Signals |
|---|---|---|
| Coupling | Tight coupling (caller knows receiver) | Loose coupling (sender doesn't know receivers) |
| When to Use | Direct control, immediate action needed | Events, notifications, multiple listeners |
| Example | player.take_damage(10) |
player.emit_signal("damaged") |
| Multiple Listeners | Only one (the called method) | Many (all connected nodes) |
| Return Value | Can return values | No return values |
Advanced Signal Patterns
# Signal with typed parameters (Godot 4)
signal damage_taken(amount: int, from: Node)
# One-time connections (disconnect after first emit)
player.connect("player_died", self, "_on_player_died", [], CONNECT_ONESHOT)
# Connecting with binds (pass extra parameters)
enemy.connect("died", self, "_on_enemy_died", [enemy.type, enemy.reward_exp])
# Disconnecting signals
func cleanup():
if player.is_connected("health_changed", self, "_on_health_changed"):
player.disconnect("health_changed", self, "_on_health_changed")
# Yield with signals (wait for signal)
func wait_for_player_death():
print("Waiting for player to die...")
yield(player, "player_died") # Pauses here until signal emitted
print("Player died! Game over.")
Practice Exercise
Create a simple health system using signals:
- Create a Health component with signals:
health_changed,health_depleted - Create a UI that connects to these signals and updates a health bar
- Create an enemy that damages the player and triggers the signals
- Use both code connections and editor connections
Scene Changing
Load, switch, and manage different game scenes
What are Scenes in Godot?
Role: Scenes are reusable, hierarchical collections of nodes (like prefabs in other engines).
Why use scenes: Modular design, reuse components, organize game structure.
Key concept: Everything in Godot is a scene - characters, UI, levels, even the entire game.
# Basic scene loading and changing
# Load a scene from file (but don't instantiate yet)
var main_menu_scene = preload("res://scenes/main_menu.tscn")
var game_level_scene = load("res://scenes/level_1.tscn")
# Instantiate a scene (create instance from packed scene)
var level_instance = game_level_scene.instantiate()
# Change to a new scene (replace current scene)
func go_to_main_menu():
get_tree().change_scene_to_packed(main_menu_scene)
func start_game():
get_tree().change_scene_to_file("res://scenes/game_level.tscn")
# Add scene as child (not replace current scene)
func spawn_enemy():
var enemy_scene = preload("res://scenes/enemy.tscn")
var enemy = enemy_scene.instantiate()
add_child(enemy)
enemy.position = Vector2(100, 100)
Scene Management Patterns
Pattern 1: Scene Transition with Effects
func change_scene_with_fade(scene_path: String):
# Create fade animation
var fade = create_fade_animation()
add_child(fade)
# Wait for fade out
yield(fade, "fade_finished")
# Change scene
get_tree().change_scene_to_file(scene_path)
# Fade in
fade.fade_in()
yield(fade, "fade_finished")
fade.queue_free()
Pattern 2: Scene Stack for Menus
var scene_stack = []
func push_scene(scene_path: String):
# Save current scene
scene_stack.append(get_tree().current_scene)
# Load new scene
var new_scene = load(scene_path).instantiate()
get_tree().root.add_child(new_scene)
get_tree().current_scene = new_scene
func pop_scene():
if scene_stack.size() > 0:
# Remove current scene
get_tree().current_scene.queue_free()
# Restore previous scene
var previous_scene = scene_stack.pop_back()
get_tree().current_scene = previous_scene
Scene Instancing & Parameters
# Passing data to instantiated scenes
# Method 1: Set properties after instantiation
func spawn_player(player_name: String, starting_health: int):
var player_scene = preload("res://scenes/player.tscn")
var player = player_scene.instantiate()
# Set properties
player.name = player_name
player.health = starting_health
add_child(player)
return player
# Method 2: Use initialization function
func spawn_enemy_with_type(enemy_type: String, position: Vector2):
var enemy_scene = preload("res://scenes/enemy.tscn")
var enemy = enemy_scene.instantiate()
# Call initialization function
enemy.initialize(enemy_type, position)
add_child(enemy)
# In enemy.gd:
func initialize(type: String, spawn_position: Vector2):
enemy_type = type
position = spawn_position
match type:
"melee":
speed = 100
damage = 20
"ranged":
speed = 80
damage = 15
Scene Best Practices
- Use descriptive scene names:
player_character.tscnnotscene1.tscn - Organize in folders:
scenes/characters/,scenes/ui/,scenes/levels/ - Keep scenes focused: One main responsibility per scene
- Use instancing for duplicates: Don't copy-paste nodes, instance scenes
- Preload frequently used scenes: Better performance than loading each time
Practice Exercise
Create a simple scene manager:
- Create 3 scenes: MainMenu, GameLevel, GameOver
- Make buttons in MainMenu to start game and quit
- In GameLevel, add a way to trigger GameOver scene
- In GameOver, add button to return to MainMenu
- Add fade transitions between scenes
Timer Node
Create delays, cooldowns, and timed events
What is the Timer Node?
Role: A built-in Godot node that emits a signal after a set time interval.
Why use: Simple way to create delays, repeating actions, cooldowns, timeouts.
When to use: Any time you need to wait before doing something, or do something repeatedly at intervals.
# Adding Timer node in code
extends Node2D
var timer: Timer
func _ready():
# Create and configure timer
timer = Timer.new()
timer.wait_time = 1.0 # 1 second
timer.one_shot = false # Repeat forever
timer.autostart = false # Don't start automatically
# Connect timeout signal
timer.connect("timeout", _on_timer_timeout)
# Add as child
add_child(timer)
# Start the timer
timer.start()
func _on_timer_timeout():
print("Timer timeout! 1 second passed.")
# Do something every second
Common Timer Use Cases
1. Cooldown System
extends Area2D
var can_damage = true
var damage_cooldown_timer: Timer
func _ready():
damage_cooldown_timer = Timer.new()
damage_cooldown_timer.wait_time = 0.5 # 0.5 second cooldown
damage_cooldown_timer.one_shot = true
damage_cooldown_timer.connect("timeout", self, "_on_damage_cooldown_timeout")
add_child(damage_cooldown_timer)
func apply_damage(target):
if can_damage:
target.take_damage(10)
can_damage = false
damage_cooldown_timer.start()
func _on_damage_cooldown_timeout():
can_damage = true
2. Spawn Enemies at Intervals
extends Node2D
onready var spawn_timer = $SpawnTimer
var enemy_scene = preload("res://enemy.tscn")
func _ready():
spawn_timer.wait_time = 2.0 # Spawn every 2 seconds
spawn_timer.start()
func _on_SpawnTimer_timeout():
var enemy = enemy_scene.instantiate()
enemy.position = Vector2(
randf_range(50, 750),
-50
)
add_child(enemy)
3. Self-Destruct Timer
extends Node2D
func _ready():
# Create one-shot timer for self-destruction
var destroy_timer = Timer.new()
destroy_timer.wait_time = 3.0 # Destroy after 3 seconds
destroy_timer.one_shot = true
destroy_timer.connect("timeout", self, "_on_destroy_timeout")
add_child(destroy_timer)
destroy_timer.start()
func _on_destroy_timeout():
queue_free() # Remove this node from scene
Timer Properties and Methods
| Property/Method | Description | Example |
|---|---|---|
wait_time |
Time in seconds between timeouts | timer.wait_time = 2.5 |
one_shot |
If true, timer stops after first timeout | timer.one_shot = true |
autostart |
Start automatically when added to scene | timer.autostart = true |
start() |
Start the timer | timer.start() |
stop() |
Stop the timer | timer.stop() |
paused |
Pause/unpause timer | timer.paused = true |
time_left |
Seconds until next timeout | var remaining = timer.time_left |
Alternative to Timer: Delta Time
When NOT to use Timer: For very simple delays in _process or _physics_process, you can use delta time accumulation.
extends Node2D
var time_since_last_shot: float = 0.0
const SHOT_COOLDOWN: float = 0.3 # 0.3 seconds between shots
func _process(delta: float):
# Accumulate time
time_since_last_shot += delta
# Check if cooldown has passed
if Input.is_action_pressed("shoot") and time_since_last_shot >= SHOT_COOLDOWN:
shoot()
time_since_last_shot = 0.0 # Reset cooldown
func shoot():
# Create bullet, play sound, etc.
print("Pew!")
Timer vs Delta Time: Which to Use?
Use Timer when:
- You need precise timing intervals
- Multiple things need to happen at the same interval
- You want to easily pause/resume timing
- You need one-shot delays
Use delta time accumulation when:
- Simple cooldowns in update loops
- You're already in _process/_physics_process
- You want to avoid adding more nodes
- Performance is critical (Timers have small overhead)
Practice Exercise
Create a power-up system with timers:
- Create a speed boost power-up that lasts 5 seconds
- Create a shield power-up that lasts 10 seconds
- Show countdown timers on screen for active power-ups
- Make power-ups not stack (reset timer if collected again)
- Add visual effects when power-up expires
Multiplayer Basics
Introduction to networking in Godot 4
Godot Multiplayer Architecture
Role: Create games where multiple players can play together over network.
Why use: Multiplayer games are popular and can be monetized.
Key concept: Godot uses a client-server model (can also do peer-to-peer).
# Basic multiplayer setup
extends Node
var peer = ENetMultiplayerPeer.new()
# Start as server (host)
func host_game(port: int = 9999):
var error = peer.create_server(port)
if error != OK:
print("Failed to create server: ", error)
return
multiplayer.multiplayer_peer = peer
print("Server started on port ", port)
# Connect as client
func join_game(ip: String = "127.0.0.1", port: int = 9999):
var error = peer.create_client(ip, port)
if error != OK:
print("Failed to connect to server: ", error)
return
multiplayer.multiplayer_peer = peer
print("Connected to server ", ip, ":", port)
# Disconnect
func disconnect_from_game():
if multiplayer.multiplayer_peer:
multiplayer.multiplayer_peer.close()
multiplayer.multiplayer_peer = null
print("Disconnected")
Remote Procedure Calls (RPCs)
Role: Call functions on other computers over network.
Annotations: Use @rpc before functions to make them callable remotely.
extends CharacterBody2D
# Called on server when player wants to move
func _process(delta: float):
if Input.is_action_pressed("move_right"):
request_move(Vector2.RIGHT)
# Client asks server to move
func request_move(direction: Vector2):
move_rpc.rpc_id(1, direction) # Send to server (ID 1)
# Server moves player and tells everyone
@rpc("any_peer", "call_local", "reliable")
func move_rpc(direction: Vector2):
var sender_id = multiplayer.get_remote_sender_id()
# Only server should process movement
if multiplayer.is_server():
# Move the player
position += direction * 100 * get_process_delta_time()
# Tell all clients about new position
update_position_rpc.rpc(sender_id, position)
# Update position on all clients
@rpc("authority", "call_local", "reliable")
func update_position_rpc(player_id: int, new_position: Vector2):
# Find player node and update position
var player = get_node("Players/" + str(player_id))
if player:
player.position = new_position
Player Synchronization
# Spawn players when they join
func _ready():
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
func _on_peer_connected(id: int):
print("Peer connected: ", id)
spawn_player.rpc_id(id, id) # Tell new player to spawn themselves
# Tell existing players about new player
for peer_id in multiplayer.get_peers():
if peer_id != id:
spawn_player.rpc_id(peer_id, id)
func _on_peer_disconnected(id: int):
print("Peer disconnected: ", id)
despawn_player.rpc(id) # Remove player from all clients
@rpc("any_peer", "call_local")
func spawn_player(player_id: int):
var player_scene = preload("res://player.tscn")
var player = player_scene.instantiate()
player.name = str(player_id)
player.set_multiplayer_authority(player_id) # This player controls this node
$Players.add_child(player)
@rpc("any_peer", "call_local")
func despawn_player(player_id: int):
var player = $Players.get_node_or_null(str(player_id))
if player:
player.queue_free()
Multiplayer Security Warning
Never trust the client! Always validate input on the server. Clients can be hacked to send any data. The server should be the ultimate authority for game state, scores, and important decisions.
Singletons (Autoloads)
Global scripts accessible from anywhere
What are Singletons/Autoloads?
Role: Scripts that are automatically loaded when game starts and persist across scene changes.
Why use: Global game state, managers, utilities, shared data between scenes.
When to use: For systems that need to exist throughout the entire game session.
Setting up Autoload in Godot Editor:
- Go to Project â Project Settings
- Select Autoload tab
- Click folder icon to select script
- Give it a name (e.g., "GameState")
- Click Add â Godot loads it automatically
# GameState.gd - A common singleton example
extends Node
# Global variables accessible from anywhere
var current_score: int = 0
var high_score: int = 0
var player_health: int = 100
var current_level: int = 1
var player_name: String = "Player"
# Signals that can be accessed globally
signal score_updated(new_score)
signal health_changed(new_health)
signal level_completed(level_number)
# Called when singleton loads
func _ready():
print("GameState singleton loaded")
load_save_data() # Load saved game data
# Public functions accessible from any scene
func add_score(points: int):
current_score += points
if current_score > high_score:
high_score = current_score
emit_signal("score_updated", current_score)
func take_damage(amount: int):
player_health -= amount
player_health = max(player_health, 0)
emit_signal("health_changed", player_health)
func complete_level():
current_level += 1
emit_signal("level_completed", current_level)
save_game()
# Save/load system
func save_game():
var save_data = {
"high_score": high_score,
"current_level": current_level,
"player_name": player_name
}
# Save to file (simplified)
print("Game saved: ", save_data)
func load_save_data():
# Load from file (simplified)
print("Loading saved data...")
Accessing Singletons from Code
# Accessing singleton from any script:
# Method 1: Direct access by name (if singleton named "GameState")
GameState.add_score(100)
var current_health = GameState.player_health
# Method 2: Through get_node (more explicit)
var game_state = get_node("/root/GameState")
game_state.take_damage(10)
# Method 3: Connect to singleton signals
func _ready():
GameState.connect("score_updated", _on_score_updated)
GameState.connect("health_changed", _on_health_changed)
func _on_score_updated(new_score: int):
score_label.text = "Score: " + str(new_score)
func _on_health_changed(new_health: int):
health_bar.value = new_health
if new_health <= 0:
game_over()
Common Singleton Patterns
1. Audio Manager
extends Node
class_name AudioManager
var music_player: AudioStreamPlayer
var sound_players: Array = []
func _ready():
# Setup audio system
music_player = AudioStreamPlayer.new()
add_child(music_player)
# Create pool of sound players
for i in range(10):
var player = AudioStreamPlayer.new()
add_child(player)
sound_players.append(player)
func play_music(music_stream: AudioStream, volume: float = 1.0):
music_player.stream = music_stream
music_player.volume_db = linear_to_db(volume)
music_player.play()
func play_sound(sound_stream: AudioStream, volume: float = 1.0):
# Find available sound player
for player in sound_players:
if not player.playing:
player.stream = sound_stream
player.volume_db = linear_to_db(volume)
player.play()
break
2. Scene Manager
extends Node
class_name SceneManager
var current_scene: Node
var loading_screen_scene = preload("res://ui/loading_screen.tscn")
func change_scene(scene_path: String):
# Show loading screen
var loading_screen = loading_screen_scene.instantiate()
get_tree().root.add_child(loading_screen)
# Load new scene in background
call_deferred("_load_scene_in_background", scene_path, loading_screen)
func _load_scene_in_background(scene_path: String, loading_screen: Node):
# Load the scene
var new_scene = load(scene_path).instantiate()
# Remove old scene
if current_scene:
current_scene.queue_free()
# Add new scene
get_tree().root.add_child(new_scene)
current_scene = new_scene
# Remove loading screen
loading_screen.queue_free()
Singleton Anti-Patterns
Avoid these singleton mistakes:
- God Object: Don't put everything in one singleton
- Tight Coupling: Don't make every script depend on singletons
- Scene Dependencies: Don't assume scene tree structure in singletons
- Overuse: Use signals and scene parameters when possible instead of singletons
When to Use Singletons
Good uses for singletons:
- Game state management (score, health, level)
- Audio management
- Save/load system
- Input management
- Global utility functions
- Asset management/loading
AI Coding Advanced
Using AI for complex game development tasks
Advanced AI-Assisted Development
Role: Leverage AI for complex game systems, optimization, and problem-solving.
Why use: Speed up development, generate complex algorithms, learn advanced patterns.
Best practices: Use AI as a collaborator, not a replacement for understanding.
AI Prompt for Complex Systems
"Create a complete inventory system in GD Script with:
- Item class with name, type, stack size, properties
- Inventory class with slots, weight limits, sorting
- Equipment system for wearing items
- Save/load functionality using JSON
- Drag-and-drop UI for inventory management"
# Example of AI-generated complex system
# AI can help generate base classes like this:
class_name InventoryItem
extends Resource
@export var item_name: String
@export var item_type: String # "weapon", "armor", "consumable"
@export var max_stack: int = 1
@export var icon: Texture2D
@export var weight: float = 0.0
@export var value: int = 0
func use(user: Node):
match item_type:
"consumable":
print("Using ", item_name)
# Apply effects to user
"weapon":
print("Equipping ", item_name)
_:
print("Item cannot be used directly")
AI for Optimization and Debugging
Optimization Prompt
"Optimize this GD Script function for performance. It's called every frame and needs to be faster:"
# Before optimization (AI might suggest this)
func find_nearest_enemy() -> Node2D:
var enemies = get_tree().get_nodes_in_group("enemies")
var nearest_enemy = null
var nearest_distance = INF
for enemy in enemies:
var distance = position.distance_to(enemy.position)
if distance < nearest_distance:
nearest_distance = distance
nearest_enemy = enemy
return nearest_enemy
# After AI optimization
var enemy_cache: Array = []
var cache_timer: float = 0.0
const CACHE_UPDATE_TIME: float = 0.5 # Update cache every 0.5 seconds
func find_nearest_enemy_optimized() -> Node2D:
# Cache enemies to avoid calling get_nodes_in_group every frame
if enemy_cache.is_empty() or cache_timer <= 0.0:
enemy_cache = get_tree().get_nodes_in_group("enemies")
cache_timer = CACHE_UPDATE_TIME
var nearest_enemy = null
var nearest_distance = INF
for enemy in enemy_cache:
var distance = position.distance_squared_to(enemy.position)
if distance < nearest_distance:
nearest_distance = distance
nearest_enemy = enemy
cache_timer -= get_process_delta_time()
return nearest_enemy
AI for Learning Advanced Concepts
Learning State Machines from AI
"Explain and provide a complete finite state machine implementation in GD Script for an enemy AI with these states: Idle, Patrol, Chase, Attack, Flee."
# AI-generated state machine example
class_name EnemyStateMachine
extends Node
enum State {IDLE, PATROL, CHASE, ATTACK, FLEE}
@export var current_state: State = State.IDLE
@export var player: Node2D
func _process(delta: float):
match current_state:
State.IDLE:
idle_state(delta)
State.PATROL:
patrol_state(delta)
State.CHASE:
chase_state(delta)
State.ATTACK:
attack_state(delta)
State.FLEE:
flee_state(delta)
func change_state(new_state: State):
# Exit current state
match current_state:
State.IDLE:
exit_idle()
# ... other exit functions
# Enter new state
current_state = new_state
match new_state:
State.IDLE:
enter_idle()
# ... other enter functions
func idle_state(delta: float):
# Check transitions
if can_see_player():
change_state(State.CHASE)
elif should_patrol():
change_state(State.PATROL)
AI Limitations in Game Development
What AI CANNOT do well (yet):
- Game Design: AI doesn't understand fun factor or player experience
- Creative Vision: AI can't replace your unique game ideas
- Performance Testing: AI can't test if code runs smoothly on real devices
- Bug-Free Code: AI-generated code often has subtle bugs
- Godot-Specific Optimization: AI may not know latest Godot best practices
Effective AI Collaboration
- Be specific: Provide context about your game and goals
- Iterate: Ask AI to improve or fix its own code
- Combine: Mix AI-generated code with your own knowledge
- Verify: Always test AI code thoroughly
- Learn: Use AI explanations to understand new concepts
Game Logic Setup
Structuring game rules and systems
What is Game Logic?
Role: The rules and systems that define how your game works.
Components: Win/lose conditions, scoring, progression, difficulty, interactions.
Importance: Good game logic makes games fun, fair, and engaging.
# Basic game logic structure
extends Node
class_name GameManager
# Game state
enum GameState {MENU, PLAYING, PAUSED, GAME_OVER, LEVEL_COMPLETE}
var current_state: GameState = GameState.MENU
# Game variables
var score: int = 0
var lives: int = 3
var current_level: int = 1
var time_remaining: float = 60.0 # 60 second timer
# Signals
signal game_started
signal game_paused
signal game_resumed
signal game_over
signal score_changed(new_score)
signal lives_changed(new_lives)
signal time_changed(new_time)
func _ready():
setup_game()
func setup_game():
# Initialize game state
score = 0
lives = 3
current_level = 1
time_remaining = 60.0
current_state = GameState.PLAYING
emit_signal("game_started")
emit_signal("score_changed", score)
emit_signal("lives_changed", lives)
emit_signal("time_changed", time_remaining)
func _process(delta: float):
if current_state == GameState.PLAYING:
# Update game timer
time_remaining -= delta
if time_remaining <= 0:
time_remaining = 0
game_over("Time's up!")
elif time_remaining <= 10.0:
emit_signal("time_changed", time_remaining) # Update UI more frequently
func add_score(points: int):
if current_state != GameState.PLAYING:
return
score += points
emit_signal("score_changed", score)
# Check for level completion
if score >= get_level_target_score(current_level):
complete_level()
func lose_life():
if current_state != GameState.PLAYING:
return
lives -= 1
emit_signal("lives_changed", lives)
if lives <= 0:
game_over("No lives remaining!")
func complete_level():
current_state = GameState.LEVEL_COMPLETE
print("Level ", current_level, " complete!")
# Load next level after delay
get_tree().create_timer(2.0).timeout.connect(
load_next_level
)
func game_over(reason: String = ""):
current_state = GameState.GAME_OVER
print("Game Over: ", reason)
emit_signal("game_over", reason)
func get_level_target_score(level: int) -> int:
# Define score needed to complete each level
return level * 1000
Game Rules Implementation
Rule 1: Collision Scoring System
extends Area2D
@export var point_value: int = 100
@export var destroy_on_collect: bool = true
func _on_body_entered(body: Node2D):
if body.is_in_group("player"):
# Apply score
GameManager.add_score(point_value)
# Visual/audio feedback
play_collect_effect()
if destroy_on_collect:
queue_free()
func play_collect_effect():
# Play sound, spawn particles, etc.
print("Collected item worth ", point_value, " points!")
Rule 2: Combo/Multiplier System
extends Node
class_name ComboSystem
var current_combo: int = 0
var max_combo: int = 0
var combo_timer: float = 0.0
const COMBO_TIMEOUT: float = 3.0 # 3 seconds between hits to maintain combo
func _process(delta: float):
if current_combo > 0:
combo_timer -= delta
if combo_timer <= 0:
reset_combo()
func add_hit(base_score: int = 100):
current_combo += 1
combo_timer = COMBO_TIMEOUT
# Calculate score with combo multiplier
var multiplier = 1.0 + (current_combo * 0.1) # 10% bonus per hit
var final_score = int(base_score * multiplier)
if current_combo > max_combo:
max_combo = current_combo
update_combo_display()
return final_score
func reset_combo():
if current_combo > 0:
print("Combo lost! Final combo: ", current_combo)
current_combo = 0
combo_timer = 0.0
update_combo_display()
func update_combo_display():
# Update UI to show current combo
print("Combo: x", current_combo)
Difficulty Scaling
extends Node
class_name DifficultyManager
enum Difficulty {EASY, NORMAL, HARD, INSANE}
@export var current_difficulty: Difficulty = Difficulty.NORMAL
# Difficulty modifiers
var difficulty_settings = {
Difficulty.EASY: {
"enemy_health_multiplier": 0.7,
"enemy_damage_multiplier": 0.5,
"spawn_rate_multiplier": 0.8,
"player_health_multiplier": 1.5,
"score_multiplier": 0.8
},
Difficulty.NORMAL: {
"enemy_health_multiplier": 1.0,
"enemy_damage_multiplier": 1.0,
"spawn_rate_multiplier": 1.0,
"player_health_multiplier": 1.0,
"score_multiplier": 1.0
},
Difficulty.HARD: {
"enemy_health_multiplier": 1.5,
"enemy_damage_multiplier": 1.5,
"spawn_rate_multiplier": 1.3,
"player_health_multiplier": 0.8,
"score_multiplier": 1.2
},
Difficulty.INSANE: {
"enemy_health_multiplier": 2.0,
"enemy_damage_multiplier": 2.0,
"spawn_rate_multiplier": 1.5,
"player_health_multiplier": 0.5,
"score_multiplier": 1.5
}
}
func get_enemy_health_multiplier() -> float:
return difficulty_settings[current_difficulty]["enemy_health_multiplier"]
func get_enemy_damage_multiplier() -> float:
return difficulty_settings[current_difficulty]["enemy_damage_multiplier"]
func get_spawn_rate_multiplier() -> float:
return difficulty_settings[current_difficulty]["spawn_rate_multiplier"]
func get_player_health_multiplier() -> float:
return difficulty_settings[current_difficulty]["player_health_multiplier"]
func get_score_multiplier() -> float:
return difficulty_settings[current_difficulty]["score_multiplier"]
func adjust_difficulty_dynamically(player_performance: float):
# Auto-adjust difficulty based on player performance
if player_performance > 0.8: # Player doing too well
current_difficulty = clamp(current_difficulty + 1, 0, Difficulty.INSANE)
elif player_performance < 0.3: # Player struggling
current_difficulty = clamp(current_difficulty - 1, Difficulty.EASY, Difficulty.INSANE)
Game Logic Design Principles
- Clear Rules: Players should understand how the game works
- Fairness: Challenges should be difficult but achievable
- Feedback: Players should know why they succeed or fail
- Progression: Game should get appropriately harder
- Balance: No single strategy should dominate
- Emergence: Simple rules should create complex gameplay
Practice Exercise
Design and implement game logic for a simple platformer:
- Create win condition: Collect 10 coins and reach exit
- Create lose condition: Fall off screen or run out of time (60 seconds)
- Implement scoring: Coins = 100 points, time bonus = remaining seconds à 10
- Add difficulty levels that affect enemy speed and spawn rate
- Create a combo system for collecting coins quickly
Advanced Debugging
Professional debugging techniques for complex issues
Systematic Debugging Approach
Role: Methodically find and fix complex bugs.
Process: Reproduce â Isolate â Identify â Fix â Test.
The 5-Step Debugging Process
- Reproduce: Make the bug happen consistently
- Isolate: Find the smallest code that causes the bug
- Identify: Determine exactly why it happens
- Fix: Apply the smallest possible change
- Test: Verify fix works and doesn't break anything else
# Example: Debugging a movement bug
# BUG REPORT: "Player sometimes gets stuck when moving right"
# Step 1: Add debug prints to understand the issue
func _physics_process(delta: float):
debug_print("=== FRAME START ===")
debug_print("Position before: ", position)
debug_print("Velocity before: ", velocity)
var input_direction = Input.get_axis("move_left", "move_right")
debug_print("Input direction: ", input_direction)
velocity.x = input_direction * speed
debug_print("Velocity after input: ", velocity)
move_and_slide()
debug_print("Position after: ", position)
debug_print("Is on floor: ", is_on_floor())
debug_print("Is on wall: ", is_on_wall())
# Check for collisions
for i in range(get_slide_collision_count()):
var collision = get_slide_collision(i)
debug_print("Collision ", i, ": with ", collision.get_collider().name)
debug_print("Collision normal: ", collision.get_normal())
Debug Visualization Tools
1. Debug Overlay
extends CanvasLayer
class_name DebugOverlay
@onready var debug_label = $DebugLabel
@onready var line_2d = $Line2D
var debug_info: Dictionary = {}
var debug_lines: Array = []
func _ready():
# Make sure debug overlay is always on top
layer = 100
set_physics_process(true)
func _physics_process(delta: float):
# Update debug text
var text = "=== DEBUG OVERLAY ===\n"
text += "FPS: %d\n" % Engine.get_frames_per_second()
for key in debug_info.keys():
text += "%s: %s\n" % [key, str(debug_info[key])]
debug_label.text = text
# Clear previous frame's lines
line_2d.clear_points()
# Draw debug lines
for line_data in debug_lines:
line_2d.add_point(line_data.from)
line_2d.add_point(line_data.to)
debug_lines.clear()
func set_debug_value(key: String, value):
debug_info[key] = value
func draw_line(from: Vector2, to: Vector2, color: Color = Color.WHITE):
debug_lines.append({"from": from, "to": to, "color": color})
line_2d.default_color = color
# Usage from other scripts:
# DebugOverlay.set_debug_value("Player Position", player.position)
# DebugOverlay.draw_line(player.position, enemy.position, Color.RED)
2. Custom Debug Shapes
extends Node2D
class_name DebugDraw
static func draw_circle(position: Vector2, radius: float, color: Color = Color.WHITE):
# Create a temporary node to draw debug shapes
var node = Node2D.new()
node.z_index = 1000 # Draw on top
var circle = CircleShape2D.new()
circle.radius = radius
var shape = CollisionShape2D.new()
shape.shape = circle
shape.position = position
# Use immediate mode for visualization
get_tree().root.add_child(node)
node.add_child(shape)
# Remove after short time
get_tree().create_timer(0.1).timeout.connect(
func(): node.queue_free()
)
# Usage:
# DebugDraw.draw_circle(enemy.position, 50.0, Color.RED)
Performance Debugging
1. Frame Time Profiling
extends Node
class_name PerformanceProfiler
var frame_times: Array[float] = []
const MAX_SAMPLES = 60 # Track last second at 60 FPS
var current_frame_start: float
func _ready():
set_process(true)
func _process(delta: float):
# Measure frame time
var frame_end = Time.get_ticks_usec()
var frame_time = (frame_end - current_frame_start) / 1000.0 # Convert to ms
frame_times.append(frame_time)
if frame_times.size() > MAX_SAMPLES:
frame_times.pop_front()
# Calculate statistics
var avg_frame_time = calculate_average(frame_times)
var max_frame_time = calculate_max(frame_times)
if max_frame_time > 16.67: # More than 60 FPS target (1000ms/60)
warn_performance_issue(avg_frame_time, max_frame_time)
current_frame_start = Time.get_ticks_usec()
func warn_performance_issue(avg: float, max_val: float):
print("PERFORMANCE WARNING: Avg frame time: %.2fms, Max: %.2fms" % [avg, max_val])
print("Target: 16.67ms for 60 FPS")
func calculate_average(array: Array) -> float:
if array.is_empty():
return 0.0
var sum: float = 0.0
for value in array:
sum += value
return sum / array.size()
func calculate_max(array: Array) -> float:
if array.is_empty():
return 0.0
var max_val = array[0]
for value in array:
if value > max_val:
max_val = value
return max_val
2. Object Creation Tracking
extends Node
class_name MemoryProfiler
var object_counts: Dictionary = {}
var last_report_time: float = 0.0
const REPORT_INTERVAL: float = 5.0 # Report every 5 seconds
func track_object_creation(object_type: String):
if not object_counts.has(object_type):
object_counts[object_type] = 0
object_counts[object_type] += 1
var current_time = Time.get_ticks_msec() / 1000.0
if current_time - last_report_time >= REPORT_INTERVAL:
report_memory_usage()
last_report_time = current_time
func report_memory_usage():
print("=== MEMORY USAGE REPORT ===")
var total_objects = 0
for object_type in object_counts.keys():
var count = object_counts[object_type]
total_objects += count
print("%s: %d instances" % [object_type, count])
print("Total objects: ", total_objects)
# Check for potential memory leaks
if total_objects > 1000:
print("WARNING: High object count - possible memory leak!")
# Usage - wrap object creation:
# var bullet = BulletScene.instantiate()
# MemoryProfiler.track_object_creation("Bullet")
# add_child(bullet)
Common Bugs and Solutions
| Bug Type | Symptoms | Common Causes | Solutions |
|---|---|---|---|
| Null Reference | "Invalid get index on null" | Node not loaded, wrong path, timing issue | Use onready, check is_instance_valid(), null checks |
| Physics Jitter | Objects shake or vibrate | Mixing _process and _physics_process, frame rate issues | Use only _physics_process for movement, multiply by delta |
| Memory Leak | Game gets slower over time | Not freeing nodes, circular references | Use queue_free(), break reference cycles |
| Input Lag | Delayed response to controls | Heavy _process, input in wrong place | Use _input/_unhandled_input, optimize heavy code |
| Collision Issues | Objects pass through each other | Wrong collision layers, high speed | Check collision masks, use move_and_collide() for bullets |
Debugging Pro Tips
- Use Godot's Debugger: Learn to use breakpoints, watch variables, step through code
- Binary Search: Comment out half your code to isolate bugs
- Rubber Duck Debugging: Explain your code to someone (or something) else
- Version Control: Use git to revert to working versions when stuck
- Take Breaks: Fresh eyes often spot what you've been missing
Practice Exercise
Debug this intentionally buggy code:
extends Node2D
var enemies = []
func _ready():
for i in range(10):
var enemy = create_enemy()
enemies.append(enemy)
func create_enemy():
var enemy = Sprite2D.new()
enemy.position = Vector2(randi() % 100, randi() % 100)
add_child(enemy)
return enemy
func _process(delta):
for enemy in enemies:
enemy.position.x += 100 * delta
if enemy.position.x > 1000:
remove_enemy(enemy)
func remove_enemy(enemy):
enemies.erase(enemy)
enemy.queue_free()
Find and fix at least 3 bugs in this code. Use proper debugging techniques.
Node Specific Codes
Working with different Godot node types
Common Node Types and Their Scripts
Role: Different nodes have different properties and methods you need to know.
| Node Type | Primary Use | Key Properties/Methods | Example Code Pattern |
|---|---|---|---|
| Sprite2D | Display 2D images | texture, modulate, flip_h, flip_v |
$Sprite2D.texture = load("res://icon.png") |
| CharacterBody2D | 2D character movement | velocity, move_and_slide(), is_on_floor() |
velocity.x = speed * input_dir |
| Area2D | Detection zones | body_entered, body_exited, area_entered |
Connect signals to detect collisions |
| AnimationPlayer | Play animations | play(), stop(), current_animation |
$AnimationPlayer.play("walk") |
| Timer | Timed events | wait_time, start(), stop(), timeout |
Connect timeout signal to functions |
Sprite2D Specific Code
extends Sprite2D
# Sprite properties
@export var normal_texture: Texture2D
@export var hover_texture: Texture2D
@export var click_texture: Texture2D
var is_hovered = false
var is_clicked = false
func _ready():
# Set initial texture
texture = normal_texture
# Make sure sprite processes input
set_process_input(true)
func _input(event: InputEvent):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
# Check if click is within sprite bounds
var mouse_pos = get_global_mouse_position()
var sprite_rect = Rect2(global_position - (scale * texture.get_size() / 2), scale * texture.get_size())
if sprite_rect.has_point(mouse_pos):
is_clicked = true
texture = click_texture
else:
if is_clicked:
is_clicked = false
texture = normal_texture
on_click() # Custom click handler
elif event is InputEventMouseMotion:
# Check hover state
var mouse_pos = get_global_mouse_position()
var sprite_rect = Rect2(global_position - (scale * texture.get_size() / 2), scale * texture.get_size())
var now_hovered = sprite_rect.has_point(mouse_pos)
if now_hovered != is_hovered:
is_hovered = now_hovered
if is_hovered and not is_clicked:
texture = hover_texture
modulate = Color(1.2, 1.2, 1.2) # Brighten
elif not is_clicked:
texture = normal_texture
modulate = Color.WHITE
func on_click():
print("Sprite clicked!")
# Add your click logic here
# Animation methods
func pulse(duration: float = 0.5):
# Create a simple pulse animation
var tween = create_tween()
tween.tween_property(self, "scale", Vector2(1.2, 1.2), duration / 2)
tween.tween_property(self, "scale", Vector2(1.0, 1.0), duration / 2)
Area2D and Collision Detection
extends Area2D
# Damage area (like a spike trap or enemy attack)
@export var damage_amount: int = 10
@export var damage_cooldown: float = 1.0 # Seconds between damage
@export var knockback_force: float = 200.0
var bodies_in_area: Array = []
var damage_timers: Dictionary = {} # Track cooldowns per body
func _ready():
# Connect area signals
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
# Set collision mask/layer if not set in editor
collision_layer = 2 # Layer 2
collision_mask = 1 # Detect layer 1 (usually players)
func _on_body_entered(body: Node2D):
if body.is_in_group("player"):
bodies_in_area.append(body)
damage_timers[body] = 0.0 # Initialize cooldown timer
# Apply immediate damage on entry
apply_damage(body)
func _on_body_exited(body: Node2D):
if body in bodies_in_area:
bodies_in_area.erase(body)
if damage_timers.has(body):
damage_timers.erase(body)
func _process(delta: float):
# Update cooldown timers
for body in damage_timers.keys():
damage_timers[body] -= delta
# Apply damage if cooldown expired and body still in area
if damage_timers[body] <= 0 and body in bodies_in_area:
apply_damage(body)
damage_timers[body] = damage_cooldown
func apply_damage(body: Node2D):
# Apply damage to body
if body.has_method("take_damage"):
body.take_damage(damage_amount)
# Apply knockback
if body is CharacterBody2D:
var direction = (body.global_position - global_position).normalized()
body.velocity += direction * knockback_force
# Visual/audio feedback
play_hit_effect()
func play_hit_effect():
# Spawn particles, play sound, etc.
print("Damage area hit something!")
AnimationPlayer Control
extends Node2D
@onready var animation_player = $AnimationPlayer
@onready var sprite = $Sprite2D
enum AnimationState {IDLE, WALK, JUMP, ATTACK, HURT, DIE}
var current_animation_state: AnimationState = AnimationState.IDLE
var previous_animation_state: AnimationState = AnimationState.IDLE
func _ready():
# Connect animation finished signal
animation_player.animation_finished.connect(_on_animation_finished)
# Start with idle animation
play_animation("idle")
func _process(delta: float):
# Determine which animation to play based on game state
var new_state = determine_animation_state()
if new_state != current_animation_state:
previous_animation_state = current_animation_state
current_animation_state = new_state
update_animation()
func determine_animation_state() -> AnimationState:
# Example logic - adjust based on your game
if Input.is_action_pressed("attack"):
return AnimationState.ATTACK
elif not is_on_floor():
return AnimationState.JUMP
elif Input.is_action_pressed("move_left") or Input.is_action_pressed("move_right"):
return AnimationState.WALK
else:
return AnimationState.IDLE
func update_animation():
var animation_name = get_animation_name_for_state(current_animation_state)
# Don't restart the same animation unless it's a one-shot
if animation_player.current_animation != animation_name:
play_animation(animation_name)
# Handle special cases
match current_animation_state:
AnimationState.ATTACK:
# Attack animations usually shouldn't be interrupted
pass
AnimationState.HURT, AnimationState.DIE:
# These animations play once then transition
pass
func get_animation_name_for_state(state: AnimationState) -> String:
match state:
AnimationState.IDLE:
return "idle"
AnimationState.WALK:
return "walk"
AnimationState.JUMP:
return "jump"
AnimationState.ATTACK:
return "attack"
AnimationState.HURT:
return "hurt"
AnimationState.DIE:
return "die"
_:
return "idle"
func play_animation(animation_name: String):
if animation_player.has_animation(animation_name):
animation_player.play(animation_name)
else:
print("Warning: Animation '", animation_name, "' not found!")
func _on_animation_finished(anim_name: String):
# Handle animation completion
match anim_name:
"attack":
# Return to previous state after attack
current_animation_state = previous_animation_state
update_animation()
"hurt":
# Return to idle after hurt animation
current_animation_state = AnimationState.IDLE
update_animation()
"die":
# Game over logic
game_over()
# Advanced animation control
func blend_animations(animation1: String, animation2: String, blend_amount: float):
# Simple animation blending example
animation_player.play(animation1)
animation_player.advance(blend_amount)
func change_animation_speed(speed_multiplier: float):
animation_player.speed_scale = speed_multiplier
func play_animation_backwards(animation_name: String):
animation_player.play(animation_name)
animation_player.advance(animation_player.current_animation_length)
animation_player.play_backwards(animation_name)
Node-Specific Best Practices
- Sprite2D: Use texture atlases for multiple frames, not individual sprites
- Area2D: Set proper collision layers/masks to optimize performance
- AnimationPlayer: Use animation trees for complex state machines
- Timer: Use one-shot timers for delays, repeating timers for pulses
- AudioStreamPlayer: Pool multiple players for sound effects
- Camera2D: Use smoothing and limits for better player experience
Practice Exercise
Create an interactive object using multiple node types:
- Use Sprite2D for visual representation
- Add Area2D for click detection
- Use AnimationPlayer for hover/click animations
- Add AudioStreamPlayer for sound effects
- Make it change color when hovered and scale when clicked
- Play different sounds for hover and click
Game Systems
Building complex gameplay systems
What are Game Systems?
Role: Interconnected components that create gameplay mechanics.
Examples: Inventory, dialogue, saving, AI, physics, UI, audio.
Design principle: Systems should be modular, reusable, and decoupled.
# Inventory System Base Class
extends Node
class_name InventorySystem
@export var max_slots: int = 20
@export var max_weight: float = 100.0
var items: Array[InventoryItem] = []
var equipped_items: Dictionary = {} # slot_type: item
signal inventory_updated
signal item_added(item: InventoryItem, slot: int)
signal item_removed(item: InventoryItem, slot: int)
signal item_equipped(item: InventoryItem, slot_type: String)
signal item_unequipped(item: InventoryItem, slot_type: String)
func _ready():
# Initialize empty inventory slots
for i in range(max_slots):
items.append(null)
func add_item(item: InventoryItem) -> bool:
# Check if we can carry this item
if get_current_weight() + item.weight > max_weight:
print("Too heavy to carry!")
return false
# Find empty slot or stack with existing item
var slot = find_available_slot(item)
if slot == -1:
print("No space in inventory!")
return false
# Add to slot
if items[slot] == null:
items[slot] = item
item.quantity = 1
else:
# Stack items
var existing_item = items[slot]
var space_left = existing_item.max_stack - existing_item.quantity
if space_left >= item.quantity:
existing_item.quantity += item.quantity
else:
# Partial stack
existing_item.quantity = existing_item.max_stack
item.quantity -= space_left
# Try to add remainder to another slot
return add_item(item) and true # Recursive call
emit_signal("item_added", item, slot)
emit_signal("inventory_updated")
return true
func remove_item(slot: int, quantity: int = 1) -> InventoryItem:
if slot < 0 or slot >= items.size() or items[slot] == null:
return null
var item = items[slot]
if quantity >= item.quantity:
# Remove entire stack
items[slot] = null
else:
# Remove partial stack
item.quantity -= quantity
# Create new item instance for the removed portion
var removed_item = item.duplicate()
removed_item.quantity = quantity
emit_signal("item_removed", item, slot)
emit_signal("inventory_updated")
return item
func find_available_slot(item: InventoryItem) -> int:
# First try to stack with existing items
for i in range(items.size()):
if items[i] != null and items[i].item_id == item.item_id:
if items[i].quantity < items[i].max_stack:
return i
# Then find empty slot
for i in range(items.size()):
if items[i] == null:
return i
return -1 # No available slots
func get_current_weight() -> float:
var total_weight: float = 0.0
for item in items:
if item != null:
total_weight += item.weight * item.quantity
return total_weight
func equip_item(slot: int, equipment_slot: String) -> bool:
if slot < 0 or slot >= items.size() or items[slot] == null:
return false
var item = items[slot]
# Check if item can be equipped in this slot
if not equipment_slot in item.equippable_slots:
print("Item cannot be equipped in this slot!")
return false
# Unequip current item in this slot if any
if equipped_items.has(equipment_slot):
unequip_item(equipment_slot)
# Equip new item
equipped_items[equipment_slot] = item
# Apply item stats to player
apply_item_stats(item)
emit_signal("item_equipped", item, equipment_slot)
return true
func unequip_item(equipment_slot: String) -> InventoryItem:
if not equipped_items.has(equipment_slot):
return null
var item = equipped_items[equipment_slot]
equipped_items.erase(equipment_slot)
# Remove item stats from player
remove_item_stats(item)
emit_signal("item_unequipped", item, equipment_slot)
return item
func apply_item_stats(item: InventoryItem):
# Apply item's stats to the player character
# This would interface with your character stats system
print("Applying stats from ", item.item_name)
Dialogue System
extends CanvasLayer
class_name DialogueSystem
@onready var dialogue_box = $DialogueBox
@onready var name_label = $DialogueBox/NameLabel
@onready var text_label = $DialogueBox/TextLabel
@onready var choices_container = $DialogueBox/ChoicesContainer
@onready var continue_indicator = $DialogueBox/ContinueIndicator
@export var text_speed: float = 0.05 # Seconds per character
@export var auto_advance_delay: float = 2.0 # Seconds before auto-advance
var current_dialogue: Dictionary
var current_line_index: int = 0
var is_dialogue_active: bool = false
var is_typing: bool = false
var typewriter_timer: float = 0.0
var current_character_index: int = 0
signal dialogue_started
signal dialogue_ended
signal dialogue_line_started(line_index: int)
signal dialogue_line_completed(line_index: int)
signal choice_selected(choice_index: int)
func _ready():
hide_dialogue()
func start_dialogue(dialogue_data: Dictionary):
if is_dialogue_active:
print("Dialogue already active!")
return
current_dialogue = dialogue_data
current_line_index = 0
is_dialogue_active = true
show_dialogue()
emit_signal("dialogue_started")
display_current_line()
func display_current_line():
if current_line_index >= current_dialogue["lines"].size():
end_dialogue()
return
var line_data = current_dialogue["lines"][current_line_index]
# Set speaker name
name_label.text = line_data["speaker"] if line_data.has("speaker") else ""
# Clear previous text
text_label.text = ""
current_character_index = 0
is_typing = true
# Clear previous choices
clear_choices()
# Hide continue indicator while typing
continue_indicator.visible = false
emit_signal("dialogue_line_started", current_line_index)
func _process(delta: float):
if not is_dialogue_active or not is_typing:
return
# Typewriter effect
typewriter_timer += delta
var line_data = current_dialogue["lines"][current_line_index]
var full_text = line_data["text"]
if typewriter_timer >= text_speed:
typewriter_timer = 0.0
current_character_index += 1
if current_character_index > full_text.length():
finish_typing()
else:
text_label.text = full_text.substr(0, current_character_index)
func finish_typing():
is_typing = false
var line_data = current_dialogue["lines"][current_line_index]
text_label.text = line_data["text"]
# Show choices if this line has them
if line_data.has("choices") and not line_data["choices"].is_empty():
show_choices(line_data["choices"])
else:
# Show continue indicator
continue_indicator.visible = true
emit_signal("dialogue_line_completed", current_line_index)
func show_choices(choices: Array):
# Create choice buttons
for i in range(choices.size()):
var choice_data = choices[i]
var button = Button.new()
button.text = choice_data["text"]
button.pressed.connect(
func(): on_choice_selected(i)
)
choices_container.add_child(button)
func on_choice_selected(choice_index: int):
var line_data = current_dialogue["lines"][current_line_index]
var choice_data = line_data["choices"][choice_index]
# Handle choice result
if choice_data.has("next_line"):
current_line_index = choice_data["next_line"]
elif choice_data.has("end_dialogue") and choice_data["end_dialogue"]:
end_dialogue()
return
else:
current_line_index += 1
clear_choices()
display_current_line()
emit_signal("choice_selected", choice_index)
func advance_dialogue():
if not is_dialogue_active:
return
if is_typing:
finish_typing()
return
var line_data = current_dialogue["lines"][current_line_index]
# Check if current line has choices
if line_data.has("choices") and not line_data["choices"].is_empty():
# Don't advance if there are choices to make
return
current_line_index += 1
display_current_line()
func end_dialogue():
is_dialogue_active = false
hide_dialogue()
emit_signal("dialogue_ended")
func show_dialogue():
dialogue_box.visible = true
set_process(true)
func hide_dialogue():
dialogue_box.visible = false
set_process(false)
func clear_choices():
for child in choices_container.get_children():
child.queue_free()
# Example dialogue data structure:
# var dialogue = {
# "lines": [
# {
# "speaker": "NPC",
# "text": "Hello, traveler!",
# },
# {
# "speaker": "NPC",
# "text": "Would you like to help me?",
# "choices": [
# {"text": "Yes, I'll help!", "next_line": 2},
# {"text": "No, I'm busy.", "end_dialogue": true}
# ]
# }
# ]
# }
Save/Load System
extends Node
class_name SaveSystem
const SAVE_DIR = "user://saves/"
const SAVE_FILE_TEMPLATE = "save_%d.json"
signal game_saved(slot: int)
signal game_loaded(slot: int)
signal save_deleted(slot: int)
func _ready():
# Ensure save directory exists
var dir = DirAccess.open("user://")
if not dir.dir_exists(SAVE_DIR):
dir.make_dir(SAVE_DIR)
func save_game(slot: int = 0, data: Dictionary = {}) -> bool:
# Get save data from game systems if not provided
if data.is_empty():
data = collect_save_data()
# Add metadata
data["metadata"] = {
"save_date": Time.get_datetime_string_from_system(),
"game_version": "1.0.0",
"play_time": GameStats.play_time
}
# Convert to JSON
var json_string = JSON.stringify(data, "\t")
# Save to file
var file_path = SAVE_DIR.path_join(SAVE_FILE_TEMPLATE % slot)
var file = FileAccess.open(file_path, FileAccess.WRITE)
if file == null:
push_error("Failed to save game: ", FileAccess.get_open_error())
return false
file.store_string(json_string)
file.close()
print("Game saved to slot ", slot)
emit_signal("game_saved", slot)
return true
func load_game(slot: int = 0) -> Dictionary:
var file_path = SAVE_DIR.path_join(SAVE_FILE_TEMPLATE % slot)
if not FileAccess.file_exists(file_path):
push_error("Save file not found: ", file_path)
return {}
var file = FileAccess.open(file_path, FileAccess.READ)
if file == null:
push_error("Failed to load game: ", FileAccess.get_open_error())
return {}
var json_string = file.get_as_text()
file.close()
var json = JSON.new()
var error = json.parse(json_string)
if error != OK:
push_error("Failed to parse save file: ", json.get_error_message())
return {}
var data = json.get_data()
# Validate save data
if not validate_save_data(data):
push_error("Save data validation failed!")
return {}
print("Game loaded from slot ", slot)
emit_signal("game_loaded", slot)
return data
func collect_save_data() -> Dictionary:
# Collect data from all game systems
var data = {}
# Player data
data["player"] = {
"position": GameState.player_position,
"health": GameState.player_health,
"level": GameState.player_level,
"experience": GameState.player_experience
}
# Inventory data
var inventory_data = []
for item in InventorySystem.items:
if item != null:
inventory_data.append({
"item_id": item.item_id,
"quantity": item.quantity
})
data["inventory"] = inventory_data
# Quest data
data["quests"] = QuestSystem.get_save_data()
# World state
data["world"] = WorldState.get_save_data()
return data
func apply_save_data(data: Dictionary):
# Apply loaded data to game systems
# Player data
if data.has("player"):
var player_data = data["player"]
GameState.player_position = player_data["position"]
GameState.player_health = player_data["health"]
GameState.player_level = player_data["level"]
GameState.player_experience = player_data["experience"]
# Inventory data
if data.has("inventory"):
InventorySystem.load_from_data(data["inventory"])
# Quest data
if data.has("quests"):
QuestSystem.load_from_data(data["quests"])
# World state
if data.has("world"):
WorldState.load_from_data(data["world"])
func validate_save_data(data: Dictionary) -> bool:
# Basic validation
if not data.has("metadata"):
return false
var metadata = data["metadata"]
# Check version compatibility
if metadata.has("game_version"):
var save_version = metadata["game_version"]
var current_version = "1.0.0" # Should be from your game config
# Simple version check (in real game, use proper semver comparison)
if save_version != current_version:
print("Warning: Save version mismatch. Save: ", save_version, ", Current: ", current_version)
# You might want to add version migration logic here
return true
func get_save_slots_info() -> Array:
# Get information about all save slots
var slots_info = []
for slot in range(10): # 10 save slots
var file_path = SAVE_DIR.path_join(SAVE_FILE_TEMPLATE % slot)
if FileAccess.file_exists(file_path):
# Load just the metadata to show in UI
var file = FileAccess.open(file_path, FileAccess.READ)
if file:
var json_string = file.get_as_text()
file.close()
var json = JSON.new()
if json.parse(json_string) == OK:
var data = json.get_data()
if data.has("metadata"):
slots_info.append({
"slot": slot,
"exists": true,
"metadata": data["metadata"]
})
continue
slots_info.append({
"slot": slot,
"exists": false,
"metadata": {}
})
return slots_info
func delete_save(slot: int) -> bool:
var file_path = SAVE_DIR.path_join(SAVE_FILE_TEMPLATE % slot)
if not FileAccess.file_exists(file_path):
print("Save file not found: ", file_path)
return false
var dir = DirAccess.open(SAVE_DIR)
if dir.remove(file_path) != OK:
push_error("Failed to delete save file: ", file_path)
return false
print("Save file deleted: ", file_path)
emit_signal("save_deleted", slot)
return true
Game System Design Principles
- Single Responsibility: Each system does one thing well
- Loose Coupling: Systems communicate through interfaces, not direct dependencies
- Data-Driven: Use configuration files for balance and content
- Extensible: Design for adding new features without breaking old ones
- Testable: Systems should be easy to test in isolation
- Serializable: All important state should be saveable
Practice Exercise
Design a simple quest system:
- Create Quest class with: ID, name, description, objectives, rewards
- Create QuestManager to track active/completed quests
- Implement objective types: Collect items, Kill enemies, Talk to NPCs
- Add quest progression tracking
- Create UI to show current quests
- Add save/load functionality for quest state
Full Game Development
Putting everything together to create a complete game
Game Development Lifecycle
Role: Structured approach to creating a complete game from idea to release.
The 6 Phases of Game Development
- Concept: Idea, genre, target audience, core mechanics
- Pre-production: Design documents, prototypes, art style
- Production: Building assets, coding, implementing features
- Testing: Bug fixing, balancing, playtesting
- Polish: Optimization, UI/UX improvements, final touches
- Release: Packaging, distribution, marketing
# Main Game Script - Ties everything together
extends Node
class_name GameMain
# Game state management
enum GamePhase {BOOT, MAIN_MENU, PLAYING, PAUSED, GAME_OVER, CREDITS}
@export var current_phase: GamePhase = GamePhase.BOOT
# Subsystems
@onready var audio_manager = $AudioManager
@onready var save_system = $SaveSystem
@onready var input_manager = $InputManager
@onready var scene_manager = $SceneManager
@onready var ui_manager = $UIManager
# Game data
var game_settings: Dictionary = {}
var player_data: Dictionary = {}
var world_state: Dictionary = {}
func _ready():
# Initialize game
initialize_game()
func initialize_game():
print("=== GAME INITIALIZATION ===")
# Load settings
load_settings()
# Apply settings to subsystems
apply_game_settings()
# Initialize subsystems
audio_manager.initialize()
input_manager.initialize()
ui_manager.initialize()
# Show splash screen or go to main menu
transition_to_phase(GamePhase.MAIN_MENU)
print("Game initialized successfully")
func load_settings():
# Try to load saved settings
var settings_path = "user://settings.cfg"
if FileAccess.file_exists(settings_path):
var file = FileAccess.open(settings_path, FileAccess.READ)
if file:
var json_string = file.get_as_text()
file.close()
var json = JSON.new()
if json.parse(json_string) == OK:
game_settings = json.get_data()
return
# Load default settings
game_settings = {
"audio": {
"master_volume": 1.0,
"music_volume": 0.8,
"sfx_volume": 1.0
},
"graphics": {
"resolution": Vector2(1280, 720),
"fullscreen": false,
"vsync": true
},
"gameplay": {
"difficulty": "normal",
"language": "en"
}
}
func save_settings():
var settings_path = "user://settings.cfg"
var file = FileAccess.open(settings_path, FileAccess.WRITE)
if file:
var json_string = JSON.stringify(game_settings, "\t")
file.store_string(json_string)
file.close()
print("Settings saved")
else:
push_error("Failed to save settings")
func apply_game_settings():
# Apply audio settings
if game_settings.has("audio"):
var audio = game_settings["audio"]
audio_manager.set_master_volume(audio["master_volume"])
audio_manager.set_music_volume(audio["music_volume"])
audio_manager.set_sfx_volume(audio["sfx_volume"])
# Apply graphics settings
if game_settings.has("graphics"):
var graphics = game_settings["graphics"]
var resolution = graphics["resolution"]
get_window().set_size(resolution)
get_window().set_mode(Window.MODE_FULLSCREEN if graphics["fullscreen"] else Window.MODE_WINDOWED)
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED if graphics["vsync"] else DisplayServer.VSYNC_DISABLED)
func transition_to_phase(new_phase: GamePhase):
# Exit current phase
exit_current_phase()
# Enter new phase
current_phase = new_phase
enter_new_phase(new_phase)
func exit_current_phase():
match current_phase:
GamePhase.MAIN_MENU:
ui_manager.hide_main_menu()
GamePhase.PLAYING:
# Save game state if needed
if should_autosave():
save_system.save_game(0)
GamePhase.PAUSED:
ui_manager.hide_pause_menu()
func enter_new_phase(phase: GamePhase):
match phase:
GamePhase.MAIN_MENU:
ui_manager.show_main_menu()
audio_manager.play_menu_music()
GamePhase.PLAYING:
start_gameplay()
GamePhase.PAUSED:
ui_manager.show_pause_menu()
GamePhase.GAME_OVER:
ui_manager.show_game_over_screen()
GamePhase.CREDITS:
ui_manager.show_credits()
func start_gameplay():
# Load or create new game
if should_load_saved_game():
var save_data = save_system.load_game(0)
if not save_data.is_empty():
load_game_state(save_data)
else:
create_new_game()
# Start gameplay systems
audio_manager.play_gameplay_music()
ui_manager.show_game_hud()
# Unpause game
get_tree().paused = false
func create_new_game():
print("Creating new game...")
# Initialize player data
player_data = {
"health": 100,
"level": 1,
"experience": 0,
"position": Vector2(0, 0)
}
# Initialize world state
world_state = {
"current_level": 1,
"time_of_day": 0.0,
"weather": "clear"
}
# Load first level
scene_manager.load_level(1)
func load_game_state(save_data: Dictionary):
print("Loading game state...")
# Apply save data to all systems
save_system.apply_save_data(save_data)
# Load appropriate level
if save_data.has("world") and save_data["world"].has("current_level"):
scene_manager.load_level(save_data["world"]["current_level"])
func should_autosave() -> bool:
# Check if we should autosave based on game state
return current_phase == GamePhase.PLAYING and game_settings.get("autosave", true)
func should_load_saved_game() -> bool:
# Check if there's a saved game and player wants to continue
# This would be set by the main menu based on player choice
return false # For new game
func _input(event: InputEvent):
# Global input handling
if event.is_action_pressed("pause"):
if current_phase == GamePhase.PLAYING:
transition_to_phase(GamePhase.PAUSED)
get_tree().paused = true
elif current_phase == GamePhase.PAUSED:
transition_to_phase(GamePhase.PLAYING)
get_tree().paused = false
if event.is_action_pressed("fullscreen"):
toggle_fullscreen()
func toggle_fullscreen():
var fullscreen = not get_window().mode == Window.MODE_FULLSCREEN
get_window().set_mode(Window.MODE_FULLSCREEN if fullscreen else Window.MODE_WINDOWED)
# Update settings
if not game_settings.has("graphics"):
game_settings["graphics"] = {}
game_settings["graphics"]["fullscreen"] = fullscreen
save_settings()
func quit_game():
# Save before quitting
if should_autosave():
save_system.save_game(0)
save_settings()
print("Quitting game...")
get_tree().quit()
Project Structure Best Practices
Recommended Folder Structure
res://
âââ addons/ # Third-party plugins
âââ assets/ # Game assets
â âââ audio/ # Sound effects and music
â âââ fonts/ # Font files
â âââ graphics/ # Sprites, textures, UI
â âââ models/ # 3D models (if applicable)
âââ scenes/ # Godot scene files
â âââ characters/ # Player, enemies, NPCs
â âââ levels/ # Game levels/worlds
â âââ ui/ # UI screens and elements
â âââ systems/ # Reusable systems/mechanics
âââ scripts/ # GD Script files
â âââ systems/ # Game systems (inventory, save, etc.)
â âââ characters/ # Character scripts
â âââ ui/ # UI scripts
â âââ utils/ # Utility functions/helpers
âââ shaders/ # Custom shaders
âââ translation/ # Localization files
Performance Optimization Checklist
Optimization Tips
- Use texture atlases - Combine small sprites into single texture
- Instance scenes - Don't duplicate nodes, instance scenes
- Pool objects - Reuse objects instead of creating/destroying
- Limit physics bodies - Use areas instead of rigid bodies when possible
- Use LOD - Lower detail models at distance
- Batch draw calls - Use MultiMeshInstance for identical objects
- Optimize scripts - Avoid heavy operations in _process
- Use signals - Instead of polling every frame
- Cache node references - Use @onready instead of get_node() repeatedly
- Profile regularly - Use Godot's profiler to find bottlenecks
Publishing Checklist
Before Releasing Your Game
- Test on target platforms - Windows, Mac, Linux, mobile, etc.
- Implement proper save system - With backup/versioning
- Add settings/options - Graphics, audio, controls
- Polish UI/UX - Clear menus, good feedback, accessibility
- Balance gameplay - Difficulty curves, rewards, progression
- Fix all critical bugs - Crashes, progression blockers
- Optimize performance - Target 60 FPS on minimum spec
- Add credits - Acknowledge contributors, assets
- Create store assets - Icons, screenshots, trailers
- Write documentation - Readme, controls, system requirements
- Set up distribution - Steam, itch.io, website, etc.
- Plan for updates - Bug fixes, content updates
Common Pitfalls to Avoid
Game Development Mistakes
- Scope creep - Adding features forever, never finishing
- No prototyping - Building full systems before testing core mechanics
- Ignoring feedback - Not playtesting with real players
- Bad project structure - Everything in one folder, no organization
- No version control - Using git only after losing work
- Optimizing too early - Making code complex before it works
- Copying without understanding - Using code/tutorials without learning
- No backup strategy - Losing everything to hardware failure
- Skipping documentation - Can't remember how your own code works
- Ignoring business side - Not thinking about marketing, monetization
Final Advice for Aspiring Game Developers
- Start small - Complete tiny games before attempting big projects
- Finish projects - A finished small game is better than an unfinished big one
- Learn fundamentals - Don't just copy code, understand how it works
- Join communities - Godot communities, game jams, forums
- Playtest constantly - Your perspective is different from players'
- Iterate - Games improve through multiple versions
- Have fun - If you're not enjoying making it, players won't enjoy playing it
Final Project Challenge
Create a complete mini-game using everything you've learned:
- Concept: Simple arcade game (space shooter, platformer, puzzle)
- Scope: 1 level, 1 player character, 1 enemy type, basic UI
- Requirements:
- Main menu with Start/Quit buttons
- Gameplay with win/lose conditions
- Scoring system
- Sound effects and music
- Game over screen with restart option
- Simple save system for high score
- Bonus: Add particle effects, screen shake, difficulty options
- Goal: A complete, playable game that can be shared with others
Remember: Done is better than perfect. Finish it, then make it better.
Comments & Prints
Document your code and debug with output
Comments - Code Documentation
Role: Add explanations that are ignored by the computer but help humans understand code.
Why use: Explain complex logic, document assumptions, leave TODOs, make code maintainable.
Print Statements for Debugging
Role: Output information to Godot's Output panel during runtime.
Why use: See variable values, track execution flow, debug logic errors.
Advanced Debugging Output
Debug Overlay Technique
For real-time debugging during gameplay, create an on-screen debug overlay: