godot-engine.md 18 KB

Godot Engine Architecture Knowledge

Overview

Godot is an open-source game engine using a scene tree architecture where everything is a node. Scenes are reusable node compositions that can be instanced, nested, and inherited. Godot supports GDScript (Python-like), C#, and C++ via GDExtension.

Architecture Fundamentals

Scene Tree Model

Godot's architecture is built on a tree of nodes. Every game element — from UI to physics bodies to audio — is a node in the scene tree.

Key concepts:

  • Node — Base building block. Has a name, can have children, receives callbacks (_ready, _process, _physics_process)
  • Scene — A saved tree of nodes (.tscn file). Can be instanced at runtime
  • Autoload — Singleton nodes loaded before any scene, accessible globally via name
  • Resource — Data objects (.tres) that can be shared across nodes and saved to disk

Node hierarchy pattern:

Main (Node)
├── World (Node2D / Node3D)
│   ├── Level (Node)
│   │   ├── TileMap / GridMap
│   │   └── SpawnPoints
│   ├── Entities (Node)
│   │   ├── Player (CharacterBody2D/3D)
│   │   └── Enemies (Node)
│   └── Camera2D / Camera3D
├── UI (CanvasLayer)
│   ├── HUD
│   └── Menus
└── Systems (Node)
    ├── AudioManager
    └── GameStateManager

Autoloads vs Dependency Injection

Autoloads (Singletons):

Use sparingly for truly global services:

  • GameManager — Game state, pause, scene transitions
  • AudioManager — Music and SFX playback
  • SaveManager — Save/load operations
  • EventBus — Global signal hub (if using event bus pattern)

When NOT to use autoloads:

  • Systems only needed in gameplay scenes (use scene composition instead)
  • Data that should be passed explicitly (use Resources or method parameters)
  • Anything that makes unit testing harder

Signal Architecture

Signals are Godot's observer pattern implementation. They decouple communication between nodes.

Best practices:

# Define signals with typed parameters
signal health_changed(new_health: int, max_health: int)
signal died

# Connect in code (preferred for dynamic connections)
enemy.died.connect(_on_enemy_died)

# Emit with arguments
health_changed.emit(current_health, max_health)

Signal vs direct call guidelines:

Use Signals When Use Direct Calls When
Notifying unknown listeners Calling a known child node
Broadcasting state changes Requesting specific behavior
Decoupling siblings Parent configuring a child
Multiple listeners possible Single target, synchronous

Resource-Based Data

Custom Resources are powerful for data-driven design:

# weapon_stats.gd
class_name WeaponStats
extends Resource

@export var damage: float = 10.0
@export var attack_speed: float = 1.0
@export var range: float = 100.0
@export var damage_type: DamageType = DamageType.PHYSICAL

var dps: float:
    get: return damage * attack_speed

Use Resources for:

  • Item definitions, enemy stats, ability data
  • Configuration presets
  • Shared data between scenes
  • Any data that designers should edit in the Inspector

GDScript vs C# Decision Guide

Factor GDScript C#
Learning curve Easy, Python-like Moderate, requires .NET knowledge
Engine integration Native, full API access Full API access, slight overhead
Performance Good for gameplay logic Better for computation-heavy code
Typing Optional static typing Strong static typing
Tooling Built-in editor VS Code, Rider, Visual Studio
Ecosystem Godot-native addons NuGet packages available
Export targets All platforms Most platforms (.NET 6+ required)
Team familiarity Godot-specific skill Transferable C# skill

Recommendation: Use GDScript as default. Use C# when the team has strong C# background or when heavy computation benefits from it. Mixing is supported but adds build complexity.

GDScript Static Typing

Always use static typing for better performance and error catching:

# Preferred: fully typed
var speed: float = 200.0
var direction: Vector2 = Vector2.ZERO
var inventory: Array[Item] = []
var stats: Dictionary = {}

func take_damage(amount: float, source: Node) -> void:
    health -= amount
    if health <= 0:
        die()

func get_nearest_enemy() -> Enemy:
    # Return type declared
    ...

Coding Best Practices

Node References

