拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Godot RTS启动流程设计:boot.tscn场景与Autoload时序解析

Godot RTS启动流程设计:boot.tscn场景与Autoload时序解析

1. 为什么启动流程值得单独拿出来讲

做过几个 Godot 项目之后你会发现,真正让人头疼的从来不是某个功能写不出来,而是项目跑起来之后,一堆系统互相踩脚:地图还没加载完,单位 AI 就开始寻路;UI 还没初始化,信号已经发过来了;存档系统读了一半,发现依赖的全局数据还是空的。这类问题在 RTS 里尤其明显,因为 RTS 天然就是多系统耦合——地图、单位、资源、建造、战斗、寻路、UI、音频,任何一个环节的时序错了,表现出来就是随机崩溃或者行为诡异。

boot.tscn这个命名本身就说明了一件事:作者没有把主场景直接设成游戏场景,而是单独做了一个启动场景。这个选择看起来只是多了一层,但它其实是整个项目可维护性的分水岭。我见过太多项目把Main.tscn直接设为主场景,前期跑得挺欢,等到要加加载界面、要处理存档恢复、要做平台差异化初始化的时候,代码就开始往_ready()里堆,最后变成一个几百行的意大利面。

这篇内容适合两类人:一类是刚接触 Godot、准备做 RTS 但不知道项目该怎么起手的开发者;另一类是做了一两个小项目,发现启动阶段总是出各种时序问题,想找一个更稳的架构方案的人。我会围绕boot.tscn这个入口,把 Godot RTS 项目的启动流程从头到尾捋一遍,包括场景树怎么组织、Autoload 怎么排、资源什么时候加载、信号怎么串、存档怎么接,以及我自己踩过的那些坑。

核心关键词会贯穿全文:boot.tscn、Godot、RTS、启动流程、场景。你不需要有很深的 Godot 经验,但至少要能看懂 GDScript 和场景树的基本概念。

2. 启动场景的整体设计与思路拆解

2.1 为什么不直接把游戏主场景设为主场景

Godot 的项目设置里有一个application/run/main_scene,很多人第一次做项目就是在这里选一个Game.tscn完事。小项目没问题,但 RTS 不行,原因有三个。

第一,RTS 的初始化是分阶段的。地图数据、单位配置表、科技树、AI 行为参数这些东西,有些需要从文件读,有些需要从服务器拉,有些需要根据玩家设置动态生成。如果主场景一上来就是游戏世界,那这些数据要么在_ready()里同步加载导致卡顿,要么异步加载但游戏世界已经开始跑了,时序完全失控。

第二,RTS 需要处理"非游戏状态"的入口。比如从主菜单进游戏、从存档进游戏、从编辑器测试进游戏,这三条路径的初始化逻辑不一样。如果主场景是游戏世界,你就得在游戏场景里判断"我是从哪来的",代码会变得很脏。而boot.tscn作为一个中间层,可以统一接收这些入口参数,然后决定怎么初始化。

第三,加载界面和过渡动画需要一个独立的场景来承载。RTS 的地图资源通常不小,加载个几秒很正常。如果主场景是游戏世界,加载界面就得做成 CanvasLayer 叠在上面,逻辑上很别扭。而boot.tscn可以自己就是加载界面,加载完了再切换到游戏场景。

所以boot.tscn的本质是一个启动协调器,它不负责游戏逻辑,只负责把项目从"什么都没准备好"带到"可以开始游戏"的状态。

2.2 启动流程的分层模型

我在实际项目里把启动流程分成四层,从下到上依次是:

  • 基础设施层:Autoload 单例、全局配置、日志系统、存档管理器。这一层在boot.tscn加载之前就已经由 Godot 引擎初始化好了。
  • 资源加载层:配置表、单位数据、地图数据、本地化文本。这一层由boot.tscn驱动,通常是异步的。
  • 场景构建层:根据加载好的数据,实例化游戏世界、单位、UI。这一层是启动流程的终点。
  • 状态切换层:从 boot 场景切换到游戏场景,处理过渡动画和输入焦点。

这个分层的好处是每一层只依赖它下面的层,不会出现循环依赖。比如资源加载层不会去碰场景构建层的东西,它只负责把数据准备好然后发个信号。

2.3 场景树在启动阶段的结构

boot.tscn的场景树我一般这样组织:

