GD Script Topics

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:

  1. Click in the gutter next to line numbers to set a red breakpoint dot
  2. Run your scene in Debug mode (F5 or Debug → Start Debugging)
  3. When execution hits the breakpoint, it pauses
  4. Use the Debugger panel to inspect variables
  5. 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:

  1. Base damage (int)
  2. Attack multiplier (float, default 1.0)
  3. 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

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.

# Single line comment - use for brief explanations
var health = 100  # Current player health

"""
Multi-line comment (three quotes)
Use for longer explanations, function documentation,
or temporarily disabling blocks of code.
"""

# TODO comments for future work
# TODO: Implement power-up system
# FIXME: This function has a bug when health = 0

# Function documentation
## Calculates damage based on weapon and enemy type.
## @param base_damage: The base damage of the weapon
## @param multiplier: Damage multiplier (default 1.0)
## @return: Final damage amount
func calculate_damage(base_damage: int, multiplier: float = 1.0) -> int:
    var damage = base_damage * multiplier
    return int(damage)

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.

# Basic print
print("Game started")

# Print multiple values
print("Player position: ", position, " Health: ", health)

# Debug prints with labels
print("DEBUG: Player took damage. New health: ", health)

# Print for tracking function calls
func complex_function():
    print("DEBUG: Entering complex_function")
    # ... complex logic ...
    print("DEBUG: Exiting complex_function")

# Print with formatting
var player_name = "Hero"
var score = 1500
print("Player %s has score: %d" % [player_name, score])

Advanced Debugging Output

# Print dictionaries and arrays nicely
var inventory = {"sword": 1, "shield": 1, "potion": 3}
print("Inventory: ", inventory)

# Conditional debugging
const DEBUG_MODE = true

func debug_print(message):
    if DEBUG_MODE:
        print("DEBUG: ", message)

# Use throughout code - easy to disable
debug_print("Player moved to " + str(position))

# Print with different severity levels
func log_info(message):
    print("[INFO] ", message)

func log_warning(message):
    print("[WARN] ", message)
    push_warning(message)  # Also shows in Godot's warning system

func log_error(message):
    print("[ERROR] ", message)
    push_error(message)  # Shows in Godot's error system

Debug Overlay Technique

For real-time debugging during gameplay, create an on-screen debug overlay:

# Create a Label node as child, reference it:
onready var debug_label = $DebugLabel

func _process(delta):
    # Update debug text every frame
    debug_label.text = "FPS: %d\nPos: %s\nHealth: %d" % [
        Engine.get_frames_per_second(),
        str(position),
        health
    ]

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:

  1. Select the node that emits the signal
  2. Go to Node tab (right side of editor)
  3. Double-click the signal you want to connect
  4. Select target node and method
  5. 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:

  1. Create a Health component with signals: health_changed, health_depleted
  2. Create a UI that connects to these signals and updates a health bar
  3. Create an enemy that damages the player and triggers the signals
  4. 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

  1. Use descriptive scene names: player_character.tscn not scene1.tscn
  2. Organize in folders: scenes/characters/, scenes/ui/, scenes/levels/
  3. Keep scenes focused: One main responsibility per scene
  4. Use instancing for duplicates: Don't copy-paste nodes, instance scenes
  5. Preload frequently used scenes: Better performance than loading each time

Practice Exercise

Create a simple scene manager:

  1. Create 3 scenes: MainMenu, GameLevel, GameOver
  2. Make buttons in MainMenu to start game and quit
  3. In GameLevel, add a way to trigger GameOver scene
  4. In GameOver, add button to return to MainMenu
  5. 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:

  1. Create a speed boost power-up that lasts 5 seconds
  2. Create a shield power-up that lasts 10 seconds
  3. Show countdown timers on screen for active power-ups
  4. Make power-ups not stack (reset timer if collected again)
  5. 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:

  1. Go to Project → Project Settings
  2. Select Autoload tab
  3. Click folder icon to select script
  4. Give it a name (e.g., "GameState")
  5. 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

  1. Be specific: Provide context about your game and goals
  2. Iterate: Ask AI to improve or fix its own code
  3. Combine: Mix AI-generated code with your own knowledge
  4. Verify: Always test AI code thoroughly
  5. 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

  1. Clear Rules: Players should understand how the game works
  2. Fairness: Challenges should be difficult but achievable
  3. Feedback: Players should know why they succeed or fail
  4. Progression: Game should get appropriately harder
  5. Balance: No single strategy should dominate
  6. Emergence: Simple rules should create complex gameplay

