GDScript Basics for Game Logic

Updated June 2026
GDScript is Godot's built-in scripting language, designed specifically for game development with a Python-like syntax that integrates deeply with the engine's node system, signals architecture, and scene tree. This guide covers the core language features you need to write game logic, from variables and functions to signals, typed collections, pattern matching, and coroutines.

GDScript exists because general-purpose languages carry overhead that game scripting does not need, while lacking tight integration with the engine's core concepts. GDScript knows about nodes, scenes, signals, and the game loop natively, so common patterns that require boilerplate in other languages are one-liners in GDScript. If you have written Python, the syntax will feel immediately familiar, though the two languages differ in their runtime, standard library, and object model.

Understand Variables and Types

Variables in GDScript are declared with the var keyword. Without type annotations, they are dynamically typed and can hold any value. With type annotations, you get compile-time checking and better editor autocompletion. The syntax is var name: Type = value, for example var health: int = 100 or var speed: float = 250.0.

Type inference with the := operator lets you skip the type name while still getting static typing. Writing var score := 0 creates a statically typed integer without explicitly saying int. This works with any expression: var position := Vector2.ZERO infers Vector2, and var enemies := [] infers an untyped Array.

Constants are declared with const and must be assigned at declaration time. Enums define named integer constants and are essential for state machines: enum State { IDLE, PATROL, CHASE, ATTACK } creates constants you reference as State.IDLE, State.PATROL, and so on. Export annotations like @export var speed: float = 200.0 expose variables in the Inspector panel, letting you tweak values without editing code.

Godot 4.x introduced typed arrays with the Array[Type] syntax. Declaring var bullets: Array[Area2D] = [] creates an array that only accepts Area2D nodes, and the engine validates this at runtime. Typed dictionaries use Dictionary[KeyType, ValueType] syntax. These typed collections catch bugs early and make your code self-documenting, especially in AI systems where you are managing lists of enemies, waypoints, or behavior nodes.

Write Functions and Use Built-in Callbacks

Functions are defined with the func keyword and use indentation to mark their body, just like Python. Return types are optional but recommended: func calculate_damage(base: float, multiplier: float) -> float: tells the editor and the type checker what this function returns.

Godot calls several built-in functions on your scripts automatically. _ready() runs once when the node enters the scene tree, making it the right place for initialization. _process(delta) runs every visual frame and is used for non-physics logic like UI updates, animation control, and input that does not affect physics. _physics_process(delta) runs at a fixed rate (60 times per second by default) and is where movement, collision responses, and AI updates belong. The delta parameter is the time elapsed since the last call, which you multiply with speed values to make movement frame-rate independent.

Other useful callbacks include _input(event) for handling raw input events, _unhandled_input(event) for input that no UI element consumed, and _enter_tree() and _exit_tree() for managing resources when nodes are added to or removed from the scene. Understanding which callback to use for which purpose prevents common bugs like jittery movement from using _process instead of _physics_process, or input being eaten by UI elements.

Use Signals for Event-Driven Logic

Signals are Godot's observer pattern implementation, and they are central to writing clean, decoupled game code. A signal is a named event that a node can emit, and any number of other nodes can listen for. You define custom signals at the top of your script: signal health_changed(new_value: int) or signal enemy_detected(enemy: CharacterBody2D).

Emit a signal with the .emit() method: health_changed.emit(current_health). Other nodes connect to the signal either in the editor (through the Node dock, Signals tab) or in code: player.health_changed.connect(_on_health_changed). The connected function receives whatever arguments the signal was emitted with.

For game AI, signals enable clean separation between detection and response. An enemy's detection area can emit player_spotted when the player enters its range, and the enemy's AI controller, the alert system, and the music manager can all connect to that signal independently. None of these systems need to know about the others, which makes the code easier to extend, test, and debug. When you add a new system that responds to player detection, you just connect it to the existing signal without modifying the detection code.

Built-in nodes emit their own signals that you connect to. A Timer emits timeout, an Area2D emits body_entered and body_exited, an AnimationPlayer emits animation_finished, and a Button emits pressed. Learning the signals available on each node type is one of the fastest ways to become productive in Godot, because the engine already broadcasts most of the events your game logic needs.

Work with Collections

Arrays in GDScript are dynamic, ordered lists that support indexing, slicing, iteration, and a rich set of built-in methods. You can append(), remove_at(), find(), sort(), filter(), map(), and reduce() arrays. The functional methods like filter and map accept callables, which are references to functions: var alive_enemies = enemies.filter(func(e): return e.health > 0).

Dictionaries are key-value stores that work like Python dictionaries or JavaScript objects. They are used extensively for configuration data, save game state, and passing structured data between systems. Access values with bracket notation data["key"] or dot notation data.key for string keys. The get() method provides a default value if the key does not exist, which prevents runtime errors when reading optional data.

For AI systems, arrays and dictionaries together form the data backbone. A patrol route is an Array[Vector2] of waypoints. An enemy registry is a Dictionary mapping enemy IDs to their nodes. A behavior tree's blackboard is a Dictionary storing shared state. Typed arrays add safety by ensuring you never accidentally add a wrong node type to a collection, which matters when iterating over entities and calling methods that only exist on specific types.

Implement State Logic with Match

The match statement is GDScript's pattern matching feature, similar to switch-case in C-family languages but with more expressive patterns. Combined with enums, it produces clean state machine code that is easy to read and extend.

A typical pattern defines an enum for states and uses match in _physics_process to dispatch behavior:

The match statement supports literal values, variable binding, array patterns, dictionary patterns, and guard expressions. For AI state machines, literal matching against enum values covers most needs. Each branch contains the logic for that state, and transitions happen by changing the state variable. The match statement evaluates from top to bottom and executes only the first matching branch, so ordering can matter when using complex patterns.

For larger state machines, consider moving each state's logic into its own function and using match only for dispatch: State.PATROL: _do_patrol(delta). This keeps the match block concise and each state's implementation self-contained. When state entry and exit logic is needed (like starting an animation when entering chase mode), add a state transition function that handles cleanup and setup between states.

Write Coroutines with Await

The await keyword pauses a function until a signal is emitted or a coroutine completes, then resumes execution on the next line. This lets you write sequential logic that spans multiple frames without manually tracking state variables or using timers with callbacks.

A simple example: await get_tree().create_timer(2.0).timeout pauses the function for two seconds. During that time, the rest of the game continues running normally. When the timer fires, execution resumes exactly where it left off. This is invaluable for scripted sequences, cutscenes, and AI behaviors that have timed steps.

For AI, coroutines simplify patterns like attack sequences where an enemy winds up, strikes, then recovers with specific timing. Without coroutines, you would need a state variable to track which phase of the attack you are in, a timer for each phase, and callback functions to handle transitions. With await, the entire sequence reads as linear code: play the windup animation, await the animation_finished signal, apply damage, play recovery, await another signal, then return to the decision-making state.

Be aware that coroutines can create subtle bugs if the node is freed while a coroutine is suspended. Always check is_instance_valid(self) after an await if there is any chance the node could be removed from the scene during the wait. This is a common gotcha in AI code where enemies can be killed while mid-behavior.

Key Takeaway

GDScript's tight integration with Godot's node system, combined with type safety, signals, pattern matching, and coroutines, makes it a purpose-built tool for game logic that covers the full range from simple movement scripts to complex AI behavior systems.