Boot (Node) ├── LoadingScreen (CanvasLayer) │ ├── Background (ColorRect) │ ├── ProgressBar (ProgressBar) │ └── StatusLabel (Label) ├── Loaders (Node) │ ├── ConfigLoader (Node) │ ├── MapLoader (Node) │ └── AssetLoader (Node) └── BootController (Node)

LoadingScreen是纯展示层,不包含任何逻辑。Loaders下面每个子节点负责一类资源的加载,它们各自独立,可以并行也可以串行。BootController是大脑,它持有对 Loaders 的引用,按顺序调用它们,并更新 LoadingScreen 的进度。

这个结构的关键点是:展示和逻辑分离。LoadingScreen 不知道自己在加载什么,它只是接收进度值。Loaders 不知道界面长什么样,它们只是干活然后发信号。BootController 在中间做协调。这样你换加载界面、加新的 Loader、改加载顺序,都不会互相影响。

2.4 Autoload 的排列顺序很关键

Godot 的 Autoload 是按列表顺序初始化的,这一点很多人忽略。如果你的SaveManager依赖ConfigManager,那ConfigManager必须排在前面。我在项目里一般这样排:

  1. LogManager:最底层,其他所有东西都可能要打日志。
  2. ConfigManager:读项目配置,比如版本号、平台信息。
  3. SaveManager:依赖 ConfigManager 确定存档路径。
  4. AudioManager:独立,但要在 UI 之前,因为 UI 可能播按钮音效。
  5. SceneManager:负责场景切换,依赖上面所有。
  6. GameState:全局游戏状态,最后初始化。

注意:Autoload 的_ready()是在主场景加载之前调用的,所以不要在 Autoload 的_ready()里做重活,否则启动会卡。重活留给boot.tscn的 Loader 去做。

3. 核心细节解析与实操要点

3.1 boot.tscn 的入口脚本怎么写

boot.tscn的根节点挂一个脚本,我一般叫boot_controller.gd。它的核心逻辑是:

extends Node signal boot_finished @onready var loading_screen = $LoadingScreen @onready var config_loader = $Loaders/ConfigLoader @onready var map_loader = $Loaders/MapLoader @onready var asset_loader = $Loaders/AssetLoader func _ready(): _run_boot_sequence() func _run_boot_sequence(): loading_screen.set_status("加载配置...") await config_loader.load_all() loading_screen.set_progress(0.3) loading_screen.set_status("加载地图...") await map_loader.load_all() loading_screen.set_progress(0.6) loading_screen.set_status("加载资源...") await asset_loader.load_all() loading_screen.set_progress(1.0) loading_screen.set_status("准备完成") await get_tree().create_timer(0.3).timeout boot_finished.emit()

这里用了await来串行等待每个 Loader 完成。Godot 4 的await配合自定义信号很好用,Loader 加载完发一个finished信号,await就会继续往下走。

关键点是进度条的更新要和实际加载进度挂钩。我见过有人进度条直接create_tween从 0 到 1 跑两秒,然后加载完了就完了,加载没完就卡在 100%。这种做法在加载时间不确定的时候会很难看。正确做法是每个 Loader 自己报告进度,BootController 汇总。

3.2 Loader 的通用接口设计

每个 Loader 我都让它实现同样的接口,这样 BootController 可以用统一的方式调用:

extends Node class_name BaseLoader signal progress_changed(value: float) signal finished var _total_steps: int = 0 var _current_step: int = 0 func load_all() -> void: _total_steps = _count_steps() _current_step = 0 await _do_load() finished.emit() func _count_steps() -> int: return 1 func _do_load() -> void: pass func _report_progress(): _current_step += 1 progress_changed.emit(float(_current_step) / float(_total_steps))

这个基类的价值在于,你加一个新的 Loader 只需要继承它,实现_count_steps()和_do_load(),进度报告和信号发射都是自动的。BootController 那边也不用改,因为它只认load_all()和finished。

3.3 配置表的加载策略

RTS 的配置表通常包括:单位属性、建筑属性、科技树、武器数据、地图配置。这些数据一般存在 JSON、CSV 或者 Godot 的 Resource 文件里。

我个人的选择是:静态数据用 Resource,动态数据用 JSON。单位属性这种基本不改的用自定义 Resource,可以在编辑器里可视化编辑,类型安全。地图配置这种可能由关卡设计师频繁改的用 JSON,改完不用重新导入。