# Use @onready for child references (resolved after _ready)
@onready var sprite: Sprite2D = $Sprite2D
@onready var collision: CollisionShape2D = $CollisionShape2D
@onready var anim_player: AnimationPlayer = $AnimationPlayer

# Use %UniqueNodeName for unique nodes in scene
@onready var health_bar: ProgressBar = %HealthBar

# For nodes in other parts of tree, use get_node or signals
# NEVER use absolute paths like /root/Main/World/Player

Export Variables

# Use @export for Inspector-configurable values
@export var move_speed: float = 200.0
@export var jump_force: float = -400.0
@export var max_health: int = 100

# Export groups for organization
@export_group("Movement")
@export var acceleration: float = 500.0
@export var friction: float = 700.0

@export_group("Combat")
@export var weapon: WeaponStats
@export var armor_rating: int = 0

# Export enums
@export_enum("Warrior", "Mage", "Rogue") var character_class: String

# Export ranges
@export_range(0, 100, 1) var volume: int = 80

State Machine Pattern

# state_machine.gd
class_name StateMachine
extends Node

@export var initial_state: State
var current_state: State

func _ready() -> void:
    for child in get_children():
        if child is State:
            child.state_machine = self
    current_state = initial_state
    current_state.enter()

func _process(delta: float) -> void:
    current_state.update(delta)

func _physics_process(delta: float) -> void:
    current_state.physics_update(delta)

func transition_to(target_state: State) -> void:
    current_state.exit()
    current_state = target_state
    current_state.enter()

# state.gd
class_name State
extends Node

var state_machine: StateMachine

func enter() -> void: pass
func exit() -> void: pass
func update(_delta: float) -> void: pass
func physics_update(_delta: float) -> void: pass

Scene Organization Conventions

project/
├── addons/                  # Third-party plugins
├── assets/                  # Raw art, audio, fonts
│   ├── sprites/
│   ├── audio/
│   │   ├── music/
│   │   └── sfx/
│   ├── fonts/
│   └── shaders/
├── scenes/                  # .tscn scene files
│   ├── main/
│   ├── levels/
│   ├── ui/
│   └── entities/
├── scripts/                 # .gd script files
│   ├── autoloads/
│   ├── components/
│   ├── resources/
│   ├── state_machines/
│   └── utils/
├── data/                    # .tres resource files, JSON configs
│   ├── items/
│   ├── enemies/
│   └── levels/
└── tests/                   # GUT test files
    ├── unit/
    ├── integration/
    └── e2e/

Naming conventions:

  • Files and folders: snake_case (e.g., player_controller.gd, main_menu.tscn)
  • Classes: PascalCase (e.g., class_name PlayerController)
  • Variables and functions: snake_case
  • Constants: UPPER_SNAKE_CASE
  • Signals: snake_case past tense (e.g., health_changed, item_collected)
  • Private members: prefix with _ (e.g., _internal_state)

Performance Optimization

General Guidelines

  • Use _physics_process for movement/physics, _process for visuals and input
  • Avoid _process on nodes that don't need per-frame updates — use signals or timers instead
  • Object pooling for frequently spawned objects (bullets, particles, enemies)
  • Use call_deferred for operations that shouldn't happen mid-frame

Rendering Performance

  • Use visibility notifiers (VisibleOnScreenNotifier2D/3D) to disable processing off-screen
  • Limit draw calls — use texture atlases, merge static geometry
  • For 2D: Use CanvasGroup for batch rendering, minimize CanvasLayer count
  • For 3D: Use LOD (MultiMeshInstance3D), occlusion culling, and the appropriate renderer:
    • Forward+ — Best for most 3D games
    • Mobile — Lower overhead, fewer features
    • Compatibility (OpenGL) — Web export, older hardware

Physics Performance

  • Use Area2D/3D for triggers instead of physics bodies when no collision response needed
  • Simplify collision shapes — circles/spheres over polygons where possible
  • Collision layers and masks — configure strictly, don't leave defaults
  • For many static bodies use StaticBody not RigidBody with freeze

