우리 Godot 게임은 비싼 PC에서만 충돌했습니다 (이유는 여기)

발행: (2026년 1월 17일 오전 06:02 GMT+9)
5 min read
원문: Dev.to

Source: Dev.to

실제 문제들

우리는 네 가지 별개의 이슈가 동시에 작용해 상황을 악화시키고 있다는 것을 찾아냈습니다.

1. 게임 플레이 중 리소스 로딩

아래 코드는 겉보기엔 무해해 보입니다:

func start_boss_fight():
    var boss_scene = load("res://scenes/InspectionBossFight.tscn")
    var boss = boss_scene.instantiate()

load() 호출은 디스크에서 읽는 동안 100‑500 ms 동안 모든 것을 멈추게 합니다. 여기에 셰이더 컴파일을 더하면 Windows TDR(Timeout Detection and Recovery)에 걸리게 됩니다. Windows는 GPU 드라이버가 ~2 초 안에 응답하지 않으면 프로세스를 강제로 종료합니다. 크래시 리포트는 없고, 그냥 죽은 상태가 됩니다.

해결책: 시작 시점에 미리 로드(preload)합니다.

const InspectionBossFightScene = preload("res://scenes/InspectionBossFight.tscn")

func start_boss_fight():
    var boss = InspectionBossFightScene.instantiate()  # 즉시 생성

셰이더 머티리얼도 마찬가지로, 시작 시에 템플릿을 만들고 런타임에 복제하도록 합니다.


2. 영원히 기다리는 await

func _on_achievement_unlocked():
    await EventBus.game_saved
    show_notification()

저장이 실패해 해당 시그널이 절대 발생하지 않으면 코루틴이 영원히 대기하게 되고 게임이 멈춥니다.

해결책: Fire‑and‑forget 방식 사용.

func _on_achievement_unlocked():
    EventBus.request_save.emit()
    show_notification()  # 확인을 기다리지 않음

3. 쌓여가는 트윈(Tween)

UI 애니메이션마다 새로운 트윈을 만들었고, 이전 트윈을 전혀 정리하지 않았습니다. 몇 시간 플레이 후에는 수천 개의 죽은 트윈 레퍼런스가 메모리에 남게 됩니다.

해결책: 트윈을 추적하고 새로 만들기 전에 기존 것을 종료합니다.

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. 안전 제한이 없는 루프

우리의 전투기 정리 루프는 한 프레임에 수천 개의 잘못된 엔트리를 처리하려 했습니다:

func cleanup_fighters():
    for i in range(fighters.size() - 1, -1, -1):
        if not is_instance_valid(fighters[i]):
            fighters.remove_at(i)

해결책: 반복 횟수에 제한을 둡니다.

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)

더 큰 작업(예: 100명 이상의 유닛에게 동시에 버프 적용)에서는 프레임마다 작업을 나눠 처리합니다:

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)하세요. Windows에서 런타임 load()는 GPU 드라이버 타임아웃을 유발할 수 있습니다.
  • 절대 발생하지 않을 수도 있는 시그널을 await 하지 마세요. 상황에 맞게 fire‑and‑forget 패턴을 사용하세요.
  • 트윈을 추적하세요. 새 트윈을 만들기 전에 기존 트윈을 종료합니다.
  • 루프에 제한을 두세요. 특히 동적 배열을 처리하는 경우에 중요합니다.
  • 큰 작업은 프레임 단위로 나눠 수행하세요. 메인 스레드가 응답성을 유지할 수 있습니다.

핵심 포인트: 고성능 PC일수록 이 버그들을 더 빨리 발견합니다. 프레임당 루프가 더 많이 실행되기 때문이죠. 최고의 하드웨어가 최악의 문제를 찾아내는 경우가 종종 있습니다.

우리는 Lost Rabbit Digital이며, GitHub에서 활동하고 있습니다. Starbrew Station은 Godot 4.5로 제작되었습니다.

Back to Blog

관련 글

더 보기 »

Python: Tprof, 타깃팅 프로파일러

Python: tprof, 타깃 프로파일러 소개. 프로파일러는 전체 프로그램의 성능을 측정하여 대부분의 시간이 어디에 소비되는지 식별합니다. 하지만 일단 당신이…