加载的时候要注意一点:Resource 的加载是同步的,JSON 的解析也是同步的。如果你有几十个 Resource 要加载,一次性load()会卡住主线程。解决办法是用ResourceLoader.load_threaded_request()做异步加载:

func _do_load(): var paths = _get_all_resource_paths() for path in paths: ResourceLoader.load_threaded_request(path) var pending = paths.size() while pending > 0: for path in paths: var status = ResourceLoader.load_threaded_get_status(path) if status == ResourceLoader.THREAD_LOAD_LOADED: var res = ResourceLoader.load_threaded_get(path) _cache[path] = res pending -= 1 _report_progress() elif status == ResourceLoader.THREAD_LOAD_FAILED: push_error("加载失败: " + path) pending -= 1 await get_tree().process_frame

这段代码的关键是await get_tree().process_frame,它让出主线程,避免死循环卡死。每帧检查一次加载状态,加载好的就取出来缓存。

3.4 地图数据的加载和实例化

RTS 的地图通常不是直接存成.tscn,而是存成数据文件,运行时根据数据生成。原因很简单:RTS 地图可能有几千个格子,每个格子有地形、资源、通行性等属性,直接做成场景节点会非常臃肿。

我的做法是地图数据存成二维数组的 JSON,加载后由MapBuilder根据数据生成 TileMapLayer 或者自定义的网格节点。这个过程放在boot.tscn里做,而不是等游戏场景加载后再做,因为地图是游戏世界的基础,没有地图其他东西都没法摆。

func _do_load(): var map_data = _read_map_json(_current_map_path) _report_progress() var map_instance = _build_map(map_data) _report_progress() GameState.current_map = map_instance

_build_map里要注意的是分帧构建。如果地图很大,一帧内生成所有格子会卡。可以每帧生成一部分,用await get_tree().process_frame让出:

func _build_map(data) -> Node2D: var map = Node2D.new() var total = data.tiles.size() var batch_size = 500 for i in range(0, total, batch_size): var end = min(i + batch_size, total) for j in range(i, end): _create_tile(map, data.tiles[j]) await get_tree().process_frame return map

3.5 单位数据的预加载

RTS 的单位通常有几十种,每种单位的场景文件(.tscn)需要在启动时预加载,否则第一次生成单位的时候会卡一下。预加载的方式有两种:

一种是preload(),编译时加载,适合确定路径的资源。但 RTS 的单位路径可能是动态的(比如从配置表读),所以更常用的是运行时load()然后缓存。

func _do_load(): var unit_ids = ConfigManager.get_all_unit_ids() for id in unit_ids: var path = ConfigManager.get_unit_scene_path(id) var scene = load(path) _unit_scene_cache[id] = scene _report_progress()

这里有个坑:load()加载的是PackedScene,不是实例。实例化要等到真正生成单位的时候。所以缓存的是PackedScene,用的时候instantiate()。

实操心得:单位场景预加载的时候,如果单位场景本身又依赖其他资源(比如材质、动画),这些依赖也会被一起加载。所以预加载一个单位场景可能实际加载了十几个资源。如果单位特别多,启动时间会明显变长。我的做法是只预加载核心单位,边缘单位按需加载。

4. 实操过程与核心环节实现

4.1 从零搭建 boot.tscn 的完整步骤

假设你现在有一个空的 Godot 4 项目,想按这套方案搭启动流程,可以按下面的步骤来。

第一步,创建 Autoload。在项目设置里添加LogManager、ConfigManager、SaveManager、GameState四个单例,脚本分别放在res://autoload/下。注意顺序,LogManager放最前面。

第二步,创建boot.tscn。根节点用Node,命名Boot。下面加LoadingScreen(CanvasLayer)、Loaders(Node)、BootController(Node)。LoadingScreen里放一个ColorRect做背景、一个ProgressBar、一个Label。

第三步,写boot_controller.gd,挂到BootController上。实现_run_boot_sequence(),按顺序调用各个 Loader。

第四步,写各个 Loader。先写ConfigLoader,它负责读res://config/下的 JSON 文件。再写MapLoader,读地图数据并构建地图。最后写AssetLoader,预加载单位场景。

