백그라운드 로딩¶
(새로운 레벨로 가는 것과 같이) 메인 씬을 교체하려 할 때, 로딩 진행상황을 알려주는 화면을 보여주고 싶을 때가 있을 겁니다. 메인 로드 메소드(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_ready
가 false``인 경우), 스레드를 블로킹하며 로드가 끝날 때까지 기다립니다. 만약 리소스가 큐에 없다면 평소처럼 ``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")
참고: 이 형태의 코드는 실제 환경에서 테스트되지 않았습니다. 여기에 문제가 발생했다면, 고도 커뮤니티 채널 중 하나에 도움을 요청해 보세요.