7. 让框架适配你的项目
七个程序集、移除可选包、用自己的实现替换服务、关闭子系统,以及文件在磁盘上的位置。
这个框架生来就是可以重塑的。每个服务都可以换成你自己的实现,或整个关掉;当你不想要某些可选部分时,它们会干净地自行移除。本章逐一展示这些调节手段,外加框架在磁盘上存放文件的位置。
7.1 七个程序集,以及该引用哪些
运行时代码被拆分为七个程序集。拆分的目的是让可选 Unity 包保持可选:每个附加程序集只在其对应的包已安装时才参与编译。它们共享同一个命名空间 CommonGameSystem.Core,因此你的 using 语句永远不变。
如果你的脚本位于默认的 Assembly-CSharp(你没有创建任何程序集定义文件),可以跳过本节——Unity 会自动为你引用一切。
如果你的代码使用自己的程序集定义(.asmdef),按你所使用的内容添加引用:
| 程序集 | 里面有什么 | 何时引用它…… |
|---|---|---|
CommonGameSystem.Core | 23 个服务中的 18 个:日志、事件、时间、存档、设置、对象池、音频、场景流、成就、定时器、补间、补间序列、随机数、状态机、状态栈、延迟事件队列、命令注册表,以及 UI 面板栈契约。 | 始终引用。它是核心,而且完全不需要任何 Unity 包。 |
CommonGameSystem.Input | 输入服务:动作映射上下文与重绑定。 | 你的代码使用 IInputService。需要 Input System 包。 |
CommonGameSystem.UI | 面板栈的 uGUI 实现。 | 你的代码构造 PanelStack 或使用它的 uGUI 辅助类。仅解析 IPanelStack 的话只需要 Core。 |
CommonGameSystem.Localization | 本地化服务及其文本绑定组件。 | 你的代码使用 ILocalizationService。需要 uGUI。 |
CommonGameSystem.Assets | 资产提供器(按地址加载、引用计数)。 | 你的代码使用 IAssetProvider。需要 Addressables。 |
CommonGameSystem.AddressableScene | 来自 Addressables 的叠加场景加载。 | 你的代码使用 IAddressableSceneService。需要 Addressables。 |
CommonGameSystem.Bootstrap | 自动启动序列。 | 永远不需要。里面没有任何供你调用的东西。 |
把框架引用添加到你自己的程序集定义:
- 在 Project 窗口中选中你的
.asmdef文件。 - 在 Inspector 中找到 Assembly Definition References,点击 +。
- 选择
CommonGameSystem.Core,再加上你用到的附加程序集(例如CommonGameSystem.Input)。 - 点击 Inspector 底部的 Apply。
7.2 移除某个可选 Unity 包
有三个 Unity 包是可选的。移除任何一个都不会弄坏框架:需要它的程序集会把自己排除在编译之外,其余一切照常工作。启动流程仍会正常完成。
| 你移除的包 | 什么被关掉 | 什么仍然工作 |
|---|---|---|
Input System<br>com.unity.inputsystem | 输入服务消失(它的类型不再参与编译)。UI 面板栈退回到空操作版本,因为焦点导航需要输入。 | 其他一切。剩下的每个服务都正常运行;面板栈的调用只是什么都不做。 |
uGUI<br>com.unity.ugui | uGUI 面板栈退回到空操作版本。本地化服务消失。演示场景的脚本也会把自己排除在外。 | 其他一切,包括输入。 |
Addressables<br>com.unity.addressables | 资产提供器和可寻址场景服务消失。 | 其他一切,包括常规场景服务。 |
两点提醒。第一,你自己的脚本中凡是提到已移除服务的地方将无法编译——请把那些引用一并移除,或者用 ServiceLocator.TryResolve<T>(...) 检查可用性,并把相关代码放在你自己的脚本宏定义之后。第二,重新安装该包会让一切原样回归,无需任何额外设置。
7.3 用你自己的实现替换服务
每个服务都只通过它的接口被使用,所以你可以换上自己的版本,任何调用方都不会察觉。模式永远是同样的三行:拿到旧实例,用 Replace 注册你的实例,然后释放旧实例。
下面是一个完整的真实例子。存档服务接受一个可插拔的序列化器——负责把你的数据转成文本的那个部件。这个自定义序列化器把存档存成 Base64 而不是可读的 JSON,玩家就没法随手编辑它们了:
using System;
using CommonGameSystem.Core;
using UnityEngine;
// The framework's serializer seam has just two methods.
public sealed class Base64SaveSerializer : ISaveSerializer
{
public string Serialize(object o)
{
string json = JsonUtility.ToJson(o);
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(json);
return Convert.ToBase64String(bytes);
}
public object Deserialize(Type t, string s)
{
byte[] bytes = Convert.FromBase64String(s);
string json = System.Text.Encoding.UTF8.GetString(bytes);
return JsonUtility.FromJson(json, t);
}
}
替换脚本放在你的第一个场景里:
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(-100)] // swap before other scripts cache the save service
public sealed class SaveSetup : MonoBehaviour
{
private void Awake()
{
var previous = ServiceLocator.Resolve<ISaveService>() as System.IDisposable;
ServiceLocator.Replace<ISaveService>(
new SaveService(new Base64SaveSerializer()));
previous?.Dispose();
}
}
同样的模式适用于任何服务:写一个实现该接口的类(或者像第 6.1 和 6.2 章那样,用不同的选项构建框架自带的类),然后 Replace 它。三条规则让替换干净利落:
- 尽早替换。 脚本会保留它们解析到的引用,因此要在你的第一个场景、以执行顺序 -100 替换服务,赶在任何东西缓存旧实例之前。少数框架服务也会在启动时相互接线——例如设置持久化和成就会一直持有它们启动时拿到的存档服务——因此过晚的替换只影响之后才解析的代码。
- 释放被你顶替的实例。
Replace不会销毁旧实例。有七个服务(时间、对象池、音频、UI 面板栈、调度器、补间、补间序列)各自在层级中拥有一个名为 “[CGS] ...” 的隐藏辅助对象。释放旧实例会移除它;跳过释放则它会运行一整个会话。 - 先替换,后释放,顺序绝不能反过来,这样就不会有脚本在中间的空档解析到一个已死的实例。
7.4 关闭某个子系统
要彻底关掉一项功能,用它内置的空操作版本替换它。所有调用方继续工作;调用只是什么都不做。读取操作返回安全的默认值(零、false、空,或“未找到”)。
ServiceLocator.Replace<IAudioService>(new NullAudio()); // total silence, no code changes
每个服务都有一个:
| 服务(接口) | 空操作类 |
|---|---|
| ILogger | NullLogger |
| IObjectPoolService | NullObjectPool |
| ITimeService | NullTimeService |
| IEventBus | NullEventBus |
| ISaveService | NullSaveService |
| IConfiguration | NullConfiguration |
| IInputService | NullInputService |
| IAudioService | NullAudio |
| IPanelStack | NullPanelStack |
| ISceneService | NullSceneService |
| ILocalizationService | NullLocalization |
| IScheduler | NullScheduler |
| ITweenService | NullTweenService |
| IAssetProvider | NullAssetProvider |
| IRandomService | NullRandomService |
| IStateMachineService | NullStateMachineService |
| IPushdownStackService | NullPushdownStackService |
| ITweenSequenceService | NullTweenSequenceService |
| IAddressableSceneService | NullAddressableSceneService |
| IDeferredBus | NullDeferredBus |
| ICommandRegistry | NullCommandRegistry |
| IAchievementService | NullAchievementService |
7.3 节的释放规则同样适用于这里:当你关掉那七个拥有 “[CGS] ...” 辅助对象的服务之一时,请释放被你顶替的实例。
7.5 你的文件在磁盘上的位置
框架写入的所有内容都放在 Unity 标准的按游戏区分的数据文件夹 Application.persistentDataPath 之下:
- Windows:
C:\Users\<you>\AppData\LocalLow\<Company>\<Product> - macOS:
~/Library/Application Support/<Company>/<Product> - Linux:
~/.config/unity3d/<Company>/<Product>
其内部:
- 存档 ——
saves/<slot>.json,每个槽位一个文件,外加上一版本的.bak备份。写入期间,服务会先写一个临时文件再原子地替换进去,因此崩溃或断电永远不会损坏已有存档。 - 设置 —— 同一
saves/文件夹中以config_开头的文件(例如config_AudioSettings.json)。如果你关掉了存档服务,设置会自动退回到 Unity 的 PlayerPrefs。 - 成就 —— 通过存档服务存储在同一文件夹中。
- 输入重绑定 —— 存储在 Unity 的 PlayerPrefs 中(Windows 上是注册表;macOS 上是 plist 文件)。
公司名和产品名来自 Edit > Project Settings > Player。同步文件的云存档系统(如 Steam Auto-Cloud)直接指向 saves/ 文件夹即可。
下一章:8. 疑难解答与常见问题