第五步,在项目设置里把boot.tscn设为主场景。

第六步,写一个SceneManager的goto_game()方法,在boot_finished信号触发后调用,切换到游戏场景。

4.2 加载进度的精确计算

进度条要准,关键是每个 Loader 的权重要对。如果 ConfigLoader 只花 0.1 秒,AssetLoader 花 3 秒,那两个各占 50% 进度就会让进度条在 ConfigLoader 阶段飞快、AssetLoader 阶段几乎不动。

我的做法是给每个 Loader 分配权重,权重根据预估耗时来定:

var loader_weights = { "ConfigLoader": 0.1, "MapLoader": 0.3, "AssetLoader": 0.6 }

然后在 BootController 里汇总:

var total_progress = 0.0 for loader_name in loader_weights: var loader = loaders[loader_name] var weight = loader_weights[loader_name] total_progress += loader.current_progress * weight loading_screen.set_progress(total_progress)

这样进度条的增长速度和实际加载耗时大致匹配,用户体验好很多。

4.3 存档恢复的启动路径

从存档进游戏和从主菜单进游戏,启动流程不一样。从主菜单进,地图是新的,单位是初始的。从存档进,地图状态、单位位置、资源数量都要恢复。

我的处理方式是在boot.tscn接收一个boot_params字典,由上一个场景传入:

# 主菜单里 SceneManager.goto_boot({"mode": "new_game", "map": "map_01"}) # 存档界面里 SceneManager.goto_boot({"mode": "load_game", "save_id": "slot_1"})

boot_controller.gd里根据mode决定加载路径:

func _run_boot_sequence(): var mode = _params.get("mode", "new_game") if mode == "new_game": await _boot_new_game() elif mode == "load_game": await _boot_from_save()

_boot_from_save()会先读存档数据,然后用存档里的地图路径去加载地图,最后把单位状态恢复到地图上。这里的关键是存档数据要在资源加载之前读,因为存档里可能记录了地图路径和单位列表,这些决定了要加载哪些资源。

4.4 场景切换的过渡处理

boot.tscn完成使命后,要切换到游戏场景。直接change_scene_to_file()会有一个瞬间的黑屏或者跳变。我的做法是在LoadingScreen上加一个淡出动画:

func _transition_to_game(): var tween = create_tween() tween.tween_property(loading_screen, "modulate:a", 0.0, 0.5) await tween.finished get_tree().change_scene_to_packed(_game_scene)

注意change_scene_to_packed是延迟执行的,它会在当前帧结束后才真正切换。所以淡出动画要等完再调用,否则动画会被打断。

注意:change_scene_to_packed会释放当前场景。如果你在boot.tscn里缓存了一些数据想传给游戏场景,不能直接传引用,要通过 Autoload 中转。我一般把加载好的数据放在GameState里,游戏场景从GameState取。

4.5 启动阶段的输入屏蔽

启动过程中用户可能会乱点,比如点开始按钮、按 ESC。这些输入如果传到还没准备好的系统里,可能引发空引用。我的做法是在boot.tscn的_ready()里设置get_tree().paused = false但用一个全屏的Control拦截输入:

func _ready(): $LoadingScreen/InputBlocker.mouse_filter = Control.MOUSE_FILTER_STOP $LoadingScreen/InputBlocker.grab_focus()

InputBlocker是一个覆盖全屏的Control,mouse_filter设为STOP会吃掉所有鼠标事件。键盘事件可以在_input()里set_input_as_handled()拦截。

4.6 启动日志和错误处理

启动阶段出问题最难查,因为这时候日志系统可能还没完全就绪。我的做法是LogManager作为第一个 Autoload,它的_ready()里只做一件事:打开日志文件,准备好写入。之后所有启动阶段的日志都写到文件里,同时输出到控制台。

每个 Loader 的_do_load()里都要包一层错误处理:

func _do_load(): var result = _try_load() if result.is_error(): LogManager.error("ConfigLoader 失败: " + result.message) _fallback_to_default()

_fallback_to_default()是关键。启动阶段不能因为一个配置文件读不到就整个崩掉,要有降级方案。比如单位配置读不到,就用内置的默认单位数据,至少让游戏能跑起来,然后给用户一个提示。

5. 常见问题与排查技巧实录

5.1 启动阶段常见问题速查表

