后台加载

当切换游戏的主场景时,例如进入一个新的关卡,你可能想显示一个加载屏幕,并显示一些正在进行的进度.主加载方法( ResourceLoader::load 或GDScript中的 load )会阻塞线程,使你的游戏在资源加载时显得冻结和无响应.本文档讨论了使用 ResourceInteractiveLoader 类来实现更平滑的加载屏幕的替代方法.

ResourceInteractiveLoader

ResourceInteractiveLoader 类允许在阶段中加载资源.每次调用 poll 方法时,都会加载一个新阶段,并将控制权返回给调用方.每个阶段通常是由主资源加载的子资源.例如,如果您正在加载一个场景,该场景加载10幅图像,那么每个图像将是一个阶段.

用法

一般用法如下

获取ResourceInteractiveLoader

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

此方法将向您提供一个ResourceInteractiveLoader,您将使用它来管理加载操作.

轮询

  1. Error ResourceInteractiveLoader::poll();

使用此方法可以推进加载的进度. 每次调用 poll 都会加载资源的下一个阶段. 请记住,每个阶段都是一个完整的``原子``资源,例如图像或网格,它往往需要几帧才能加载.

没有错误时返回 OK ,加载完成后返回 ERR_FILE_EOF . 返回其他任何值时表示存在错误并且已停止加载.

加载进度(可选)

要查询加载进度,请使用以下方法:

  1. int ResourceInteractiveLoader::get_stage_count() const;
  2. int ResourceInteractiveLoader::get_stage() const;

get_stage_count 返回要加载的阶段总数.``get_stage`` 返回当前正在加载的阶段.

强制完成(可选)

  1. Error ResourceInteractiveLoader::wait();

如果需要在当前帧中加载整个资源,请使用此方法,而无需执行别的步骤.

获取资源

  1. Ref<Resource> ResourceInteractiveLoader::get_resource();

如果一切顺利,请使用此方法检索已加载的资源.

示例

此示例演示如何加载新场景. 请结合 单例(自动加载) 示例来看.

首先,我们设置一些变量,用游戏的主场景初始化 current_scene :

  1. var loader
  2. var wait_frames
  3. var time_max = 100 # msec
  4. var current_scene
  5. func _ready():
  6. var root = get_tree().get_root()
  7. current_scene = root.get_child(root.get_child_count() -1)

当需要切换场景时,游戏中会调用函数 goto_scene .它请求一个交互式加载器,并调用 set_process(true) 开始在 _process 回调中轮询加载器.它还会启动一个 “加载” 动画,可以显示一个进度条或加载屏幕.

  1. func goto_scene(path): # Game requests to switch to this scene.
  2. loader = ResourceLoader.load_interactive(path)
  3. if loader == null: # Check for errors.
  4. show_error()
  5. return
  6. set_process(true)
  7. current_scene.queue_free() # Get rid of the old scene.
  8. # Start your "loading..." animation.
  9. get_node("animation").play("loading")
  10. wait_frames = 1

_process 中对加载进行轮询. poll 被调用,然后我们处理该调用的返回值. OK 表示继续轮询, ERR_FILE_EOF 表示加载完成,其他的表示有错误.另外注意我们跳过一帧(通过 wait_frames ,在 goto_scene 函数上设置),让加载画面显示.

注意我们是如何使用 OS.get_ticks_msec 来控制阻塞线程的时间.有些阶段可能加载得很快,这意味着我们可能会在一帧中塞进多个对 poll 的调用;有些阶段可能需要比 time_max 值更多,所以请记住我们无法精确控制时间.

  1. func _process(time):
  2. if loader == null:
  3. # no need to process anymore
  4. set_process(false)
  5. return
  6. # Wait for frames to let the "loading" animation show up.
  7. if wait_frames > 0:
  8. wait_frames -= 1
  9. return
  10. var t = OS.get_ticks_msec()
  11. # Use "time_max" to control for how long we block this thread.
  12. while OS.get_ticks_msec() < t + time_max:
  13. # Poll your loader.
  14. var err = loader.poll()
  15. if err == ERR_FILE_EOF: # Finished loading.
  16. var resource = loader.get_resource()
  17. loader = null
  18. set_new_scene(resource)
  19. break
  20. elif err == OK:
  21. update_progress()
  22. else: # Error during loading.
  23. show_error()
  24. loader = null
  25. break