Practice Exercise

Design and implement game logic for a simple platformer:

  1. Create win condition: Collect 10 coins and reach exit
  2. Create lose condition: Fall off screen or run out of time (60 seconds)
  3. Implement scoring: Coins = 100 points, time bonus = remaining seconds × 10
  4. Add difficulty levels that affect enemy speed and spawn rate
  5. 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

  1. Reproduce: Make the bug happen consistently
  2. Isolate: Find the smallest code that causes the bug
  3. Identify: Determine exactly why it happens
  4. Fix: Apply the smallest possible change
  5. 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

  1. Use Godot's Debugger: Learn to use breakpoints, watch variables, step through code
  2. Binary Search: Comment out half your code to isolate bugs
  3. Rubber Duck Debugging: Explain your code to someone (or something) else
  4. Version Control: Use git to revert to working versions when stuck
  5. 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

  1. Sprite2D: Use texture atlases for multiple frames, not individual sprites
  2. Area2D: Set proper collision layers/masks to optimize performance
  3. AnimationPlayer: Use animation trees for complex state machines
  4. Timer: Use one-shot timers for delays, repeating timers for pulses
  5. AudioStreamPlayer: Pool multiple players for sound effects
  6. Camera2D: Use smoothing and limits for better player experience

Practice Exercise

Create an interactive object using multiple node types:

  1. Use Sprite2D for visual representation
  2. Add Area2D for click detection
  3. Use AnimationPlayer for hover/click animations
  4. Add AudioStreamPlayer for sound effects
  5. Make it change color when hovered and scale when clicked
  6. 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

  1. Single Responsibility: Each system does one thing well
  2. Loose Coupling: Systems communicate through interfaces, not direct dependencies
  3. Data-Driven: Use configuration files for balance and content
  4. Extensible: Design for adding new features without breaking old ones
  5. Testable: Systems should be easy to test in isolation
  6. Serializable: All important state should be saveable

Practice Exercise

Design a simple quest system:

  1. Create Quest class with: ID, name, description, objectives, rewards
  2. Create QuestManager to track active/completed quests
  3. Implement objective types: Collect items, Kill enemies, Talk to NPCs
  4. Add quest progression tracking
  5. Create UI to show current quests
  6. 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

  1. Concept: Idea, genre, target audience, core mechanics
  2. Pre-production: Design documents, prototypes, art style
  3. Production: Building assets, coding, implementing features
  4. Testing: Bug fixing, balancing, playtesting
  5. Polish: Optimization, UI/UX improvements, final touches
  6. 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

  1. Test on target platforms - Windows, Mac, Linux, mobile, etc.
  2. Implement proper save system - With backup/versioning
  3. Add settings/options - Graphics, audio, controls
  4. Polish UI/UX - Clear menus, good feedback, accessibility
  5. Balance gameplay - Difficulty curves, rewards, progression
  6. Fix all critical bugs - Crashes, progression blockers
  7. Optimize performance - Target 60 FPS on minimum spec
  8. Add credits - Acknowledge contributors, assets
  9. Create store assets - Icons, screenshots, trailers
  10. Write documentation - Readme, controls, system requirements
  11. Set up distribution - Steam, itch.io, website, etc.
  12. 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

  1. Start small - Complete tiny games before attempting big projects
  2. Finish projects - A finished small game is better than an unfinished big one
  3. Learn fundamentals - Don't just copy code, understand how it works
  4. Join communities - Godot communities, game jams, forums
  5. Playtest constantly - Your perspective is different from players'
  6. Iterate - Games improve through multiple versions
  7. 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:

  1. Concept: Simple arcade game (space shooter, platformer, puzzle)
  2. Scope: 1 level, 1 player character, 1 enemy type, basic UI
  3. 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
  4. Bonus: Add particle effects, screen shake, difficulty options
  5. Goal: A complete, playable game that can be shared with others

Remember: Done is better than perfect. Finish it, then make it better.