백그라운드 로딩

(새로운 레벨로 가는 것과 같이) 메인 씬을 교체하려 할 때, 로딩 진행상황을 알려주는 화면을 보여주고 싶을 때가 있을 겁니다. 메인 로드 메소드(ResourceLoader::load 또는 그냥 GDScript에서 load 함수)는 스레드를 블로킹하기 때문에 리소스가 불러와지기 전까지는 게임이 멈추고 반응하지 않는 것처럼 보일 것입니다. 이 문서에서는 대신 ResourceInteractiveLoader 클래스를 사용하여 보다 부드러운 로딩 화면을 만드는 법에 대해 이야기할 것입니다.

ResourceInteractiveLoader

ResourceInteractiveLoader 클래스는 리소스를 여러 단계에 걸쳐 로드할 수 있도록 합니다. poll 함수가 호출될 때마다 새로운 스테이지가 로드되며 호출한 함수에게로 제어가 반환됩니다. 각 스테이지는 일반적으로 메인 리소스에 의해 로드되는 서브 리소스입니다. 예를 들어 여러분이 10개의 이미지를 로드하는 씬 하나를 불러온다면, 각 이미지가 하나의 스테이지가 될 것입니다.

사용례

대개 아래와 같이 사용합니다

ResourceInteractiveLoader 가져오기

Ref<ResourceInteractiveLoader> ResourceLoader::load_interactive(String p_path);

이 함수에서 반환된 ResourceInteractiveLoader를 사용하여 로딩 작업을 관리할 수 있습니다.

폴링(Polling)

Error ResourceInteractiveLoader::poll();

이 함수로 로딩을 한단계 더 진행시킵니다. poll 의 각 호출마다 여러분의 리소스의 다음 스테이지를 로드합니다. 각 스테이지는 이미지 하나 또는 메쉬 하나와 같은 하나의 "원자적"인 리소스임을 명심하시기 바랍니다. 그렇기 때문에 로딩에 몇 프레임을 사용할 수 있습니다.

에러가 없으면 OK 가 반환되며, 로딩이 끝나면 ERR_FILE_EOF 가 반환됩니다. 그 외의 다른 리턴값은 에러가 발생했으며 로딩이 멈췄음을 의미합니다.

로딩 진행도(선택사항)

현재 로딩 상황을 알아보려면 다음 함수들을 사용해 보세요:

int ResourceInteractiveLoader::get_stage_count() const;
int ResourceInteractiveLoader::get_stage() const;

get_stage_count 는 로딩에 필요한 전체 스테이지 수를 반환합니다. get_stage 는 현재 불러오는 중이 스테이지를 반환합니다.

강제로 완료시키기 (선택사항)

Error ResourceInteractiveLoader::wait();

전체 리소스를 추가 스텝 없이 현재 프레임에 전부 불러오려면 이 함수를 사용합니다.

리소스 가져오기

Ref<Resource> ResourceInteractiveLoader::get_resource();

모든 일들이 잘 진행됐다면 아래 함수로 불러온 리소스를 가져올 수 있습니다.

예제

이 예제서는 새로운 씬을 로드하는 방법을 보여줍니다. 싱글톤(오토로드)(Singletons(AutoLoad)) 예제의 문맥에서 봐 주시기 바랍니다.

우선 몇몇 변수들을 설정하고 current_scene 을 게임의 메인 씬으로 초기화합니다:

var loader
var wait_frames
var time_max = 100 # msec
var current_scene


func _ready():
    var root = get_tree().get_root()
    current_scene = root.get_child(root.get_child_count() -1)

goto_scene 함수는 씬이 바뀌어야 할 때 게임에서 호출됩니다. 이 함수는 인터랙티브 로더를 요청하고, set_process(true) 를 호출하여 _progress 콜백에서 로더를 폴링하기 시작합니다. 또한 "loading" 애니메이션을 시작하여 프로그래스 바나 로딩 화면을 보여줄 수 있습니다.

func goto_scene(path): # Game requests to switch to this scene.
    loader = ResourceLoader.load_interactive(path)
    if loader == null: # Check for errors.
        show_error()
        return
    set_process(true)

    current_scene.queue_free() # Get rid of the old scene.

    # Start your "loading..." animation.
    get_node("animation").play("loading")

    wait_frames = 1

_process 에서 로더를 폴링합니다. poll 을 호출하고 나서, 리턴값을 이용해야 합니다. OK 가 리턴되면 폴링을 계속해야 하고, ERR_FILE_EOF 는 로딩이 끝났으며, 다른 리턴값은 에러가 있음을 의미합니다. 또한 로딩 화면을 띄워주기 위해 (goto_scene 함수에서 설정한 wait_frames 변수를 통해) 한 프레임을 건너뛰는 점을 참고하시기 바랍니다.

여기서 어떻게 OS.get_ticks_msec 를 사용하여 얼마나 이 스레드를 블로킹할지 참고하시기 바랍니다. 어떤 스테이지는 빨리 로드될 수 있기 때문에 한 프레임에 ``poll``을 한번 이상 호출할 수 있을지 모릅니다. 또 어떤 경우는 ``time_max``보다 오랜 시간이 걸릴 수도 있습니다. 따라서 타이밍에 대해서는 정밀한 제어를 할 수 없다는 점을 염두하시기 바랍니다.

