Coding Quests
The Scroll Library
Guides

5 GDScript Mistakes Every Beginner Makes (and How to Fix Them)

February 12, 2026Updated June 11, 20265 min read

You've picked up Godot, written a few scripts, and things mostly work. Until they don't. The errors are cryptic, your character moves at the wrong speed on someone else's machine, signals fire into the void, and you have no idea what you broke.

Every Godot developer has been here. I made all five of these mistakes myself. Once you understand why each one happens, you stop making it, so that's the format: the symptom, the why, the fix.

1. Forgetting to Multiply by Delta

This is the classic. You write movement code like this:

GDScript
func _process(_delta: float) -> void:
position.x += speed

It works on your machine. Then a friend tests the build and the character moves at a completely different speed. Why?

_process runs once per frame. On a 120 FPS machine that code runs 120 times per second. On a 60 FPS laptop, only 60 times. Same code, half the movement.

The fix is multiplying by delta, the time elapsed since the last frame. That makes movement frame-rate independent:

GDScript
func _process(delta: float) -> void:
position.x += speed * delta

Now speed means units per second no matter what hardware runs the game. The same rule applies to anything time-based: movement, rotation, timers, animations.

One more thing: for physics-related movement (characters, projectiles), use _physics_process instead of _process. It runs at a fixed rate, 60 times per second by default, and plays much nicer with Godot's physics engine.

2. Using get_node With Hardcoded Paths

Beginners love get_node. It's simple, it works, and it breaks the moment you rename anything.

GDScript
# Fragile - breaks if you rename or move the node
var health_bar = get_node("../UI/HealthBar")

Restructure your scene tree (you will, probably this week) and every hardcoded path either crashes at runtime or fails silently. The silent version is worse.

Use @onready with $ for nodes inside the same scene, and @export for references that might move around:

GDScript
# For nodes in the same scene - short and clean
@onready var health_bar: ProgressBar = $UI/HealthBar
# For references that might change - drag-and-drop in the Inspector
@export var health_bar: ProgressBar

@export is the one I push hardest. You drag the node into the Inspector field once, and the reference survives any amount of scene reshuffling.

3. Connecting Signals in Code but Forgetting to Disconnect

You connect a signal in _ready:

GDScript
func _ready() -> void:
$Button.pressed.connect(_on_button_pressed)

Seems fine. But if this node gets freed and recreated (menus, level transitions, object pooling), you can end up connecting the same signal twice. Or worse, the old connection tries to call a method on a freed object, and that crash is miserable to trace back.

For signals between nodes in the same scene, connect them in the editor instead (the Node tab, then Signals). Godot manages those for you. For connections you have to make in code, disconnect in _exit_tree:

GDScript
func _ready() -> void:
$Button.pressed.connect(_on_button_pressed)
func _exit_tree() -> void:
if $Button.pressed.is_connected(_on_button_pressed):
$Button.pressed.disconnect(_on_button_pressed)

Or use the CONNECT_ONE_SHOT flag if you only need the signal to fire once:

GDScript
$Button.pressed.connect(_on_button_pressed, CONNECT_ONE_SHOT)

Signals are how Godot scenes talk to each other, so they're worth understanding properly. If they still feel like magic, the Godot 4 signals tutorial goes deeper.

4. Putting Everything in One Giant Script

Your player script starts innocent. Movement. Then you add health. Then inventory, combat, dialogue, save and load. Six weeks later Player.gd is 800 lines, does everything, and you're scared to touch any of it.

Break it into components. Godot's node system is designed for this. Instead of one script, give the player child nodes that each own one job:

CharacterBody3D (Player)
├── HealthComponent      → handles HP, damage, death
├── InventoryComponent   → handles items, equipment
├── CombatComponent      → handles attacks, hitboxes
└── StateMachine         → handles state transitions

Each component is a small, focused script. They talk through signals, you can test them on their own, reuse them on enemies, and debug without scrolling through hundreds of lines. Here's what one looks like:

GDScript
# HealthComponent.gd - clean, focused, reusable
class_name HealthComponent
extends Node
signal health_changed(new_health: int)
signal died
@export var max_health := 100
var health: int
func _ready() -> void:
health = max_health
func take_damage(amount: int) -> void:
health = max(health - amount, 0)
health_changed.emit(health)
if health == 0:
died.emit()

This pattern scales. A weekend jam game and a 20-hour RPG both work with the same shape. And that StateMachine node deserves its own article, which it has: the Godot 4 state machine tutorial.

5. Not Using Type Hints

GDScript is dynamically typed by default, so this runs without complaint:

GDScript
var speed = 5
var name = "Player"
var items = []

But it means Godot can't catch type errors until runtime, autocomplete gives up on you, and you'll burn an evening on a bug the editor could have flagged the moment you typed it.

Type hints cost a few extra characters:

GDScript
var speed: float = 5.0
var player_name: String = "Player"
var items: Array[Item] = []
func take_damage(amount: int) -> void:
health -= amount
func get_speed() -> float:
return speed * speed_multiplier

And they pay you back daily. The editor flags type errors before you run the game. Autocomplete actually works, because it knows items holds Item objects. Function signatures document themselves, so you can read what something expects without guessing. Typed code can even run slightly faster in some cases, since Godot can optimize it.

Get the habit early. Future you says thanks.

The Pattern Behind All Five Mistakes

Look at the list again. Every one of these is a shortcut that works right now and breaks later:

  • No delta → breaks on different hardware
  • Hardcoded paths → breaks when you refactor
  • Unmanaged signals → breaks when nodes are freed
  • God scripts → breaks when complexity grows
  • No type hints → breaks when you debug

Good GDScript isn't clever. It's code that still works in three weeks, after you've forgotten how it works.

The fastest way to make these habits stick is building a real system with them baked in from line one. The inventory system quest is free and uses everything on this list: components, signals, typed code, the lot. Start the Quest before your project gets big enough to punish the shortcuts.

FAQ

What's the most common beginner mistake in Godot?

Forgetting delta, by a wide margin. It hides during development because your own frame rate is stable, then shows up the moment anyone else runs the game. If movement speed feels different across machines, check delta first.

Should I use _process or _physics_process for movement?

_physics_process for anything that touches the physics engine: CharacterBody movement, projectiles, collision checks. It runs at a fixed 60 ticks per second by default, so physics stays consistent. _process is for visuals and UI that should update every rendered frame.

Do I have to use type hints in GDScript?

No, GDScript runs fine without them. But they're free error checking, working autocomplete, and a small performance gain in some cases, so there's no good reason to skip them. Type everything new you write today and backfill old scripts as you touch them.

gdscriptbeginnertips

Stop reading. Start building.

Beat the demo boss by writing real Godot code, then build this for real in the Inventory System Quest.

Free, and no card needed. Built by a real person, with new quests every month.

Get the next Godot build in your inbox

New quests, project breakdowns, and game-dev tips. Free, no spam, unsubscribe anytime.

Written by Coding Quests

We teach Godot 4 by making you build complete systems: inventories, save systems, action roguelike controllers, enemy AI. The scrolls are free. The quests are where it sticks.