如果你已经跟着 Godot3D 新手入门全流程教程做到第 32 课,大概率正在面对这样一个问题:游戏场景越做越大,点击“开始游戏”之后,画面直接卡住,甚至白屏一两秒,然后才进入场景。这个教程就是要解决这个体验问题。先给结论:用 Godot 4 内置的 ResourceLoader 异步加载 API,把资源的读取和解析挪到后台线程,同时在界面上放一个加载过渡界面,用进度条告诉玩家“还在加载,不是死机”。
先说清楚一个容易踩的坑:这里的 Godot3D 指 Godot 引擎的 3D 开发流程,不是 Godot 3.x 版本。下面所有代码基于 Godot 4.x,推荐使用 4.2 或 4.3 稳定版。Godot 3.x 里没有ResourceLoader.load_threaded_*这套方法,如果打开项目后找不到对应 API,先确认引擎版本。
本文会带你完成四件事:搞懂 Godot 4 资源异步加载的三个 API 和状态机制;搭一个带进度条、遮罩层和状态文案的加载过渡界面;把加载流程封装成 Autoload 单例,以后每个场景切换都能复用;最后用 Profiler 验证切换过程是否真的不卡了。整个方案只用 GDScript 和引擎内置节点,不需要第三方插件。
1. Godot3D 资源异步加载核心能力速览
| 项目 | 说明 |
|---|---|
| 引擎版本 | Godot 4.x,推荐 4.2 / 4.3 稳定版 |
| 开发语言 | GDScript |
| 核心 API | ResourceLoader.load_threaded_request()、load_threaded_get_status()、load_threaded_get() |
| 解决痛点 | 场景切换时主线程被阻塞,表现为卡顿、白屏、长时间无响应 |
| 加载过渡界面 | CanvasLayer + ColorRect 遮罩 + ProgressBar,可扩展提示文案和过渡动画 |
| 批量能力 | 支持一次请求多个场景、模型、音频资源,并做聚合进度 |
| 缓存模式 | 支持复用缓存、替换缓存、忽略缓存三种模式 |
| 第三方插件 | 不需要,引擎内置 |
| 适合人群 | 已经能搭基础 3D 场景、需要解决关卡切换体验的入门开发者 |
这套方案不会改变你原本的场景管理思路。它只是把“切场景的那一刻才去读硬盘”改成“提前在后台把资源读进内存,界面同步显示进度”。核心思想就一句话:不卡主线程,让玩家看得见进度。
2. 为什么 Godot3D 切换场景会卡顿:同步加载的问题来源
新手阶段最容易忽略的是加载方式对帧率的影响。load()、preload()和SceneTree.change_scene_to_file()都是同步操作,也就是说,资源从磁盘读入、解压、反序列化、解析成引擎资源,整个过程都在主线程完成。在这个期间,Godot 的渲染循环、输入响应、动画更新全部暂停。场景小的时候感觉不到,场景一大,玩家就会看到画面停在上一帧,或者干脆一块黑屏。
3D 场景的资源开销比 2D 高得多。一个正常的 3D 关卡通常包含这些内容:
- 从外部导入的 GLB / OBJ 模型,每个模型携带大量顶点、UV、骨骼数据。
- 2K、4K 贴图和法线贴图,材质解析与纹理上传都需要时间。
- 环境光照、反射探针、WorldEnvironment 的环境球数据。
- 大量 MeshInstance3D 节点和物理碰撞体。
- GridMap、实例化场景、NPC、道具等子资源。
其中任意一项都可能在切换帧产生几百毫秒的阻塞。测试时你会发现,节点数量越多、贴图越大,白屏时间越长。这不是代码写错了,而是同步加载的天然缺陷。
异步加载的作用,是把“读取文件 + 解析资源”放到后台线程去执行,主线程继续跑游戏逻辑和渲染。这样加载期间玩家还能看到旧场景、看到加载动画,不会感觉程序死了。但要注意边界:异步加载解决的是资源读取和解析的阻塞,不是节点实例化的奇迹。如果一个场景有几万个节点,PackedScene.instantiate()本身在主线程执行,仍然可能产生一帧开销。这个问题后面第 7 节会专门讲。
3. 环境准备与测试场景搭建
开始写代码之前,先准备一个能明显看出卡顿的测试场景。如果没有一个足够重的目标场景,后面的进度条和加载界面都看不出效果。
环境要求:
- 安装 Godot 4.2 或 4.3 稳定版,新建项目,项目名随意,建议叫
AsyncLoadDemo。 - 不需要额外下载插件。
- 目标平台选 Desktop 即可,后面测试时在编辑器里直接运行。
- 项目里至少建好三个目录:
scenes/levels/、scenes/ui/、scripts/autoload/。
然后创建一个重型测试场景,操作步骤:
- 在
scenes/levels/下新建场景,根节点选 Node3D,保存为heavy_test.tscn。 - 添加一个 WorldEnvironment 节点,背景环境选一点偏暗的颜色,方便加载界面遮罩对比。
- 添加一个 DirectionalLight3D,旋转一下方向,让场景有明暗。
- 添加一个 MeshInstance3D,Mesh 属性选择内置的 BoxMesh 或 SphereMesh。
- 在场景树面板中选中这个 MeshInstance3D,按住 Ctrl+D 连续复制,复制 200 到 500 个。复制时可以随机调整每个节点的位置、旋转和缩放,让场景不那么整齐。操作慢的话也可以每复制一批就整体移动一下。
复制的目的是让.tscn文件包含足够多的节点数据,这样load()解析这个文件时需要处理大量文本数据,能产生可感知的加载时间。如果你的电脑性能很好,复制 500 个还是不卡,就继续把数量翻倍,或者在一个节点上加 2K 贴图材质。
再建一个主菜单场景main_menu.tscn,根节点选 Control,添加一个 Button,文本写“同步加载测试”,再添加一个 Button,文本写“异步加载测试”,最后加一个 Label 用于显示调试信息。这个菜单场景是给后面验证用的。
给同步按钮写一个基准测试脚本,挂在主菜单根节点上:
# main_menu.gd extends Control @onready var debug_label: Label = $DebugLabel func _on_start_sync_pressed() -> void: var t0 := Time.get_ticks_msec() var scene: PackedScene = load("res://scenes/levels/heavy_test.tscn") var cost: int = Time.get_ticks_msec() - t0 debug_label.text = "同步加载耗时: %d ms" % cost print("同步加载耗时: ", cost, " ms") get_tree().change_scene_to_packed(scene)先运行一次,点“同步加载测试”。如果画面停顿明显,说明测试场景够重,可以继续。如果停顿几乎感觉不到,就把场景节点数量再加一倍。
4. Godot4 资源异步加载 API 基础
Godot 4 的 ResourceLoader 提供了三个配合使用的方法,初学者最容易混的是它们的调用时机。
| 方法 | 作用 | 关键点 |
|---|---|---|
ResourceLoader.load_threaded_request(path, type_hint, use_sub_threads, cache_mode) | 发起后台加载 | 调用后立即返回,不阻塞主线程 |
ResourceLoader.load_threaded_get_status(path) | 查询加载状态和进度 | 返回[status, progress_array] |
ResourceLoader.load_threaded_get(path) | 取回加载完成的资源 | 状态为 DONE 后才能调用,否则会阻塞等待 |
其中load_threaded_get_status()返回的数组第一个元素是状态码,对应下面的常量:
| 状态常量 | 值 | 含义 |
|---|---|---|
ResourceLoader.THREAD_LOAD_IN_PROGRESS | 0 | 加载中 |
ResourceLoader.THREAD_LOAD_DONE | 1 | 加载完成 |
ResourceLoader.THREAD_LOAD_FAILED | 2 | 加载失败 |
ResourceLoader.THREAD_LOAD_INVALID | 3 | 路径无效或没有对应的加载请求 |
状态数组的第二个元素是进度数组,里面存放每个子资源的进度,范围 0 到 1。注意两个特殊情况:数组可能为空,表示当前阶段无法给出进度;数组里可能出现 -1,表示该阶段没法精确计算,比如 PackedScene 正在做最后的实例化准备。UI 处理进度时一定要容忍这两种情况,否则进度条会卡住或者直接显示负值。
下面是不带 UI 的最小异步加载模式,先用协程验证 API 能跑通:
# 在任意脚本的协程中调用 func load_scene_async(scene_path: String) -> void: if not ResourceLoader.exists(scene_path): printerr("场景文件不存在: ", scene_path) return ResourceLoader.load_threaded_request(scene_path) while true: var result: Array = ResourceLoader.load_threaded_get_status(scene_path) var status: int = result[0] if status == ResourceLoader.THREAD_LOAD_DONE: break elif status == ResourceLoader.THREAD_LOAD_FAILED: printerr("场景加载失败: ", scene_path) return await get_tree().process_frame var packed: PackedScene = ResourceLoader.load_threaded_get(scene_path) get_tree().change_scene_to_packed(packed)这个循环每个渲染帧查询一次状态,直到完成。因为load_threaded_get_status()本身不阻塞,所以循环期间游戏画面仍然在更新。注意不要把load_threaded_get()放到请求发起后立即调用,那个方法会在资源没就绪时等待,如果你在主函数里直接调用,效果等于同步加载,还是会卡。
load_threaded_request()的后两个参数初学者可以先不管:use_sub_threads表示是否允许子线程并行加载子资源,设为 true 时多文件场景加载更快,但进度反馈可能更粗糙;cache_mode默认使用 REUSE 模式,资源加载后会留在缓存中,第二次加载相同资源会快很多。后面第 7 节批量预加载时会用到。
5. 制作加载过渡界面:场景结构与脚本实现
API 能跑通之后,就把加载过程包装成一个可复用的过渡界面。这个界面要完成三件事:遮住旧场景避免穿帮、显示进度条和文案、在加载完成后自动切换到目标场景。
在scenes/ui/下新建场景,根节点选 CanvasLayer,命名LoadingScreen,保存为loading_screen.tscn。节点结构如下:
LoadingScreen (CanvasLayer) ├── Background (ColorRect) ├── CenterContainer (CenterContainer) │ └── VBoxContainer (VBoxContainer) │ ├── TitleLabel (Label) │ ├── ProgressBar (ProgressBar) │ ├── StatusLabel (Label) │ └── TipLabel (Label) └── loading_screen.gd搭建时注意几个关键点:
- CanvasLayer 的 Layer 属性建议设置为 100,确保它画在游戏所有普通 UI 之上。
- Background 的 ColorRect 锚点设置为全屏,颜色选深色,比如
#101010ff。 - ProgressBar 的 Min Value 设为 0,Max Value 设为 100,Step 设为 1。
- TitleLabel 写固定标题,StatusLabel 用来显示“加载中… 45%”这样的实时状态,TipLabel 显示每关切换时的提示文案。
CanvasLayer 加 ColorRect 的好处是它不依赖当前场景的 UI 布局,即使游戏里已经有自己的 CanvasLayer,只要层数足够高就不会被遮挡。
在根节点上挂loading_screen.gd:
extends CanvasLayer ## 加载过渡界面:驱动 ResourceLoader 异步加载并显示进度 signal loading_finished @onready var progress_bar: ProgressBar = $CenterContainer/VBoxContainer/ProgressBar @onready var status_label: Label = $CenterContainer/VBoxContainer/StatusLabel @onready var tip_label: Label = $CenterContainer/VBoxContainer/TipLabel var target_path := "" var _is_loading := false func _ready() -> void: set_process(false) func start_loading(path: String) -> void: target_path = path _is_loading = true progress_bar.value = 0 status_label.text = "正在准备资源..." tip_label.text = "请稍候,大场景需要一些时间" ResourceLoader.load_threaded_request(target_path) set_process(true) func _process(_delta: float) -> void: if not _is_loading: return var result: Array = ResourceLoader.load_threaded_get_status(target_path) var status: int = result[0] var progress: Array = result[1] if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS: progress_bar.value = _calc_progress(progress) status_label.text = "加载中... %d%%" % int(progress_bar.value) elif status == ResourceLoader.THREAD_LOAD_DONE: _finish_loading() elif status == ResourceLoader.THREAD_LOAD_FAILED: _fail_loading() func _calc_progress(progress: Array) -> float: if progress.is_empty(): return progress_bar.value var total := 0.0 var valid_count := 0 for p in progress: var v: float = p if v >= 0.0: total += v valid_count += 1 if valid_count == 0: return progress_bar.value return clampf(total / valid_count * 100.0, 0.0, 100.0) func _finish_loading() -> void: _is_loading = false set_process(false) status_label.text = "加载完成,正在进入..." var packed: PackedScene = ResourceLoader.load_threaded_get(target_path) get_tree().change_scene_to_packed(packed) loading_finished.emit() func _fail_loading() -> void: _is_loading = false set_process(false) status_label.text = "加载失败" tip_label.text = "请检查资源路径,或确认文件导入是否成功"脚本逻辑很直白:start_loading()发起后台加载并开启_process轮询;_process每次读取状态和进度;进度数组为空或全是 -1 时,进度条保持当前位置,不做除以零或显示负值;状态变为 DONE 后取回 PackedScene,执行场景切换,然后通过信号通知外部清理界面;状态为 FAILED 时显示错误文案。
这里有几个新手容易忽略的细节:
- 进度数组不是每个场景都返回平滑的 0 到 100。很多场景在最后的实例化准备阶段会返回 -1,此时进度条可能在 85% 左右停一下,然后直接跳到 100%。这是正常的。
loading_finished信号必须在change_scene_to_packed()之后发出,因为外部拿到信号后要清理加载界面,而此时旧场景已经被替换,界面应该在新场景之上继续显示一帧再消失。_calc_progress用所有有效子资源进度的平均值。如果你只需要一个粗略进度,也可以直接取progress[0]。不同场景、不同 Godot 小版本下数组长度不一样,建议先打印观察,再决定用哪种算法。
如果想要更柔和的效果,可以在_ready()里加一个淡入补间,让整个 CanvasLayer 的 modulate 透明度从 0 渐变到 1。加载完成后的淡出可以放在外部管理器里处理,避免和场景切换逻辑搅在一起。
6. 用 Autoload 封装场景加载管理器
加载界面直接挂在当前场景里有一个大坑:当change_scene_to_packed()执行时,旧场景会被释放,如果加载界面是旧场景的子节点,它会跟着一起消失,玩家什么都看不到。解决办法是把加载界面挂到 Autoload 单例节点下。Autoload 节点常驻场景树,不随场景切换销毁,加载界面自然就不会消失。
在scripts/autoload/下新建SceneLoader.gd:
extends Node ## SceneLoader 场景加载管理器:挂在 Autoload 下使用 const LoadingScreenScene: PackedScene = preload("res://scenes/ui/loading_screen.tscn") var _loading_screen: CanvasLayer = null func change_scene(path: String) -> void: if _loading_screen != null: push_warning("当前已有加载界面在运行,忽略新的切换请求") return _loading_screen = LoadingScreenScene.instantiate() add_child(_loading_screen) _loading_screen.loading_finished.connect(_on_loading_finished) _loading_screen.start_loading(path) func _on_loading_finished() -> void: await get_tree().process_frame if _loading_screen != null: _loading_screen.queue_free() _loading_screen = null然后打开 Project Settings,选择 Autoload 标签页,把SceneLoader.gd添加进来,名称保持SceneLoader。添加之后,任何脚本里都能直接写SceneLoader.change_scene("res://scenes/levels/heavy_test.tscn")。
回看第 3 节的主菜单,把异步按钮连接到:
func _on_start_async_pressed() -> void: SceneLoader.change_scene("res://scenes/levels/heavy_test.tscn")运行项目,对比两个按钮:同步加载会卡住整个游戏,然后突然进入重型场景;异步加载会先出现深色遮罩和进度条,进度跑满后无缝进入重型场景。加载期间即使按下键盘,角色逻辑也照常运行,因为没有一条代码阻塞主线程。
为什么这里要await get_tree().process_frame?因为change_scene_to_packed()对旧场景的释放发生在本帧结束,新场景的_ready()也要在这一帧才会执行。如果信号连接后立刻queue_free()加载界面,玩家可能看到新场景闪了一下遮罩就消失,不够平滑。等一帧再清理,体验更稳。
这套封装的扩展性很好。以后你的项目里想做“从主菜单进入关卡”“从关卡返回主菜单”“BOSS 战前加载战斗场景”,全部统一调用SceneLoader.change_scene(),不需要每个按钮自己写一遍加载逻辑。如果某个场景加载很快,希望直接切换不显示加载界面,可以在change_scene()里加一个可选参数show_loading: bool = true,为 false 时直接走同步切换。
再进一步,很多游戏在主菜单停留时就该预加载第一个关卡资源。SceneLoader 里可以追加两个批量方法:
var _preloaded: Dictionary = {} func preload_resources(paths: Array[String]) -> void: for p in paths: if not ResourceLoader.exists(p): push_warning("资源不存在,跳过: %s" % p) continue ResourceLoader.load_threaded_request(p) func wait_for_resources(paths: Array[String]) -> void: while true: var pending := false for p in paths: var st: int = ResourceLoader.load_threaded_get_status(p)[0] if st == ResourceLoader.THREAD_LOAD_FAILED: push_error("资源加载失败: %s" % p) return if st == ResourceLoader.THREAD_LOAD_IN_PROGRESS: pending = true break if not pending: break await get_tree().process_frame for p in paths: var res: Resource = ResourceLoader.load_threaded_get(p) _preloaded[p] = respreload_resources()负责发起批量请求,wait_for_resources()负责等待全部完成。主菜单进入空闲状态时请求关卡场景、公共模型、全局音频,等到玩家真正点击“开始游戏”时,目标资源已经在缓存里,加载界面会一闪而过甚至可以直接跳过。注意不要一次预加载太多大资源,否则内存占用会明显上涨。
7. 效果验证与性能观察方法
代码写完必须验证“真的不卡了”,不能只凭感觉。Godot 编辑器自带调试和性能分析工具,验证分三步。
第一步,回到主菜单,运行项目,点“同步加载测试”。观察画面是否出现明显停顿,并把第 3 节的同步耗时记录在 Label 上。如果这个值是几百毫秒,说明测试场景有效。
第二步,点“异步加载测试”。此时画面应该先出现加载界面,进度条平滑推进,旧场景没有卡死。注意,异步切换并不是完全没有开销。change_scene_to_packed()在切换当帧会释放旧场景、实例化新场景,如果重型场景有几千个节点,这一帧仍然可能出现一个小尖峰。这个尖峰属于实例化成本,不是资源读取成本。想进一步优化,就要把大关卡拆成多个 chunk 分批加载,或者用 MultiMesh 和 RenderingServer 减少节点数量。新手阶段判断标准很简单:加载期间的画面是否不再长时间空白。
第三步,用 Profiler 确认。在编辑器菜单打开 Debugger,切到 Profiler 标签,点击开始录制,然后去游戏里分别触发两种切换。录制结束后观察帧时间曲线:同步加载会有一个很尖的高峰,代表主线程长时间阻塞;异步加载整体曲线更平缓,只可能在场景切换帧出现一个小波动。如果你看到的是异步加载也出现一个巨大的尖峰,去检查这个尖峰是不是发生在加载完成后、实例化那一刻,而不是发生在加载中。
还可以用脚本打印状态和进度变化,确认进度数组的行为:
# 临时调试代码,放在 loading_screen.gd 的 _process 中 var result: Array = ResourceLoader.load_threaded_get_status(target_path) print(result) print("status = ", result[0], " progress = ", result[1])典型的输出可能是[0, [0.35, 0.72, -1.0]]或者[1, [-1.0]]。看到 -1 不要慌,那是阶段不可计算。你可以据此调整_calc_progress()的算法,比如只统计大于等于 0 的项。
内存方面,Godot 的 Debugger 面板带有进程监视器,可以看到内存和 CPU 使用率。资源加载后如果不再使用,引擎会按引用计数自动释放。但如果你在 Autoload 里长期持有PackedScene或场景实例的引用,资源就不会被释放。所以像_preloaded这样的字典,用完要及时清空,避免切几个关卡后内存只升不降。
8. Godot3D 资源异步加载常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 进度条长时间不动 | 进度数组为空或全是 -1,实际仍在加载 | 在_process中打印result | 进度不可知时显示“加载中”动画,不依赖数字 |
load_threaded_get()报错 | 状态未 DONE 就取资源,或路径没有发起过请求 | 打印状态码 | 先轮询到 DONE 再调用 |
| 加载完成后切场景仍然卡顿 | 场景节点过多,instantiate()阻塞主线程 | Profiler 查看尖峰位置 | 分块加载、降低单场景节点数、使用 MultiMesh |
| 加载界面看不到或瞬间消失 | 界面挂在了旧场景下,切换时被释放 | 检查场景树 | 把界面挂到 Autoload 节点下 |
| 状态返回 THREAD_LOAD_FAILED | 路径错误、依赖资源缺失、文件导入失败 | 用ResourceLoader.exists()检查路径,查看.import文件 | 修正路径,重新导入资源 |
| 进度条先到 100% 再卡一会 | 最后阶段进度不可计算 | 观察 DONE 出现时机 | UI 文案改显示“正在进入场景” |
| 反复切场景后内存越来越高 | Autoload 或全局变量持有资源引用 | 进程监视器查看内存曲线 | 释放不需要的引用,清空预加载字典 |
开了use_sub_threads=true后进度变乱 | 子线程并行加载导致进度数组不稳定 | 对比不同参数下的输出 | 对进度要求高时保持false |
新手最常踩的是第三个问题:把实例化卡顿误当成加载卡顿。记住一个判断标准:如果缓冲界面出现时进度条能动、游戏窗口没有进入“无响应”状态,说明资源加载已经不再阻塞主线程。加载完成之后change_scene_to_packed()那一帧的少量开销,属于场景切换的固有成本,优化思路和资源加载不同。
另外一个容易被忽略的问题:ResourceLoader.load_threaded_request()只能针对磁盘上的资源路径调用,不能在请求后立刻修改、重命名或删除文件。做热更新、动态打包内容时,要保证加载期间资源文件保持不变。
9. Godot3D 加载过渡界面最佳实践与使用建议
把功能跑通之后,建议按下面这些原则把它做成更工程化的方案。
第一,不是所有切换都需要加载界面。小型 UI 场景、配置资源、几十 KB 的预制体直接load()就好。给每次切换都加异步加载和过渡动画,反而会让操作变得拖沓。判断标准是同步加载耗时是否超过一两帧。
第二,保留一套最小可运行配置。重场景的测试文件和主菜单按钮可以放在项目里长期保留,后续调整资源加载策略时,用它验证效果,不用每次重新搭测试场景。
第三,加载界面建议有最短展示时间。有些场景加载太快,进度条一闪就消失,玩家还没意识到“正在加载”,体验上反而突兀。可以在start_loading()里记录一个开始时间,即使加载已经完成,也强制展示至少 0.3 到 0.5 秒。
第四,失败路径要处理完整。加载失败时不要只改一行文字。比较好的做法是加载界面增加一个“重试”按钮,点击后重新调用start_loading(),同时提供一个“返回主菜单”的兜底入口。关卡文件损坏不能只靠玩家重启游戏解决。
第五,批量预加载时要控制资源规模。主菜单空闲时预加载第一个关卡是很好的实践,但不要贪多。一次请求几十个大型模型,内存峰值会很难看。建议预加载只在“即将大概率进入”的资源上,优先级低的内容留到正式加载界面里处理。
第六,场景切换后如果新场景需要播放开场动画,最好在loading_finished信号之后再用await延迟一帧启动,避免和场景实例化挤在同一帧。动画播放器和音效播放器都建议在_ready()中只做准备,由场景管理器统一触发。
第七,关于素材合规:3D 模型、贴图、音乐如果来自第三方资源网站,商用前确认授权协议。测试阶段随意,但发布游戏时素材授权问题容易带来风险。
第八,Autoload 节点的名称一旦确定,项目里就不要随意改名。SceneLoader.change_scene()这类全局调用在改名后全部会失效,编译期不会报错,但运行时会出现Parser Error或空引用,排查起来很费劲。
下一步可以做的几件事
现在你可以做这样一件验证:把主菜单的“开始游戏”按钮改成调用SceneLoader.change_scene("res://scenes/levels/heavy_test.tscn"),跑一次完整流程,观察加载界面是否出现、进度条是否平滑、切场景是否有白屏闪断。如果还有卡顿,先开 Profiler 定位尖峰出现在加载阶段还是实例化阶段,再决定是调整加载方式还是优化场景结构。
这套方案可以直接当作你项目里的场景切换基建。单个按钮不再需要关心加载逻辑,只要传给 SceneLoader 一个路径,剩下的由加载界面和 Autoload 管理器完成。后续再想深入,可以研究几个方向:把大型关卡拆成多个 chunk 做分批异步加载、通过CACHE_MODE_REPLACE实现热更新资源替换、用更复杂的过渡动画把加载界面做得更有设计感。整个教程系列里,解决场景切换体验是很实用的一环,建议收藏备用,做正式项目时回来直接抄这个加载管理器。