func _process(time):
    if loader == null:
        # no need to process anymore
        set_process(false)
        return

    # Wait for frames to let the "loading" animation show up.
    if wait_frames > 0:
        wait_frames -= 1
        return

    var t = OS.get_ticks_msec()
    # Use "time_max" to control for how long we block this thread.
    while OS.get_ticks_msec() < t + time_max:
        # Poll your loader.
        var err = loader.poll()

        if err == ERR_FILE_EOF: # Finished loading.
            var resource = loader.get_resource()
            loader = null
            set_new_scene(resource)
            break
        elif err == OK:
            update_progress()
        else: # Error during loading.
            show_error()
            loader = null
            break

몇몇 추가 도우미 함수가 있습니다. update_progress 함수는 프로그레스 바를 갱신하거나 일시정지된 애니메이션을 업데이트할 수 있습니다(애니메이션은 처음부터 끝까지의 로딩 진행도를 대표합니다). set_new_scene 함수가 새 씬을 트리에 배치합니다. 씬을 로드한 것이기 때문에 로더에서 가져온 리소스에 instance() 를 호출할 필요가 있습니다.

func update_progress():
    var progress = float(loader.get_stage()) / loader.get_stage_count()
    # Update your progress bar?
    get_node("progress").set_progress(progress)

    # ...or update a progress animation?
    var length = get_node("animation").get_current_animation_length()

    # Call this on a paused animation. Use "true" as the second argument to
    # force the animation to update.
    get_node("animation").seek(progress * length, true)


func set_new_scene(scene_resource):
    current_scene = scene_resource.instance()
    get_node("/root").add_child(current_scene)

멀티스레드 사용하기

ResourceInteractiveLoader은 여러 스레드에서 사용할 수 있습니다. 사용해보기에 앞서 생각해봐야 할 것들이 있습니다:

세마포어 사용

지금 스레드가 메인 스레드에서 새 리소스를 요청하는 동안 (busy loop나 비슷한 걸 하는 대신) sleep하기 위하여 Semaphore 를 사용하세요.

폴링하는 동안에는 메인 스레드 블로킹하지 않기

메인 스레드에서 로더 클래스를 호출할 때 사용하는 뮤텍스가 있다면 로더 클래스에서 poll 하는 동안에는 메인 스레드를 잠그지 않아야 합니다. 리소스 로딩이 끝나면 (VisualServer과 같은) 로우레벨 API에서 일부 리소스를 필요로 할 수도 있습니다. 이 API들은 리소스를 가져오기 위해 메인 스레드를 잠글 필요가 있을 수도 있습니다. 이는 여러분의 스레드가 리소스 로드를 기다리는 동안 메인 스레드가 여러분의 뮤텍스를 기다리고 있으므로 데드락을 일으킬 수 있습니다.

예제 클래스

스레드 내에서 리소스 로딩을 하는 예제 클래스 예제가 있습니다: resource_queue.gd. 아래와 같이 사용하면 됩니다:

func start()

스레드를 시작하기 위해 클래스를 인스턴싱한 다음 함수를 호출하세요.

func queue_resource(path, p_in_front = false)

리소스를 큐에 넣습니다. 선택 인자인 "p_in_front"를 사용하여 큐의 앞부분에 넣을 수 있습니다.

func cancel_resource(path)

큐에서 리소스를 제거합니다. 로딩이 끝난 리소스를 폐기합니다.

func is_ready(path)

리소스 전체가 로드되었으며 가져올 준비가 된 경우 true 를 반환합니다.

func get_progress(path)

리소스 로딩 진행도를 가져옵니다. (리소스가 큐에 없는 경우와 같이)에러가 있는 경우 -1을 반환하고 아닌 경우 0.0에서 1.0 사이의 진행도 숫자를 반환합니다. 대개 (프로그레스 바를 갱신하는 것과 같은) 보여주기 위한 용도로 사용됩니다. 리소스를 실제로 사용할 수 있는지 확인하려면 is_ready 를 사용하세요.

func get_resource(path)

로드가 완료된 리소스를 가져오거나, 에러가 있으면 null``을 반환합니다. 만약 리소스가 전부 로드되지 않았다면(``is_readyfalse``인 경우), 스레드를 블로킹하며 로드가 끝날 때까지 기다립니다. 만약 리소스가 큐에 없다면 평소처럼 ``ResourceLoader::load 를 호출하고 그 결과를 반환합니다.

예시:

# Initialize.
queue = preload("res://resource_queue.gd").new()
queue.start()

# Suppose your game starts with a 10 second cutscene, during which the user
# can't interact with the game.
# For that time, we know they won't use the pause menu, so we can queue it
# to load during the cutscene:
queue.queue_resource("res://pause_menu.tres")
start_cutscene()

# Later, when the user presses the pause button for the first time:
pause_menu = queue.get_resource("res://pause_menu.tres").instance()
pause_menu.show()

# When you need a new scene:
queue.queue_resource("res://level_1.tscn", true)
# Use "true" as the second argument to put it at the front of the queue,
# pausing the load of any other resource.

# To check progress.
if queue.is_ready("res://level_1.tscn"):
    show_new_level(queue.get_resource("res://level_1.tscn"))
else:
    update_progress(queue.get_progress("res://level_1.tscn"))

# When the user walks away from the trigger zone in your Metroidvania game:
queue.cancel_resource("res://zone_2.tscn")

참고: 이 형태의 코드는 실제 환경에서 테스트되지 않았습니다. 여기에 문제가 발생했다면, 고도 커뮤니티 채널 중 하나에 도움을 요청해 보세요.