Memory and Loading

  • Preload vs load: preload() for small, always-needed assets; load() or ResourceLoader.load_threaded_request() for large or conditional assets
  • Free nodes explicitly when done: queue_free() for deferred, free() for immediate
  • Use ResourceLoader threaded loading for seamless scene transitions:

    func change_scene_async(path: String) -> void:
    ResourceLoader.load_threaded_request(path)
    # Show loading screen
    while ResourceLoader.load_threaded_get_status(path) != ResourceLoader.THREAD_LOAD_LOADED:
        await get_tree().process_frame
    var scene = ResourceLoader.load_threaded_get(path)
    get_tree().change_scene_to_packed(scene)
    

GDScript Performance Tips

  • Use static typing everywhere — typed code runs significantly faster
  • Avoid get_node() in loops — cache references with @onready
  • Use Array.map(), filter(), reduce() carefully — they create closures
  • For hot paths, consider moving to C# or GDExtension (C++)

Popular Third-Party Plugins

Essential Development

Plugin Purpose Source
GUT (Godot Unit Test) Testing framework AssetLib or git submodule
Phantom Camera Advanced camera system (2D/3D) AssetLib
Limbo AI Behavior trees and state machines for AI AssetLib or GitHub
Dialogic 2 Dialogue and timeline system AssetLib
Terrain3D Large-scale 3D terrain editing GitHub releases

Gameplay Systems

Plugin Purpose Source
GodotSteam Steamworks SDK integration GitHub / GDExtension
Godot Jolt Jolt Physics replacement (faster, more stable 3D) AssetLib
SmartShape2D Procedural 2D terrain/shapes AssetLib
Maaack's Game Template Project template with menus, settings, save system GitHub / AssetLib
Quest Manager Quest/objective tracking AssetLib

Visual and Audio

Plugin Purpose Source
Godot Shaders Curated shader collection GitHub
Sprite3D Billboard 2D sprites in 3D world AssetLib
SFXPlayer Advanced SFX management with pooling AssetLib

Networking

Plugin Purpose Source
Godot Steam Multiplayer Peer Steam networking transport GitHub
Nakama Open-source game server (auth, matchmaking, realtime) Docker + GDScript SDK
WebRTC Browser-compatible P2P networking Built-in module

Export and Platform

Plugin Purpose Source
Godot Google Play Google Play services integration GitHub
Godot Admob Ad monetization for mobile GitHub

AI-Assisted Development

Godot has no official MCP server, but being open source it has spawned several community ones — all free, unlike Unity's official MCP, which sits behind a paid Unity AI subscription. An MCP (Model Context Protocol) server lets an AI assistant inspect and edit your project directly rather than guessing from chat descriptions.

  • GoPeak (HaD0Yun/Gopeak-godot-mcp) — MIT-licensed, actively maintained, ~95+ tools for Godot 4.x. Covers the full edit → run → inspect → fix loop: scene and GDScript editing, resource and shader work, LSP diagnostics, DAP breakpoints, live scene-tree inspection, screenshots, and input injection for testing. Run it with npx -y gopeak, then point your MCP client at the Godot executable and a tool profile (compact, full, or legacy).

Other community options — tugcantopaloglu/godot-mcp (149 tools, runtime GDScript over TCP), plus bradypp/godot-mcp and ee0pdt/Godot-MCP — are catalogued with install steps in engine-mcps.yaml. MCP repos move fast, so confirm a project is still maintained before adopting it.

Common Architectural Patterns

Component Pattern (Composition over Inheritance)

# Instead of deep inheritance, compose nodes:
# Player (CharacterBody2D)
#   ├── HealthComponent
#   ├── HitboxComponent
#   ├── HurtboxComponent
#   ├── StateMachine
#   │   ├── IdleState
#   │   ├── RunState
#   │   └── JumpState
#   └── InteractionComponent

# health_component.gd
class_name HealthComponent
extends Node

signal health_changed(current: int, maximum: int)
signal died

@export var max_health: int = 100
var current_health: int

func _ready() -> void:
    current_health = max_health

func take_damage(amount: int) -> void:
    current_health = max(0, current_health - amount)
    health_changed.emit(current_health, max_health)
    if current_health == 0:
        died.emit()

func heal(amount: int) -> void:
    current_health = min(max_health, current_health + amount)
    health_changed.emit(current_health, max_health)

Event Bus Pattern

# autoloads/event_bus.gd
extends Node

