Intro Into Gdscript

πŸ€– Introduction to GDScript (Godot 4.x)

I really enjoy GDScriptβ€”it sits in a sweet spot as a practical mix of Python, Lua, and Go:

While GDScript started as a purely dynamically typed language, modern GDScript 2.0 (Godot 4.x) introduces strong static typing, annotations, first-class signals, and substantial performance improvements.


⚑ The Low-Level Dev's Mental Model

Coming from a C/C++ or Odin background, think of GDScript as your high-level orchestration layer:


πŸ”‘ 1. Variables & The Static Type System

In Godot 4, static typing is strongly recommended. Typed GDScript executes up to 2–3x faster because the VM can skip dynamic method lookups and type validation at runtime.

extends CharacterBody3D
class_name PlayerController

# 1. Dynamic typing (flexible, but slower)
var unassigned_var
var dynamic_score = 100

# 2. Explicit static typing (Recommended)
var max_health: int = 100
var move_speed: float = 7.5
var player_name: String = "Ryan"
var is_active: bool = true

# 3. Type inference (:= infers type from right-hand assignment)
var velocity_multiplier := 1.25 # Inferred as float
var target_direction := Vector3.ZERO # Inferred as Vector3

# 4. Constants
const GRAVITY: float = 9.81
const MAX_INVENTORY_SLOTS: int = 24

🏷️ 2. Essential Modern Annotations

Annotations in Godot 4 replace the older export keywords and provide clear metadata to the editor and compiler.

# Exposes variable to the Godot Inspector
@export var walk_speed: float = 5.0
@export_range(0.0, 100.0, 0.5) var armor_rating: float = 25.0

# Grouping variables cleanly in the Inspector
@export_group("Combat Stats")
@export var attack_power: int = 15
@export var critical_chance: float = 0.15

# Safe node reference: assigns once the node and its children enter the scene tree
@onready var camera: Camera3D = $CameraPivot/SpringArm3D/Camera3D
@onready var animation_tree: AnimationTree = %AnimationTree # Scene Unique Node syntax

# Runs script directly inside the Godot Editor viewport
# @tool

# Networking RPC configuration
# @rpc("any_peer", "call_local", "reliable")
Scene Unique Nodes (%)

Instead of brittle long node paths like $UI/Margin/HBox/VBox/Label, mark your node as Access as Unique Name in the scene dock. In code, access it directly with %Label. If you rearrange the UI hierarchy later, the code reference doesn't break!


πŸ“‘ 3. Signals (First-Class Observer Pattern)

Signals are Godot’s built-in observer pattern. They decouple your systems: nodes emit signals when things happen, and listener nodes subscribe to them.

# Defining typed signals
signal health_changed(current_hp: int, max_hp: int)
signal died

var current_hp: int = 100
var max_hp: int = 100

func take_damage(amount: int) -> void:
    current_hp = clampi(current_hp - amount, 0, max_hp)
    # Emit the signal to all listeners
    health_changed.emit(current_hp, max_hp)
    
    if current_hp <= 0:
        died.emit()

func _ready() -> void:
    # Connecting via Callables (Godot 4 style)
    health_changed.connect(_on_health_changed)

func _on_health_changed(hp: int, max: int) -> void:
    print("Health updated: %d / %d" % [hp, max])

πŸ”„ 4. The Node Lifecycle (_ready, _process, _physics_process)

Understanding execution order is critical for preventing null pointer exceptions and simulation jitter.

extends Node3D

# Called when node enters tree, before children are ready
func _enter_tree() -> void:
    pass

# Called once when this node AND all children have entered the tree
func _ready() -> void:
    # Safe to access children nodes here
    pass

# Called every render frame (variable delta). Use for visual updates / UI
func _process(delta: float) -> void:
    # Rotate visual mesh
    rotate_y(1.0 * delta)

# Called every fixed physics tick (default 60Hz). Use for movement and physics
func _physics_process(delta: float) -> void:
    # Physics math, CharacterBody movement, Raycasts
    pass

# Called when node leaves tree (cleanup)
func _exit_tree() -> void:
    pass
Frame Delta vs Physics Delta

Never handle Character movement or physics collisions in _process(). Frame rate fluctuations will cause inconsistent speed and wall-clipping. Always use _physics_process(delta).


⏳ 5. Asynchronous Logic (await)

Godot 4 scrapped yield in favor of await, mimicking modern async/await patterns from Go and JS:

func attack_routine() -> void:
    print("Attack windup...")
    # Wait for a 0.5 second one-shot timer
    await get_tree().create_timer(0.5).timeout
    
    print("Strike executed!")
    
    # Wait for an animation signal to finish
    await animation_player.animation_finished
    print("Ready for next command.")

πŸ“¦ 6. Custom Resources: Data-Oriented Design

In Godot, Resource is the equivalent of Unity's ScriptableObject or Unreal's UDataAsset. It’s great for item databases, enemy stats, and audio profiles.

# ItemData.gd
class_name ItemData
extends Resource

@export var item_id: String = "item_001"
@export var display_name: String = "Iron Blade"
@export var damage: int = 25
@export var icon: Texture2D

You can save this as an .tres asset in the filesystem and drag-and-drop it into any node's @export var item: ItemData slot!


πŸ“Š Comparison Summary: GDScript vs C++ vs C#

Feature GDScript (Godot 4) C# C++ / GDExtension
Compilation Bytecode / VM (Fast restart) JIT / AOT (.NET 8+) Native machine code (DLL)
Engine Integration Native 1st class (built-in editor) External IDE via LSP C ABI via godot-cpp
Execution Speed Fast for game logic (typed) High (~2-5x faster) Maximum (Bare metal)
Memory Management Reference counted (RefCounted) Garbage Collected (.NET GC) Manual / Smart Pointers
Best Used For UI, Gameplay flow, Prototyping Core logic, Large codebases Shaders, DSP, Physics, Sockets