问题现象可能原因排查方法解决方案
启动卡在某个进度不动Loader 死循环或 await 没触发在 Loader 里加日志,看最后一条日志停在哪检查 await 的信号是否真的会发射
进度条到 100% 但没进游戏boot_finished 信号没连接或 SceneManager 没调用检查信号连接和 SceneManager 的 goto_game在 boot_finished 回调里加日志确认
游戏场景加载后黑屏游戏场景的 _ready 报错看控制台错误输出修复游戏场景的初始化逻辑
从存档进游戏单位位置错乱存档数据恢复顺序不对检查单位恢复是否在地图构建之后确保地图先构建,再恢复单位
启动时间越来越长资源缓存没清理或重复加载统计每次启动加载的资源数量加缓存机制,避免重复 load
Autoload 报空引用Autoload 顺序不对检查项目设置里的 Autoload 列表顺序被依赖的放前面

5.2 await 不触发的排查

await不触发是启动流程里最常见的问题。表现是代码停在await那一行,后面的都不执行。原因通常是信号没发射,或者发射的时机不对。

排查方法:在await前后各加一条日志,然后在信号发射的地方也加日志。如果只看到await前的日志,没看到信号发射的日志,说明 Loader 的逻辑有问题。如果看到了信号发射的日志但await后的日志没出现,说明信号连接有问题。

还有一种情况是信号在await之前就发射了。比如 Loader 的load_all()是同步完成的,finished信号在await之前就发了,那await就会一直等。解决办法是让load_all()至少await一次process_frame,确保信号在await之后发射。

5.3 资源加载失败的降级处理

资源加载失败在开发阶段很常见,比如路径写错、文件被删、格式不对。启动阶段遇到这种情况,不能直接崩,要有降级。

我的做法是每个 Loader 维护一个_failed_resources列表,加载失败的记录进去,但不中断流程。启动完成后如果_failed_resources不为空,弹一个提示框告诉用户哪些资源加载失败,然后用默认资源替代。

func _do_load(): for path in _paths: if not ResourceLoader.exists(path): _failed_resources.append(path) _use_default(path) continue var res = load(path) if res == null: _failed_resources.append(path) _use_default(path) else: _cache[path] = res _report_progress()

5.4 启动性能优化的几个实操技巧

启动时间超过 5 秒用户就会不耐烦。优化启动性能有几个方向:

减少预加载资源数量。不是所有单位都需要预加载,只预加载前几关会用到的。其他的按需加载,第一次用的时候可能卡一下,但总体启动时间短了。

压缩资源体积。Godot 的纹理压缩、音频压缩都能显著减小资源体积。特别是 RTS 的地图纹理,如果用了 4K 贴图,加载时间会很长。适当降到 2K 或者用压缩格式。

并行加载。如果多个 Loader 之间没有依赖关系,可以并行跑。Godot 4 的await可以同时等多个信号:

var config_task = config_loader.load_all() var asset_task = asset_loader.load_all() await config_task await asset_task

但要注意,并行加载会争抢 IO 和内存,如果资源太多反而更慢。我的经验是配置和资源可以并行,地图加载要单独跑,因为地图构建是 CPU 密集的。

用 ResourceLoader 的线程加载。前面提到的load_threaded_request是真正的后台线程加载,不阻塞主线程。但要注意线程安全,加载完的资源要在主线程取。

5.5 启动流程的可测试性

启动流程很容易变成"只能整体跑,没法单独测"的黑盒。我的做法是让每个 Loader 都可以独立运行。BaseLoader的load_all()不依赖 BootController,你可以单独实例化一个 Loader 然后调load_all()看它能不能正常工作。

另外,我会写一个boot_test.gd脚本,在编辑器里直接运行,模拟完整的启动流程但不切换场景。这样可以快速验证启动逻辑,不用每次都跑整个游戏。

实操心得:启动流程的调试信息一定要详细。我一般会在每个关键节点打日志,包括"开始加载配置"、"配置加载完成,共 N 条"、"开始构建地图"、"地图构建完成,共 M 个格子"。这些日志在出问题的时候能快速定位到是哪一步卡住了。

5.6 不同平台的启动差异

Godot 支持多平台,不同平台的启动行为有差异。比如桌面平台文件 IO 快,移动平台慢;桌面平台可以多线程加载,某些平台对线程有限制。