# Game flow
signal game_started
signal game_paused
signal game_over(score: int)

# Gameplay
signal enemy_defeated(enemy_type: String, position: Vector2)
signal item_collected(item: Item)
signal level_completed(level_id: String)

# UI
signal show_dialogue(text: String, speaker: String)
signal notification_requested(message: String)

Scene Transition Manager

# autoloads/scene_manager.gd
extends Node

signal scene_changed(scene_name: String)
signal loading_progress(progress: float)

var _current_scene: Node
var _loading_screen: Control

func change_scene(path: String, transition: String = "fade") -> void:
    _show_transition(transition)
    await _transition_midpoint
    _load_scene_async(path)

Godot 4 vs Godot 3 Key Differences

If architecture docs or tutorials reference Godot 3, note these changes:

Godot 3 Godot 4
yield() await
KinematicBody2D CharacterBody2D
move_and_slide(velocity) returns velocity Set self.velocity = ... then call move_and_slide() (no args, returns void)
export var @export var
onready var @onready var
connect("signal", obj, "method") signal.connect(callable)
tween = create_tween() Same, but Tween is refcounted
GDNative (C++) GDExtension (C++)
PoolStringArray PackedStringArray

Licensing and Cost

Godot is licensed under the MIT License — fully free and open source with no royalties, no per-seat fees, and no revenue share. You can modify the engine source, use it commercially, and ship on any platform without paying Godot anything.

Cost implications:

  • Engine: Free forever, no revenue thresholds
  • Console export: Requires third-party porting services (see below) — paid
  • Plugins: Most community plugins are MIT/free; some Asset Store items are paid
  • No vendor lock-in: you own your export templates and can fork the engine

When NOT to Use Godot

Godot may be the wrong choice when:

  • AAA-quality 3D visuals are required — Godot's 3D renderer is improving rapidly but does not yet match Unreal's Nanite/Lumen or Unity's HDRP for high-fidelity visuals
  • Console launch is a hard requirement — Console export requires contracting with specific porting companies (W4 Games, Lone Wolf Technology, Pineapple Works) under NDA, adds cost, and limits release timing. This is a significant business risk for console-first projects
  • Large team with C# expertise and no GDScript willingness — While Godot supports C#, the ecosystem (plugins, tutorials, community) is overwhelmingly GDScript-oriented
  • Proven production pipeline for 100+ person teams — Godot's tooling for large-scale team workflows (asset pipelines, custom editors, build systems) is less mature than Unity or Unreal
  • VR/XR is the primary platform — Godot has OpenXR support but the ecosystem (interaction toolkits, platform SDKs) is substantially less mature than Unity XR Toolkit or Unreal OpenXR

Console Export Reality

Console export for Godot is substantially more complex than desktop/mobile/web:

  • Not available through the open-source engine — Console SDKs (Nintendo, Sony, PlayStation) are under NDA and cannot be distributed with Godot
  • Requires a third-party porting company with console SDK access:
    • W4 Games — Founded by Godot core contributors, offers official console porting
    • Lone Wolf Technology — Provides Godot console export solutions
    • Pineapple Works — Another porting option
  • Cost and timeline: Porting contracts add development cost and can introduce delays. Budget this early in planning
  • Developer program membership required — You still need Nintendo Developer, PlayStation Partners, or Xbox ID@Xbox registration independently
  • Testing and certification — Console certification requirements add QA overhead regardless of engine

If console is a target: Factor porting cost and timeline into the architecture decision. Consider whether Unity or Unreal's built-in console export support would reduce risk for your project.

Architecture Decision Checklist

When choosing Godot for a project, verify:

  • Target platforms supported by Godot export (web, mobile, desktop, consoles via third-party)
  • GDScript vs C# decision based on team skills
  • 2D vs 3D renderer selection (Forward+, Mobile, Compatibility)
  • Physics engine: default Godot Physics or Jolt plugin for 3D
  • Networking approach if multiplayer (built-in MultiplayerAPI, Steam, Nakama)
  • Audio middleware needed or engine-native sufficient
  • Key plugins identified and verified as Godot 4 compatible
  • Console export strategy evaluated (see Console Export Reality section — requires porting company, adds cost and timeline)