How: Developed by Juan Linietsky and Ariel Manzur, first released publicly in 2014 as open-source.
Who: Maintained by the Godot Engine community and the Godot Foundation.
Why: To provide a fully free, open-source game engine with no royalties, no vendor lock-in, and a clean scene-based architecture.
Introduction
Godot is a feature-rich, cross-platform game engine for 2D and 3D games. It uses a unique scene/node system, supports GDScript (Python-like), C#, and C++ (via GDExtension), and exports to Windows, Linux, macOS, Android, iOS, and Web.
Advantages
Fully open-source and free — no royalties, no subscription.
Lightweight editor (~100MB), fast iteration.
Unified 2D and 3D workflows in one engine.
GDScript is beginner-friendly and tightly integrated.
Active community, frequent releases (Godot 4.x is a major leap).
Disadvantages
Smaller ecosystem than Unity/Unreal (fewer ready-made assets).
C# support in Godot 4 is still maturing (.NET 6+).
3D rendering less mature than Unreal Engine for AAA-level visuals.
Editor & Project Setup
Editor Layout
Scene Panel (left) — tree of all nodes in the current scene.
Viewport (center) — visual editor for 2D/3D.
Inspector (right) — properties of the selected node.
.tscn Text Scene — human-readable, version-control friendly (default for scenes)
.scn Binary Scene — compiled binary, faster to load, used in exported builds
.tres Text Resource — human-readable resource (materials, custom data, etc.)
.res Binary Resource — compiled binary version of .tres
.escn Exported Scene — identical to .tscn but marks the file as externally exported
(e.g. from Blender); auto-compiled to .scn on import
When you export your game, Godot automatically converts .tscn → .scn and .tres → .res for performance.
Use .tscn / .tres during development (readable diffs in git), and let the exporter handle the rest.
# Loading works the same regardless of format:var scene = preload("res://scenes/Player.tscn") # text scenevar scene = preload("res://scenes/Player.scn") # binary scene (exported)var data = preload("res://data/item.tres") # text resourcevar data = preload("res://data/item.res") # binary resource
UID (Unique Identifier) System
In Godot 4, files and assets can be identified by a UID rather than a hardcoded project path (res://...).
How it Works
Every imported asset or resource is assigned a globally unique 64-bit integer ID.
This ID is saved inside a .uid file (for text resources) or within the .import files of your project directory.
The engine compiles all UIDs into a central database located at .godot/uid_cache.bin.
UIDs use the prefix uid:// followed by a base-36 representation of the unique integer (e.g., uid://d4e94a21924e).
Why Use UIDs?
Prevents Broken References: If you rename or move an asset (e.g., a texture, script, or sub-scene) within the Godot FileSystem dock, Godot automatically updates its cache, but other scene files referencing it do not break because they reference the stable UID instead of the path.
Better Team Collaboration: Decreases merge conflicts in team environments when multiple developers move or organize directories.
Example: Path vs. UID
Hardcoded Path reference (Fragile):
# If Player.tscn is moved to res://characters/player/Player.tscn, this fails:var player_scene = preload("res://scenes/Player.tscn")
UID Reference (Resilient):
# This works even if the scene is moved or renamed anywhere in the project:var player_scene = preload("uid://cc1ygqg530w8x")
You can copy a resource’s UID by right-clicking it in the Godot FileSystem dock and selecting Copy UID.
Best Practices & Caveats
Version Control: Always commit .uid files to Git. If a .uid file is deleted, Godot will regenerate it, which might assign a new ID and cause reference issues on other machines.
Text Files: When looking at a .tscn file, you will see resources linked via both UID and path, like ext_resource type="PackedScene" uid="uid://..." path="res://..." id="1". Godot uses the path as a fallback if the UID cache is rebuilding.
Common Node Types
Node2D/3D Base for all 2D/3D nodes (has position, rotation, scale)
Sprite2D/3D Displays a texture in 2D/3D
AnimatedSprite2D/3D Sprite with frame animation
CollisionShape2D/3D Defines collision area shape
Area2D/3D Detects overlaps (no physics response)
CharacterBody2D/3D Kinematic body for player/enemies
RigidBody2D/3D Physics-simulated body
StaticBody2D/3D Immovable physics body (walls, floors)
Camera2D/3D 2D/3D camera with follow/zoom
Label UI text
Button UI button
CanvasLayer UI layer (always on top of game world)
3D Node Types
Node3D Base for all 3D nodes
MeshInstance3D Renders a 3D mesh
CollisionShape3D 3D collision shape
CharacterBody3D 3D kinematic body
RigidBody3D 3D physics body
Camera3D 3D camera
DirectionalLight3D Sun-like light
OmniLight3D Point light (all directions)
SpotLight3D Cone-shaped spotlight
WorldEnvironment Sky, ambient light, fog, tone mapping
Instancing Scenes
# In editor: drag .tscn into scene tree# In code:var bullet_scene = preload("res://scenes/Bullet.tscn")func shoot(): var bullet = bullet_scene.instantiate() bullet.position = $Muzzle.global_position get_parent().add_child(bullet)
GDScript
GDScript is Godot’s built-in scripting language — Python-like syntax, tightly integrated with the engine, supports static typing, signals, coroutines, lambdas, and full OOP.
For the complete in-depth GDScript language reference (variables, types, OOP, signals, annotations, coroutines, patterns, and more), see the dedicated GDScript note.
Quick reference:
extends CharacterBody2D # inherit a Godot classclass_name Player # register as global class@export var speed: float = 200.0 # editable in Inspector@onready var anim := $AnimationPlayer # assigned at _readysignal health_changed(new_hp: int)var health: int = 100: set(v): health = clamp(v, 0, 100) health_changed.emit(health)func _physics_process(delta: float) -> void: var dir = Input.get_axis("move_left", "move_right") velocity.x = dir * speed move_and_slide()
Signals & Event-Driven Architecture
Signals are Godot’s implementation of the Observer Pattern. They allow nodes to send notifications to other nodes without needing direct references, enabling a decoupled, event-driven codebase.
The golden rule of Godot scene design: “Signals Up, Call Down”. Parents call methods on children directly; children emit signals to notify parents of events.
Defining & Emitting Signals
Signals are declared at the top of a script using the signal keyword. They can define typed arguments.
extends CharacterBody2D# 1. Declaration (simple)signal clicked# 2. Declaration with typed arguments (highly recommended)signal health_changed(old_value: int, new_value: int)signal status_effect_applied(effect_name: StringName, duration: float)var health: int = 100func take_damage(amount: int) -> void: var old_health = health health = max(0, health - amount) # 3. Emitting the signal health_changed.emit(old_health, health)
Connecting Signals
1. Via the Editor UI
Select the node in the Scene tree.
Go to the Node tab (next to Inspector) → Signals.
Double-click a signal → Select the target node → Click Connect.
This creates a connection entry visible in the Editor and saved in the .tscn file.
2. Via Code (GDScript Callables)
In Godot 4, connections are made using the connect method on the signal object, passing a Callable (method reference).
# Connect signal to local methodfunc _ready() -> void: $Button.pressed.connect(_on_button_pressed) # Checking if connected if not health_changed.is_connected(_on_health_changed): health_changed.connect(_on_health_changed)func _on_button_pressed() -> void: print("Button clicked!")func _on_health_changed(old_val: int, new_val: int) -> void: print("Health went from ", old_val, " to ", new_val)
Advanced Signal Connections
1. Passing Extra Arguments (Binding)
You can bind extra arguments to a connection that weren’t defined in the original signal. This is useful for passing data like indices or node references.
# Multiple buttons call the same method, but pass different parametersfunc _ready() -> void: $BuySwordButton.pressed.connect(_on_purchase_clicked.bind("sword", 150)) $BuyShieldButton.pressed.connect(_on_purchase_clicked.bind("shield", 100))func _on_purchase_clicked(item_type: String, cost: int) -> void: print("Purchasing: ", item_type, " for ", cost, " gold.")
2. Lambda (Anonymous) Connections
You can connect a signal directly to an inline anonymous function (lambda).
func _ready() -> void: # Basic inline lambda $Timer.timeout.connect(func(): print("Time is up!")) # Lambda with closure (captures local variables) var local_multiplier = 1.5 $Enemy.died.connect(func(xp_reward: int): add_experience(xp_reward * local_multiplier) )
3. Connection Flags (Custom Behavior)
You can pass connection flags as an optional parameter to connect().
# Connect flags are passed via ConnectFlags enum# CONNECT_DEFERRED: Defers the call to the end of the physics/idle frame.# Vital when modifying physics states or scene hierarchy during collision signals.body_entered.connect(_on_body_entered, CONNECT_DEFERRED)# CONNECT_ONE_SHOT: Automatically disconnects the signal after it fires once.level_loaded.connect(_on_level_init, CONNECT_ONE_SHOT)# CONNECT_PERSISTED: Connection is saved when the scene is saved (used inside @tool scripts).property_changed.connect(_on_prop_update, CONNECT_PERSISTED)
Signal Disconnection & Safety
Connections are automatically removed when either the emitter or receiver node is deleted (queue_free()), so memory leaks are rarely an issue for scene nodes.
However, manual disconnection is sometimes needed:
To prevent calling code on an object that is in the middle of deletion or check if a Callable is valid:
# Check if target object exists and callable is validvar my_callable = Callable(target_node, "receive_event")if my_callable.is_valid(): my_callable.call()
Component-Based Architecture
The Composition Pattern
In game development, deep inheritance trees (e.g. Entity → Actor → Character → Player) cause rigid codebases. If both a Player and an ExplosiveBarrel need health, putting health in a shared ancestor class forces it onto nodes that shouldn’t have it (like static walls).
Godot implements Composition using Nodes as Components. Instead of inheriting features, a parent node gets its capabilities by having small, specialized child nodes attached to it.
Composition vs. Inheritance Architecture
INHERITANCE PATHWAY (Rigid)
Node2D ── PhysicsBody2D ── CharacterBody2D ── LivingEntity ── Player
│
└── Enemy (forces health on static objects)
COMPOSITION PATHWAY (Modular)
CharacterBody2D (Player)
├── HealthComponent (Node) - Manages health variables and math
├── HitboxComponent (Area2D) - Detects incoming damage
├── InputComponent (Node) - Gathers inputs
└── VelocityComponent (Node) - Handles movement math
In-Depth Tutorial: Creating Components
Here is a complete component system for damage, health, and movement.
1. HealthComponent
A pure logical component (extends Node) that tracks health, handles heals/damage, and notifies the parent via signals.
# res://components/HealthComponent.gdclass_name HealthComponentextends Nodesignal health_changed(old_value: int, new_value: int)signal health_depleted@export var max_health: int = 100@onready var current_health: int = max_healthfunc damage(amount: int) -> void: if amount <= 0: return var old_health = current_health current_health = max(0, current_health - amount) health_changed.emit(old_health, current_health) if current_health == 0: health_depleted.emit()func heal(amount: int) -> void: if amount <= 0: return var old_health = current_health current_health = min(max_health, current_health + amount) health_changed.emit(old_health, current_health)
2. HitboxComponent
An Area2D component that represents the physical area where an object can receive damage. It delegates damage to a HealthComponent.
# res://components/HitboxComponent.gdclass_name HitboxComponentextends Area2D# Reference to the health component representing this object@export var health_component: HealthComponent# Allows specifying a damage multiplier (e.g., 2.0 for a headshot hitbox)@export var damage_multiplier: float = 1.0func receive_damage(base_damage: int) -> void: if health_component: var final_damage = int(base_damage * damage_multiplier) health_component.damage(final_damage)
3. HurtboxComponent
An Area2D component that represents the area that inflicts damage. It monitors overlaps with HitboxComponents.
# res://components/HurtboxComponent.gdclass_name HurtboxComponentextends Area2D@export var damage: int = 10func _ready() -> void: # Connect collision signal area_entered.connect(_on_area_entered)func _on_area_entered(area: Area2D) -> void: # Check if overlapping area is a Hitbox if area is HitboxComponent: area.receive_damage(damage)
4. VelocityComponent
Manages movement velocity calculations, easing friction, and acceleration, keeping movement math separate from input collecting.
# res://components/VelocityComponent.gdclass_name VelocityComponentextends Node@export var max_speed: float = 300.0@export var acceleration: float = 1200.0@export var friction: float = 800.0var velocity: Vector2 = Vector2.ZEROfunc accelerate_in_direction(direction: Vector2, delta: float) -> void: if direction != Vector2.ZERO: velocity = velocity.move_toward(direction * max_speed, acceleration * delta) else: velocity = velocity.move_toward(Vector2.ZERO, friction * delta)func apply_to_body(body: CharacterBody2D) -> void: body.velocity = velocity body.move_and_slide() # Save resolved velocity back velocity = body.velocity
Connecting Components in a Scene
In your main Entity scene (e.g., Player or Enemy), you arrange these components as children and write a minimal orchestration script.
Important: In the editor, assign HealthComponent to the export property health_component on the HitboxComponent.
Orchestration Script (player.gd)
extends CharacterBody2D@onready var health_comp: HealthComponent = $HealthComponent@onready var velocity_comp: VelocityComponent = $VelocityComponentfunc _ready() -> void: # Orchestrate responses to component signals health_comp.health_depleted.connect(_on_death) health_comp.health_changed.connect(_on_health_changed)func _physics_process(delta: float) -> void: # Gather inputs and delegate calculations to components var move_dir = Input.get_vector("move_left", "move_right", "move_up", "move_down") # Calculate speed and apply movement physics velocity_comp.accelerate_in_direction(move_dir, delta) velocity_comp.apply_to_body(self)func _on_health_changed(old_hp: int, new_hp: int) -> void: print("Ouch! Player health is now: ", new_hp) # e.g., trigger screen shake or hit flash shaderfunc _on_death() -> void: set_physics_process(false) # play death animation, then delete queue_free()
Core Lifecycle Methods
Built-in Callbacks
extends Nodefunc _init() -> void: # Called when object is created (before added to scene) passfunc _ready() -> void: # Called once when node enters the scene tree # All children are ready at this point passfunc _process(delta: float) -> void: # Called every frame (tied to FPS) # delta = time since last frame in seconds position.x += speed * deltafunc _physics_process(delta: float) -> void: # Called every physics tick (default 60/s, fixed timestep) # Use for movement, physics, collision move_and_slide()func _input(event: InputEvent) -> void: # Called for every input event if event is InputEventKey: print(event.keycode)func _unhandled_input(event: InputEvent) -> void: # Called if no other node consumed the input passfunc _notification(what: int) -> void: # Low-level lifecycle notifications if what == NOTIFICATION_WM_CLOSE_REQUEST: get_tree().quit()func _exit_tree() -> void: # Called when node is removed from scene tree pass
delta Time Pattern
# Always multiply movement by delta for frame-rate independencefunc _process(delta: float) -> void: position += velocity * delta # Without delta: moves 200px per frame (FPS-dependent) # With delta: moves 200px per second (FPS-independent)
Input System
Input Actions (Project Settings → Input Map)
# Check action statefunc _process(delta: float) -> void: if Input.is_action_pressed("move_right"): velocity.x = speed elif Input.is_action_pressed("move_left"): velocity.x = -speed else: velocity.x = 0 if Input.is_action_just_pressed("jump"): jump() if Input.is_action_just_released("attack"): end_attack()# Get axis (returns -1, 0, or 1)var h = Input.get_axis("move_left", "move_right")var v = Input.get_axis("move_up", "move_down")var dir = Vector2(h, v).normalized()
Mouse Input
func _input(event: InputEvent) -> void: if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: print("Left click at: ", event.position) if event is InputEventMouseMotion: print("Mouse moved: ", event.relative)# Get mouse positionvar mouse_pos = get_global_mouse_position()
Gamepad Input
# Axis (joystick)var h = Input.get_joy_axis(0, JOY_AXIS_LEFT_X)var v = Input.get_joy_axis(0, JOY_AXIS_LEFT_Y)# Buttonif Input.is_joy_button_pressed(0, JOY_BUTTON_A): jump()
2D Game Development
CharacterBody2D — Player Movement
extends CharacterBody2Dconst SPEED = 200.0const JUMP_VELOCITY = -400.0const GRAVITY = 980.0func _physics_process(delta: float) -> void: # Apply gravity if not is_on_floor(): velocity.y += GRAVITY * delta # Horizontal movement var direction = Input.get_axis("move_left", "move_right") velocity.x = direction * SPEED # Jump if Input.is_action_just_pressed("jump") and is_on_floor(): velocity.y = JUMP_VELOCITY move_and_slide() # handles collision automatically
# Sprite2D — single texture$Sprite2D.texture = preload("res://assets/player.png")$Sprite2D.flip_h = true # mirror horizontally# AnimatedSprite2D — frame-based animation# Set up SpriteFrames resource in editor, then:$AnimatedSprite2D.play("run")$AnimatedSprite2D.stop()$AnimatedSprite2D.animation = "idle"$AnimatedSprite2D.frame = 0# Connect animation_finished signal$AnimatedSprite2D.animation_finished.connect(_on_anim_done)
TileMap — Level Design
# TileMap node with TileSet resource# In editor: paint tiles, set collision layers# In code: read/write tilesvar tilemap = $TileMapvar cell = tilemap.get_cell_source_id(0, Vector2i(5, 3))tilemap.set_cell(0, Vector2i(5, 3), 1, Vector2i(0, 0))# Convert world position to tile coordsvar tile_pos = tilemap.local_to_map(player.position)
3D Game Development
CharacterBody3D — 3D Player Movement
extends CharacterBody3Dconst SPEED = 5.0const JUMP_VELOCITY = 4.5var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")func _physics_process(delta: float) -> void: if not is_on_floor(): velocity.y -= gravity * delta if Input.is_action_just_pressed("jump") and is_on_floor(): velocity.y = JUMP_VELOCITY var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back") var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() if direction: velocity.x = direction.x * SPEED velocity.z = direction.z * SPEED else: velocity.x = move_toward(velocity.x, 0, SPEED) velocity.z = move_toward(velocity.z, 0, SPEED) move_and_slide()
Camera3D — First Person Look
extends Node3D@export var sensitivity: float = 0.003@onready var camera = $Camera3Dfunc _ready() -> void: Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion: rotate_y(-event.relative.x * sensitivity) camera.rotate_x(-event.relative.y * sensitivity) camera.rotation.x = clamp(camera.rotation.x, -PI/2, PI/2)
MeshInstance3D & Materials
# Assign mesh in editor or codevar mesh_instance = $MeshInstance3D# Create material in codevar mat = StandardMaterial3D.new()mat.albedo_color = Color.REDmat.metallic = 0.5mat.roughness = 0.3mesh_instance.material_override = mat# Load texturemat.albedo_texture = preload("res://assets/texture.png")
Raycasting 3D
func shoot_ray() -> void: var space_state = get_world_3d().direct_space_state var cam = $Camera3D var from = cam.global_position var to = from + (-cam.global_transform.basis.z * 100.0) var query = PhysicsRayQueryParameters3D.create(from, to) query.exclude = [self] var result = space_state.intersect_ray(query) if result: print("Hit: ", result.collider.name) print("Point: ", result.position) print("Normal: ", result.normal)
Physics System
Collision Layers & Masks
Layer: What this object IS (what layer it occupies)
Mask: What this object DETECTS (what layers it scans)
Example:
Player → Layer 1, Mask 2 (detects enemies)
Enemy → Layer 2, Mask 1 (detects player)
Bullet → Layer 3, Mask 2 (detects enemies only)
# Set in editor (Physics → Layers) or in code:collision_layer = 1 # bit 1collision_mask = 2 # bit 2# Check group membershipif body.is_in_group("enemy"): body.take_damage(10)
RigidBody2D / RigidBody3D
extends RigidBody2Dfunc _ready() -> void: # Apply impulse (instant force) apply_impulse(Vector2(0, -500)) # Apply continuous force apply_force(Vector2(100, 0)) # Set linear velocity directly linear_velocity = Vector2(200, 0) # Connect body_entered signal body_entered.connect(_on_collision)func _on_collision(body: Node) -> void: print("Collided with: ", body.name)
Groups
# Add to group in editor (Node → Groups) or code:add_to_group("enemies")add_to_group("collectibles")# Checkif is_in_group("enemies"): take_damage(10)# Call method on all in groupget_tree().call_group("enemies", "freeze")# Get all nodes in groupvar enemies = get_tree().get_nodes_in_group("enemies")
Control Base UI node
Label Display text
Button Clickable button
TextEdit Multi-line text input
LineEdit Single-line text input
ProgressBar Health/loading bar
Slider Value slider
CheckBox Toggle checkbox
OptionButton Dropdown menu
Panel Background panel
VBoxContainer Vertical layout
HBoxContainer Horizontal layout
GridContainer Grid layout
ScrollContainer Scrollable area
Anchor presets (Inspector → Layout):
Top Left / Top Right / Bottom Left / Bottom Right
Center / Full Rect (stretches to fill parent)
# In code:
$Label.anchor_left = 0.0
$Label.anchor_right = 1.0 # stretch full width
$Label.offset_left = 10
$Label.offset_right = -10
Audio System
AudioStreamPlayer
# AudioStreamPlayer — non-positional (music, UI sounds)# AudioStreamPlayer2D — positional 2D audio# AudioStreamPlayer3D — positional 3D audio@onready var music = $AudioStreamPlayer@onready var sfx = $SFXfunc _ready() -> void: music.stream = preload("res://audio/theme.ogg") music.play()func play_jump_sound() -> void: sfx.stream = preload("res://audio/jump.wav") sfx.play()# Volume (in dB)music.volume_db = -10.0# Pitchsfx.pitch_scale = randf_range(0.9, 1.1) # random pitch variation# Stopmusic.stop()print(music.playing) # false
Audio Buses
Audio buses (Project → Audio):
Master → Music → SFX → Ambient
# Assign bus in code:
$AudioStreamPlayer.bus = "Music"
# Control bus volume:
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Music"), -5.0)
AudioServer.set_bus_mute(AudioServer.get_bus_index("SFX"), true)
Resource System & Data Management
Resources
# Resources are data containers saved as .tres or .res files# Examples: Texture2D, AudioStream, Material, TileSet, Font# Load resourcevar texture = load("res://assets/player.png")var texture = preload("res://assets/player.png") # compile-time load# Custom Resourceclass_name ItemDataextends Resource@export var item_name: String = ""@export var damage: int = 0@export var icon: Texture2D# Use it:var sword = ItemData.new()sword.item_name = "Iron Sword"sword.damage = 25ResourceSaver.save(sword, "res://data/iron_sword.tres")# Load back:var loaded = load("res://data/iron_sword.tres") as ItemData
@export — Inspector Variables
extends Node@export var speed: float = 200.0@export var health: int = 100@export var sprite: Texture2D@export var enemy_scene: PackedScene@export_range(0, 100) var volume: float = 50.0@export_enum("Idle", "Run", "Attack") var state: int = 0@export_color_no_alpha var tint: Color = Color.WHITE
Saving & Loading Game Data
# Save with ConfigFile (simple key-value)func save_game() -> void: var config = ConfigFile.new() config.set_value("player", "health", health) config.set_value("player", "position", position) config.save("user://save.cfg")func load_game() -> void: var config = ConfigFile.new() if config.load("user://save.cfg") == OK: health = config.get_value("player", "health", 100) position = config.get_value("player", "position", Vector2.ZERO)# Save with JSONfunc save_json() -> void: var data = {"health": health, "score": score} var file = FileAccess.open("user://save.json", FileAccess.WRITE) file.store_string(JSON.stringify(data))func load_json() -> void: var file = FileAccess.open("user://save.json", FileAccess.READ) if file: var data = JSON.parse_string(file.get_as_text()) health = data["health"]
Scene Management
Changing Scenes
# Change scene (unloads current, loads new)get_tree().change_scene_to_file("res://scenes/Level2.tscn")# Change scene from PackedScenevar next_level = preload("res://scenes/Level2.tscn")get_tree().change_scene_to_packed(next_level)# Reload current sceneget_tree().reload_current_scene()# Quit gameget_tree().quit()
# Annotate functions with @rpc to call them on remote peers@rpc("any_peer") # any peer can call thisfunc take_damage(amount: int) -> void: health -= amount@rpc("authority", "call_local") # only server calls, runs locally toofunc sync_position(pos: Vector2) -> void: position = pos@rpc("any_peer", "reliable") # guaranteed delivery (like TCP)func chat_message(msg: String) -> void: print(msg)# Call on specific peertake_damage.rpc_id(peer_id, 25)# Call on all peerssync_position.rpc(global_position)
MultiplayerSpawner & MultiplayerSynchronizer
# MultiplayerSpawner — auto-spawns nodes on all clients# Add MultiplayerSpawner node, set spawn_path and auto_spawn_list in editor# MultiplayerSynchronizer — syncs properties automatically# Add MultiplayerSynchronizer node, configure replication in editor# Or in code:var sync = $MultiplayerSynchronizersync.root_path = NodePath("..")# Then add properties to replicate via editor or:# ReplicationConfig resource → add properties like "position", "health"
print("value: ", health) # basic printprint_rich("[color=red]Error![/color]") # colored outputpush_warning("Low health!") # shows in debugger as warningpush_error("Critical failure!") # shows as error, doesn't stop executionassert(health > 0, "Health must be positive") # crashes in debug builds# Print only in debug buildsif OS.is_debug_build(): print("Debug info: ", position)
Debugger Panel
Bottom panel → Debugger tab:
Errors — runtime errors and warnings
Stack Trace — call stack when paused/crashed
Inspector — live node property inspection while running
Profiler — CPU time per function (enable before running)
Visual Profiler — GPU frame time breakdown
Network — RPC and sync traffic monitor
Monitors — FPS, memory, physics objects, draw calls
Breakpoints
Click the line number gutter in the script editor to set a breakpoint.
Run the game → execution pauses at the breakpoint.
Use Step Over (F10), Step Into (F11), Continue (F12) to navigate.
Inspect local variables in the Debugger → Stack Locals panel.
Performance Tips
# Use _physics_process for physics, _process for visuals only# Cache node references with @onready instead of $Node in loops@onready var player = $Player # cached once# Avoid get_node() in hot loops# Use object pooling for bullets/particles instead of queue_free + instantiate# Check draw calls: Debug → Visible Collision Shapes / Navigation# Use VisibleOnScreenNotifier2D/3D to disable off-screen processing# Profile with:var time = Time.get_ticks_usec()# ... code ...print("Took: ", Time.get_ticks_usec() - time, " µs")
# .pck = packed resource file containing all game assets# Can be distributed separately from the executable# Useful for DLC or updates# Load external .pck at runtime:ProjectSettings.load_resource_pack("res://dlc_pack.pck")
Android Export
Requirements:
- Android SDK (API 28+)
- JDK 17+
- Godot Android export templates
Editor → Export → Android:
- Set package name (e.g. com.yourname.yourgame)
- Set keystore for release builds
- Enable permissions (INTERNET, VIBRATE, etc.)
- Min SDK: 21 (Android 5.0)
C# in Godot 4
Setup
Use the Godot .NET version (not the standard build).
Download: godotengine.org → .NET version
Requires: .NET SDK 6.0 or later
IDE: VS Code (with C# extension) or JetBrains Rider
C# Script Basics
using Godot;public partial class Player : CharacterBody2D{ [Export] public float Speed = 200.0f; [Export] public int Health = 100; public override void _Ready() { GD.Print("Player ready!"); } public override void _PhysicsProcess(double delta) { var velocity = Velocity; var direction = Input.GetAxis("move_left", "move_right"); velocity.X = direction * Speed; Velocity = velocity; MoveAndSlide(); } public void TakeDamage(int amount) { Health -= amount; if (Health <= 0) QueueFree(); }}
GDScript:
+ Faster iteration, no compile step
+ Tightly integrated with Godot API
+ Best for game logic, UI, scripting
- Slower than C# for heavy computation
C#:
+ Faster execution (JIT compiled)
+ Strong typing, better IDE support
+ Familiar for Unity developers
- Requires .NET build, slower hot-reload
- Some Godot 4 features lag behind GDScript support
GDExtension (C++ / Rust / Other Languages)
What is GDExtension
GDExtension lets you write native code (C++, Rust, Swift, etc.)
that integrates with Godot as if it were built-in.
Use it for: performance-critical systems, existing C++ libraries,
custom physics, or platform-specific features.
Replaces GDNative from Godot 3.
Uses godot-cpp bindings (official C++ library).
Build with SCons, output a .gdextension file pointing to your .dll/.so.
Advanced Godot Design Patterns & Tips
State Machine Pattern
For complex logic trees (like a player character who can move, jump, slide, wall-climb, and attack), basic if/else checks rapidly descend into “spaghetti code”. A Finite State Machine (FSM) ensures a character is in exactly one state at a time and defines clear rules for changing states.
1. Simple Enum State Machine (Best for simple AI / simple actors)
enum State { IDLE, RUN, JUMP, FALL }var current_state: State = State.IDLEfunc _physics_process(delta: float) -> void: # State action execution match current_state: State.IDLE: _state_idle(delta) State.RUN: _state_run(delta) State.JUMP: _state_jump(delta) State.FALL: _state_fall(delta) # State transition logic _check_transitions()func _check_transitions() -> void: var on_floor = is_on_floor() var move_input = Input.get_axis("move_left", "move_right") != 0 match current_state: State.IDLE: if move_input: current_state = State.RUN elif not on_floor: current_state = State.FALL State.RUN: if not move_input: current_state = State.IDLE elif not on_floor: current_state = State.FALL
2. Node-Based State Machine (Best for complex actors/bosses)
Each state is represented by its own node/script. This keeps code modular, files short, and makes adding states easy.
The State Base Class
# res://scripts/fsm/State.gdclass_name Stateextends Node# Reference to the FSM managervar fsm: StateMachine# Reference to the physical entity (e.g. CharacterBody2D)var actor: CharacterBody2D# Virtual methods to be overridden by child states:func enter() -> void: passfunc exit() -> void: passfunc handle_input(event: InputEvent) -> void: passfunc update(delta: float) -> void: passfunc physics_update(delta: float) -> void: pass
The StateMachine Manager Class
# res://scripts/fsm/StateMachine.gdclass_name StateMachineextends Node@export var initial_state: Statevar current_state: State# Assign physical actor to pass down to states@onready var actor: CharacterBody2D = get_parent()func _ready() -> void: # Setup all children states for child in get_children(): if child is State: child.fsm = self child.actor = actor if initial_state: transition_to(initial_state.name)func _unhandled_input(event: InputEvent) -> void: if current_state: current_state.handle_input(event)func _process(delta: float) -> void: if current_state: current_state.update(delta)func _physics_process(delta: float) -> void: if current_state: current_state.physics_update(delta)func transition_to(target_state_name: String) -> void: var target_state = get_node(target_state_name) as State if not target_state: push_error("State machine: State not found: " + target_state_name) return if current_state: current_state.exit() current_state = target_state current_state.enter()
Example Concrete State: PlayerIdleState
# res://scripts/fsm/PlayerIdleState.gdclass_name PlayerIdleStateextends Statefunc enter() -> void: actor.get_node("AnimationPlayer").play("idle") actor.velocity.x = 0func physics_update(delta: float) -> void: # Apply gravity if not actor.is_on_floor(): fsm.transition_to("PlayerFallState") return # Transition to run if input detected var input = Input.get_axis("move_left", "move_right") if input != 0: fsm.transition_to("PlayerRunState") return # Transition to jump if requested if Input.is_action_just_pressed("jump"): fsm.transition_to("PlayerJumpState")
The Event Bus is a central communications hub that allows disconnected nodes to broadcast and listen to game-wide events (e.g., player scoring, UI popups, game state shifts) without referencing each other or navigating the scene tree.
How to Setup the Event Bus
Create a script called EventBus.gd.
Go to Project Settings → Autoload, select EventBus.gd, and add it as an Autoload named EventBus (making it a global singleton).
# Inside an Enemy script when it diesfunc die() -> void: # Broadcast the event globally to any listeners EventBus.enemy_defeated.emit(100) EventBus.show_notification.emit("Enemy Slain! +100 PTS") queue_free()
Subscriber (Listening to Events)
# Inside a UI Score Label scriptextends Labelvar current_score: int = 0func _ready() -> void: # Subscribe to the global Event Bus EventBus.enemy_defeated.connect(_on_enemy_defeated)func _on_enemy_defeated(points: int) -> void: current_score += points text = "Score: " + str(current_score)
Performance & Cleaning Up
Safety: Since EventBus is a singleton, if a listening node is freed (queue_free()), Godot automatically disconnects its methods. However, if you hook up non-node Callables (like transient Resources or custom classes), you MUST disconnect them manually on deletion to prevent calling invalid memory.
In complex game interfaces (like menus, inventories, or skill trees), mixing data logic (e.g. inventory contents, gold coins) with UI rendering logic (e.g. updating labels, scaling progress bars) creates unmaintainable script structures. Godot developers solve this with a modified MVP/MVC pattern.
Structure breakdown
Model (Data): A custom Resource containing game data. Has no visual logic. (e.g. CharacterStats.tres).
View (Presentation): Control nodes that handle layout, button triggers, and graphics. They have no business logic and do not perform calculations.
Controller / Presenter (Glue): A controller script that connects the Model to the View. It listens to data changes on the model and pushes them to the view.
Script Implementation Example
1. The Model (Stats Resource)
# res://resources/PlayerStats.gdclass_name PlayerStatsextends Resourcesignal health_changed(new_hp: int)signal gold_changed(new_gold: int)@export var max_health: int = 100var health: int = 100 : set(value): health = clamp(value, 0, max_health) health_changed.emit(health)var gold: int = 0 : set(value): gold = max(0, value) gold_changed.emit(gold)
2. The View (UI Layout Controller)
# res://ui/PlayerHUDView.gdclass_name PlayerHUDViewextends CanvasLayer@onready var health_bar = $Margin/VBox/HealthBar@onready var gold_label = $Margin/VBox/GoldLabel# Pure setter functions, no business calculationsfunc update_health_display(health: int, max_health: int) -> void: health_bar.max_value = max_health health_bar.value = healthfunc update_gold_display(amount: int) -> void: gold_label.text = "Gold: " + str(amount)
3. The Controller / Presenter (Scene Orchestrator)
# res://scenes/GameHUDController.gdextends Node@export var player_stats: PlayerStats@onready var hud_view: PlayerHUDView = $PlayerHUDViewfunc _ready() -> void: if player_stats: # Connect Model signals to updater methods in View player_stats.health_changed.connect( func(new_hp): hud_view.update_health_display(new_hp, player_stats.max_health) ) player_stats.gold_changed.connect( func(new_gold): hud_view.update_gold_display(new_gold) ) # Initialize view starting state hud_view.update_health_display(player_stats.health, player_stats.max_health) hud_view.update_gold_display(player_stats.gold)
Object Pooling
# Reuse nodes instead of instantiate/queue_free every framevar pool: Array[Node] = []func get_from_pool() -> Node: for node in pool: if not node.visible: node.visible = true return node var new_node = bullet_scene.instantiate() add_child(new_node) pool.append(new_node) return new_nodefunc return_to_pool(node: Node) -> void: node.visible = false
@tool Scripts (Editor Scripts)
@toolextends Node2D@export var tile_count: int = 10 : set = _set_tile_countfunc _set_tile_count(value: int) -> void: tile_count = value _rebuild() # runs in editor when you change the valuefunc _rebuild() -> void: # Generate tiles procedurally in the editor for child in get_children(): child.queue_free() for i in tile_count: var tile = preload("res://scenes/Tile.tscn").instantiate() tile.position.x = i * 64 add_child(tile)
Coroutines with await
# await pauses execution until a signal fires or a time passesfunc show_message(text: String) -> void: $Label.text = text $Label.visible = true await get_tree().create_timer(2.0).timeout $Label.visible = false# await animation finish$AnimationPlayer.play("attack")await $AnimationPlayer.animation_finished$AnimationPlayer.play("idle")# await signalawait player_diedget_tree().reload_current_scene()
Navigation & Pathfinding
# 2D: NavigationRegion2D + NavigationAgent2Dextends CharacterBody2D@onready var nav_agent = $NavigationAgent2Dfunc move_to(target_pos: Vector2) -> void: nav_agent.target_position = target_posfunc _physics_process(delta: float) -> void: if nav_agent.is_navigation_finished(): return var next = nav_agent.get_next_path_position() var dir = (next - global_position).normalized() velocity = dir * speed move_and_slide()# 3D: NavigationRegion3D + NavigationAgent3D (same pattern)
More Learn
Explore the following links for valuable resources, communities, and tools to enhance your skills : -