一些额外的辅助函数. update_progress 更新进度条,或者也可以更新暂停的动画(动画从头到尾表示整个加载过程). set_new_scene 将新加载的场景放在树上. 因为它是一个被加载的场景,所以需要在从加载器获得的资源上调用 instance() .

  1. func update_progress():
  2. var progress = float(loader.get_stage()) / loader.get_stage_count()
  3. # Update your progress bar?
  4. get_node("progress").set_progress(progress)
  5. # ...or update a progress animation?
  6. var length = get_node("animation").get_current_animation_length()
  7. # Call this on a paused animation. Use "true" as the second argument to
  8. # force the animation to update.
  9. get_node("animation").seek(progress * length, true)
  10. func set_new_scene(scene_resource):
  11. current_scene = scene_resource.instance()
  12. get_node("/root").add_child(current_scene)

使用多个线程

多个线程可以用在ResourceInteractiveLoader中. 如果您尝试一下,请记住以下几点:

使用信号量

当你的线程等待主线程请求一个新的资源时,使用 Semaphore 来休眠,而不是一个繁忙的循环或类似的东西.

在轮询期间不阻塞主线程

如果你有一个互斥锁,允许从主线程调用你的加载器类,当你在加载器类上调用 poll 时,不要锁定主线程.当一个资源加载完成后,它可能需要一些来自低级API(VisualServer等)的资源,这可能需要锁定主线程来获取这些资源.如果主线程在等待你的互斥锁,而你的线程却在等待加载资源,这可能会导致死锁.

示例类

您可以在这里找到一个用于在线程中加载资源的示例类: resource_queue.gd. 用法如下:

  1. func start()

在实例化类之后调用以启动线程.

  1. func queue_resource(path, p_in_front = false)

对于资源队列.使用可选的参数 “p_in_front” ,将其放在队列的前面.

  1. func cancel_resource(path)

从队列中删除资源,丢弃任何已完成的加载.

  1. func is_ready(path)

如果资源已完全加载并准备好被检索,则返回 true .

  1. func get_progress(path)

获取一个资源的进度.如果有错误,返回-1(例如,如果资源不在队列中),或者返回一个介于0.0和1.0之间的数字,表示加载的进度.主要用于预览的目的(更新进度条等),使用 is_ready 来了解资源是否真的准备完成.

  1. func get_resource(path)

返回完全加载的资源,或者错误时返回 null .如果资源没有完全加载( is_ready 返回 false ),它将阻塞你的线程并完成加载.如果资源不在队列中,它将调用 ResourceLoader::load 来正常加载并返回.

示例:

  1. # Initialize.
  2. queue = preload("res://resource_queue.gd").new()
  3. queue.start()
  4. # Suppose your game starts with a 10 second cutscene, during which the user
  5. # can't interact with the game.
  6. # For that time, we know they won't use the pause menu, so we can queue it
  7. # to load during the cutscene:
  8. queue.queue_resource("res://pause_menu.tres")
  9. start_cutscene()
  10. # Later, when the user presses the pause button for the first time:
  11. pause_menu = queue.get_resource("res://pause_menu.tres").instance()
  12. pause_menu.show()
  13. # When you need a new scene:
  14. queue.queue_resource("res://level_1.tscn", true)
  15. # Use "true" as the second argument to put it at the front of the queue,
  16. # pausing the load of any other resource.
  17. # To check progress.
  18. if queue.is_ready("res://level_1.tscn"):
  19. show_new_level(queue.get_resource("res://level_1.tscn"))
  20. else:
  21. update_progress(queue.get_progress("res://level_1.tscn"))
  22. # When the user walks away from the trigger zone in your Metroidvania game:
  23. queue.cancel_resource("res://zone_2.tscn")

: 这段代码目前的形式没有在实际的场景中进行测试.如果您遇到任何问题,请在 `Godot的社区频道<https://godotengine.org/community>`__ 中寻求帮助.