Players started reporting that our game was freezing during boss fights. No crash logs. No Sentry reports. Just "Not Responding" and then... nothing.
The weird part? It was happening on RTX 4090s while working fine on older hardware. Yeah.
My brother and I run Lost Rabbit Digital together, and we've been building Starbrew Station (a space coffee shop idle game, it's a whole thing) in Godot 4.5. Here's what we found and how we fixed it.
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()
It's not. That load() call freezes everything for 100-500ms 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
Same thing for shader materials. Don't create them at runtime, create templates at startup and duplicate them.
2. Awaits That Wait Forever
func _on_achievement_unlocked():
await EventBus.game_saved
show_notification()
If the save fails and never emits that signal? This waits forever. The game just 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. We never killed the old ones. After a few hours of gameplay, thousands of dead tween references just sitting 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 one 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 bigger operations (like applying buffs to 100+ units at once), chunk it across frames:
func apply_cascade_inspiration(units: Array):
var index = 0
while index < units.size():
for i in range(10): # 10 per frame
if 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. Fire and forget instead.
- Track your tweens. Kill old ones before creating new ones.
- Put limits on loops. Especially ones processing dynamic arrays.
- Chunk big operations across frames. Your players will thank you.
The kicker: high-end PCs hit these bugs faster because they ran 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.
Top comments (1)
Great debugging skills, can't imagine how annoying to track down when you can't reproduce it.