我的做法是在ConfigManager里根据OS.get_name()判断平台,然后调整加载策略。移动平台减少预加载资源,桌面平台可以多加载一些。Web 平台要特别注意,因为浏览器对内存和文件访问有限制,启动流程要尽量轻量。

func get_load_strategy() -> String: match OS.get_name(): "Android", "iOS": return "minimal" "Web": return "streaming" _: return "full"

minimal策略只加载核心资源,其他按需加载。streaming策略用分块加载,避免一次性占用太多内存。full策略预加载所有资源,启动最快但内存占用高。

6. 启动流程的扩展与维护

6.1 加一个新的 Loader 要改哪些地方

假设你要加一个LocalizationLoader负责加载多语言文本。按这套架构,你只需要做三件事:

第一,写localization_loader.gd,继承BaseLoader,实现_count_steps()和_do_load()。

第二,在boot.tscn的Loaders节点下加一个子节点,挂上这个脚本。

第三,在boot_controller.gd的_run_boot_sequence()里加一行await localization_loader.load_all(),并在loader_weights里给它分配权重。

BootController 的其他逻辑不用改,LoadingScreen 也不用改。这就是分层架构的好处。

6.2 启动流程的版本兼容

游戏更新后,存档格式可能变了,配置表结构可能变了。启动流程要能处理这些兼容问题。

我的做法是在ConfigManager里维护一个data_version,每次启动时检查存档里的版本号和当前版本号。如果存档版本旧,走一个迁移流程:

func migrate_save(save_data: Dictionary) -> Dictionary: var version = save_data.get("version", 0) if version < 2: save_data = _migrate_v1_to_v2(save_data) if version < 3: save_data = _migrate_v2_to_v3(save_data) return save_data

迁移流程放在boot.tscn的存档加载路径里,在资源加载之前执行。这样后续的加载逻辑拿到的都是最新格式的数据,不用到处判断版本。

6.3 启动流程的监控和埋点

上线之后你希望知道玩家的启动成功率、平均启动时间、卡在哪个阶段最多。这些数据对优化很有价值。

我的做法是在boot_controller.gd里记录每个阶段的开始和结束时间,启动完成后汇总成一个字典,通过一个AnalyticsManager上报:

var _timings = {} func _run_boot_sequence(): _timings["start"] = Time.get_ticks_msec() _timings["config_start"] = Time.get_ticks_msec() await config_loader.load_all() _timings["config_end"] = Time.get_ticks_msec() # ... 其他阶段 _timings["end"] = Time.get_ticks_msec() _report_timings()

_report_timings()把各阶段耗时算出来,上报到你的数据平台。如果某个阶段耗时异常,就能针对性地优化。

6.4 启动流程的自动化测试

启动流程的回归测试很重要,因为改一个 Loader 可能影响整个启动。我一般写一个 GUT(Godot Unit Test)测试,模拟启动流程并断言各个阶段的状态:

func test_boot_sequence(): var boot = load("res://boot.tscn").instantiate() add_child(boot) await boot.boot_finished assert_true(GameState.is_ready) assert_not_null(GameState.current_map) assert_gt(GameState.unit_scenes.size(), 0)

这个测试跑一遍完整的启动流程,确保没有回归。CI 里每次提交都跑,能提前发现问题。

6.5 启动流程的文档化

最后一点,启动流程一定要有文档。不是那种自动生成的 API 文档,而是手写的、说明"为什么这么设计"的文档。我一般会在项目根目录放一个BOOT_FLOW.md,里面画一个简单的流程图(用文字描述),列出每个 Loader 的职责、依赖关系、加载顺序,以及常见的修改场景。

这份文档的价值在于,当项目交给别人维护,或者你自己几个月后回来看,能快速理解启动流程的设计意图。没有文档的话,看到一堆 Loader 和 await,很容易改错。

我在实际项目里最大的体会是:启动流程的复杂度不在于代码量,而在于时序和依赖关系。boot.tscn这个模式的核心价值,就是把这些时序和依赖关系显式地表达出来,而不是藏在各个场景的_ready()里。你多花一天时间把启动流程搭好,后面能省下一周的调试时间。特别是 RTS 这种多系统耦合的项目,启动流程稳了,整个项目的开发节奏都会顺很多。

返回列表