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:
func _process(_delta: float) -> void: position.x += speedIt 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:
func _process(delta: float) -> void: position.x += speed * deltaNow 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_processinstead 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.
# Fragile - breaks if you rename or move the nodevar 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:
# 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:
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:
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:
$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:
# HealthComponent.gd - clean, focused, reusableclass_name HealthComponentextends Nodesignal health_changed(new_health: int)signal died@export var max_health := 100var health: intfunc _ready() -> void: health = max_healthfunc 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:
var speed = 5var 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:
var speed: float = 5.0var player_name: String = "Player"var items: Array[Item] = []func take_damage(amount: int) -> void: health -= amountfunc get_speed() -> float: return speed * speed_multiplierAnd 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.


