Our Godot Game Only Crashed on Expensive PCs (Here's Why)
Source: Dev.to
The Actual Problems
We tracked it down to four separate issues that were all making things worse together.
1. Loading Resources During Gameplay
This looks harmless:
func start_boss_fight():
var boss_scene = load("res://scenes/InspectionBossFight.tscn")
var boss = boss_scene.instantiate()
That load() call freezes everything for 100‑500 ms while it reads from disk. Combine that with shader compilation and you hit Windows TDR (Timeout Detection and Recovery). Windows kills any process that doesn’t respond to the GPU driver within ~2 seconds. No crash report, just dead.
Fix: Preload at startup instead.
const InspectionBossFightScene = preload("res://scenes/InspectionBossFight.tscn")
func start_boss_fight():
var boss = InspectionBossFightScene.instantiate() # instant
Do the same for shader materials: create templates at startup and duplicate them instead of creating them at runtime.
2. Awaits That Wait Forever
func _on_achievement_unlocked():
await EventBus.game_saved
show_notification()
If the save fails and never emits that signal, the coroutine waits forever and the game hangs.
Fix: Fire and forget.
func _on_achievement_unlocked():
EventBus.request_save.emit()
show_notification() # don't wait for confirmation
3. Tweens Piling Up
Every UI animation created a new tween, and we never killed the old ones. After a few hours of gameplay, thousands of dead tween references sat in memory.
Fix: Track them and kill the old one before making a new one.
var _active_tweens: Dictionary = {}
func fade_ambient_light():
if _active_tweens.has("ambient_fade") and _active_tweens["ambient_fade"].is_valid():
_active_tweens["ambient_fade"].kill()
var tween = create_tween()
_active_tweens["ambient_fade"] = tween
tween.tween_property(light, "energy", 0.5, 1.0)
4. Loops With No Safety Limits
Our fighter‑cleanup loop could churn through thousands of invalid entries in a single frame:
func cleanup_fighters():
for i in range(fighters.size() - 1, -1, -1):
if not is_instance_valid(fighters[i]):
fighters.remove_at(i)
Fix: Add iteration limits.
const MAX_ITERATIONS_PER_FRAME = 100
func cleanup_fighters():
var iterations = 0
for i in range(fighters.size() - 1, -1, -1):
iterations += 1
if iterations > MAX_ITERATIONS_PER_FRAME:
break
if not is_instance_valid(fighters[i]):
fighters.remove_at(i)
For larger operations (e.g., applying buffs to 100+ units at once), chunk the work across frames:
func apply_cascade_inspiration(units: Array):
var index = 0
while index = units.size():
break
units[index].apply_inspiration()
index += 1
await get_tree().process_frame
TL;DR
- Preload resources at startup. Runtime
load()on Windows can trigger GPU driver timeouts. - Don’t await signals that might never fire. Use fire‑and‑forget patterns when appropriate.
- Track your tweens. Kill old ones before creating new ones.
- Put limits on loops. Especially those processing dynamic arrays.
- Chunk big operations across frames. This keeps the main thread responsive.
The kicker: high‑end PCs hit these bugs faster because they run more game loops per second. Sometimes the best hardware finds the worst problems.
We’re Lost Rabbit Digital on GitHub. Starbrew Station is built with Godot 4.5.