ZMWorld Framework
面向商业项目的 Unity World 生命周期与分层架构
World 管流程,功能以三类脚本组成模块。 用类似 Unity Scene 的 World 承载大厅、战斗或子游戏流程;World 内某项功能对应的 Data、Logic、Msg 三个脚本,概念上合称一个业务模块。
1. 产品定位
ZMWorld 是一套以 World 游戏流程 为最高组织维度的 Unity 游戏架构。World 的定位类似 Unity Scene,但它管理的是逻辑流程而非场景资源:HallWorld 代表大厅流程,PokerWorld 代表一个扑克子游戏,BattleWorld 代表战斗流程。
一个 World 通常包含很多业务。例如 HallWorld 可以监管背包、任务、榜单、商城等功能。背包的 BagDataMgr + BagLogicCtrl + BagMsgMgr 在概念上称为“背包模块”,任务与榜单同理。框架运行时不存在 Module 脚本或第四类对象,它只扫描并管理 IDataBehaviour、ILogicBehaviour、IMsgBehaviour 三类实现。
-
World 流程管控
以类似 Scene 的最高维度管理大厅、战斗和子游戏流程。
-
功能概念模块化
每项功能由自己的 Data、Logic、Msg 三类脚本共同完成。
-
自动反射装配
自动发现、排序、创建和释放 World 下的全部 Behaviour。
-
热更与工具链
支持 HybridCLR 反射入口和 Unity Editor 自动代码生成。
| 能力 | 商业项目价值 |
|---|---|
| World 流程隔离 | 像 Scene 一样划分大厅、战斗与子游戏等最高层流程 |
| 概念模块自治 | 每项功能分别拥有配套的 Data、Logic、Msg 三类脚本 |
| Data / Msg / Logic 分层 | 每项功能对应的三类脚本职责清晰 |
| 自动反射装配 | 减少初始化样板代码和漏注册问题 |
| 统一生命周期 | World 进入与退出时统一创建或释放全部 Behaviour 脚本 |
| 多 World 管理 | 支持大厅切换到 Poker、Battle 等不同游戏流程 |
| HybridCLR 入口 | 支持通过程序集和完整类型名反射创建 World |
| Editor 代码生成 | 快速生成 World 及各功能的三类脚本,统一团队规范 |
| 可控更新策略 | 默认不自动更新每层,避免无效 Update 消耗 |
框架核心理念
World 是真实运行时容器,Module 是业务概念。 World 决定当前处于大厅、战斗还是某个子游戏;某项功能对应的 Data、Logic、Msg 三个实现合称一个模块,但没有额外的 Module 类、基类、接口、注册表或生命周期。
不存在 Module 脚本
文档中的“背包模块”“任务模块”只是对一组关联脚本的统称。框架实际字典只保存 IDataBehaviour、ILogicBehaviour、IMsgBehaviour,自动装配也只识别这三个接口。
当前版本
- Package:
com.zm.worldframework - Version:
1.0.0 - 最低声明版本:Unity
2019.1 - 示例工程版本:Unity
2021.3.38f1 - Runtime Assembly:
ZM.World.Framework
2. 架构全景
2.1 三个组织层级
| 层级 | 基类 / 接口 | 负责 | 示例 |
|---|---|---|---|
| World | World |
最高维度流程、Behaviour 生命周期、切换和更新入口 | Hall、Poker、Battle |
| 功能模块(概念) | 无对应脚本 | 对同一功能三类脚本的业务统称 | 背包、任务、榜单 |
| Behaviour | 三个接口 | 实际被框架创建和管理的对象 | BagDataMgr / BagLogicCtrl / BagMsgMgr |
┌──────────────────────────────────────────────────────────────┐
│ HallWorld │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ 背包功能(概念) │ │ 任务功能(概念) │ │榜单功能(概念) │ │
│ │ BagDataMgr │ │ TaskDataMgr │ │ RankDataMgr │ │
│ │ BagLogicCtrl │ │ TaskLogicCtrl │ │ RankLogicCtrl │ │
│ │ BagMsgMgr │ │ TaskMsgMgr │ │ RankMsgMgr │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└──────────────────────────────┬───────────────────────────────┘
▼
UI / Network / SDK
2.2 创建流水线
调用 WorldManager.CreateWorld<T>() 后,框架依次执行:
- 创建
T : World实例。 - 获取 World 所在程序集及命名空间。
- 扫描同命名空间下所有三类 Behaviour 非抽象实现;框架不会识别 Module 类型。
- 根据
IBehaviourExecution配置计算各层内部顺序。 - 反射创建全部 Data、Msg、Logic 实例并注册到当前 World 的三个字典。
- 严格按 Data → Msg → Logic 调用
OnCreate()。 - 调用 World 的
OnCreate()。 - 注册到活跃 World 列表并派发创建成功回调。
- 首次创建时自动生成常驻
WorldUpdater。
为什么 Logic 最后初始化?
Logic 通常需要读取 Data 并调用 Msg。Data 和 Msg 先完成初始化,可以保证 Logic 的 OnCreate() 中安全获取依赖。
2.3 销毁流水线
WorldManager.DestroyWorld<T>(args) 的顺序为:
- Logic 层全部执行
OnDestroy()。 - Data 层全部执行
OnDestroy()。 - Msg 层全部执行
OnDestroy()。 - 清空 World 内的三类对象容器。
- 调用 World
OnDestroy()。 - 从活跃列表移除 World。
- 调用
OnDestroyPostProcess(args),可安全切换下一 World。
释放职责
事件、网络监听、计时器、资源句柄和异步任务应在对应 Behaviour 的 OnDestroy() 中解除。不要只依赖 C# GC。
3. 工程接入
3.1 推荐目录
Assets/
├── ZMPackages/
│ └── ZMWorld/ # 框架源码
└── GameScripts/
├── Launcher/
├── HallWorld/ # 最高层大厅流程
│ ├── HallWorld.cs
│ ├── BagModule/ # 可选的文件夹分组,不是 Module 类型
│ │ ├── BagDataMgr.cs
│ │ ├── BagLogicCtrl.cs
│ │ └── BagMsgMgr.cs
│ ├── TaskModule/ # 只用于代码组织
│ └── RankingModule/
├── PokerWorld/ # 独立扑克子游戏流程
│ ├── RoomModule/ # 文件夹可自由命名
│ ├── TableModule/
│ └── SettlementModule/
└── BattleWorld/ # 独立战斗流程
3.2 UPM 包信息
仓库 Assets/package.json:
{
"name": "com.zm.worldframework",
"version": "1.0.0",
"displayName": "ZM World Framework",
"description": "A Unity game framework built around World-based lifecycle management and Logic/Data/Message layering.",
"unity": "2019.1"
}
3.3 Assembly Definition
使用自定义 .asmdef 的业务程序集,需要引用:
Editor 代码生成工具必须位于仅编辑器程序集,避免进入 Player Build。
4. 十分钟快速接入
下面以 HallWorld 中的任务功能为例。World 负责大厅整体流程,TaskDataMgr、TaskLogicCtrl、TaskMsgMgr 三个独立脚本共同完成任务业务,因此在沟通和目录组织上统称“任务模块”,但不会创建 TaskModule 运行时对象。
4.1 创建 World
namespace HallWorld
{
public class HallWorld : World
{
public override IBehaviourExecution GetBehaviourExecution()
=> new HallWorldExecutionOrder();
public override void OnCreate()
{
UnityEngine.Debug.Log("[HallWorld] Ready");
}
public override void OnDestroy()
{
UnityEngine.Debug.Log("[HallWorld] Released");
}
}
}
4.2 创建任务功能的 Data 脚本
namespace HallWorld
{
public class TaskDataMgr : IDataBehaviour
{
private readonly System.Collections.Generic.HashSet<int>
mCompletedTaskIds = new();
public void OnCreate()
{
mCompletedTaskIds.Clear();
}
public bool IsCompleted(int taskId) =>
mCompletedTaskIds.Contains(taskId);
public void MarkCompleted(int taskId) =>
mCompletedTaskIds.Add(taskId);
public void OnDestroy()
{
mCompletedTaskIds.Clear();
}
}
}
4.3 创建任务功能的 Msg 脚本
namespace HallWorld
{
public class TaskMsgMgr : IMsgBehaviour
{
public void OnCreate()
{
// 注册网络响应或事件监听
}
public void SendCompleteTaskRequest(int taskId)
{
// Network.Send(...)
}
public void OnDestroy()
{
// 解除所有监听
}
}
}
4.4 创建任务功能的 Logic 脚本
namespace HallWorld
{
public class TaskLogicCtrl : ILogicBehaviour
{
private TaskDataMgr mData;
private TaskMsgMgr mMsg;
public void OnCreate()
{
mData = World.GetDataLayer<TaskDataMgr>();
mMsg = World.GetMsgLayer<TaskMsgMgr>();
}
public bool CompleteTask(int taskId)
{
if (taskId <= 0 || mData.IsCompleted(taskId))
return false;
mMsg.SendCompleteTaskRequest(taskId);
mData.MarkCompleted(taskId);
return true;
}
public void OnDestroy()
{
}
}
}
4.5 配置初始化顺序
public class HallWorldExecutionOrder : IBehaviourExecution
{
public string[] GetDataBehaviourExecution() => new[]
{
"BagDataMgr",
"TaskDataMgr",
"RankingDataMgr"
};
public string[] GetMsgBehaviourExecution() => new[]
{
"BagMsgMgr",
"TaskMsgMgr",
"RankingMsgMgr"
};
public string[] GetLogicBehaviourExecution() => new[]
{
"BagLogicCtrl",
"TaskLogicCtrl",
"RankingLogicCtrl"
};
}
未出现在数组中的类型仍会创建,排序值默认为 999,位于显式配置类型之后。相同排序值的反射顺序不应作为业务依赖。
4.6 启动与访问
using UnityEngine;
public class GameLauncher : MonoBehaviour
{
private void Start()
{
WorldManager.CreateWorld<HallWorld.HallWorld>();
HallWorld.TaskLogicCtrl taskLogic =
World.GetLogicLayer<HallWorld.TaskLogicCtrl>();
taskLogic.CompleteTask(1001);
}
private void OnDestroy()
{
WorldManager.DestroyWorld<HallWorld.HallWorld>();
}
}
5. World 与业务模块设计规范
5.1 World 对应最高层游戏流程
World 的判断标准接近“是否值得成为一张独立 Scene 或一个独立子游戏流程”,而不是“是不是一项业务功能”。
推荐作为 World:
LoginWorld:登录、选服和进入游戏前的顶层流程。HallWorld:主大厅流程,监管背包、任务、榜单、商城等业务模块。PokerWorld:一个完整扑克子游戏,监管房间、牌桌、结算等模块。BattleWorld:一套独立战斗流程,监管角色、技能、Buff、战斗结算等模块。
通常不应该单独作为 World:
- 背包、任务、榜单、商城、公会等大厅内业务。
- 技能、Buff、角色属性等战斗内业务。
- 单个弹窗、页面或网络协议。
这些功能应作为所属 World 内的业务模块。只有当一项功能拥有独立的进入/退出流程、整体生命周期和大量子业务时,才考虑提升为 World。
5.2 “模块”是三类脚本的概念组合
World 下不是只有一套笼统的 Data / Logic / Msg,而是每项功能分别提供对应的三个 Behaviour 实现。三个脚本合称一个业务模块:
| HallWorld 模块 | Data | Logic | Msg |
|---|---|---|---|
| 背包 | BagDataMgr |
BagLogicCtrl |
BagMsgMgr |
| 任务 | TaskDataMgr |
TaskLogicCtrl |
TaskMsgMgr |
| 榜单 | RankingDataMgr |
RankingLogicCtrl |
RankingMsgMgr |
这里没有 BagModule、TaskModule 或统一 ModuleBase 脚本,框架也不会实例化模块对象。模块只是团队对关联功能脚本的统称,用于分配开发人员、测试用例和代码所有权,并避免“万能 HallDataMgr / HallLogicCtrl”无限膨胀。
5.3 命名空间是 World 装配边界
框架只扫描与 World 完全相同命名空间的实现类型:
namespace BattleWorld
{
public class BattleWorld : World { }
public class HeroDataMgr : IDataBehaviour { /* ... */ }
public class HeroLogicCtrl : ILogicBehaviour { /* ... */ }
public class HeroMsgMgr : IMsgBehaviour { /* ... */ }
public class SkillDataMgr : IDataBehaviour { /* ... */ }
public class SkillLogicCtrl : ILogicBehaviour { /* ... */ }
public class SkillMsgMgr : IMsgBehaviour { /* ... */ }
}
常见错误
如果 BattleWorld 位于 Game.Battle,而技能模块位于 Game.Battle.Skill namespace,后者不会被当前反射规则发现。文件夹可以按业务模块分层,但同一 World 下需要自动装配的 C# 类型必须保持与 World 完全相同的 namespace。
5.4 Behaviour 必须可反射创建
自动装配使用 Activator.CreateInstance(type),因此实现类应满足:
- 非抽象类。
- 可被当前运行环境加载。
- 提供无参构造函数。
- 不依赖 Unity Inspector 注入。
- 不继承必须挂载到场景的
MonoBehaviour。
6. 功能三类脚本协作规范
以下规范以某项功能对应的三类脚本为单位。背包功能的 Data、Logic、Msg 在概念上合称背包模块;任务和榜单同理。跨功能协作应通过对方 Logic 的公开用例或项目级事件完成,不要直接修改其他功能的 Data。
6.1 Data Layer
Data 层是当前模块业务状态的唯一可信来源。
推荐:
- 属性使用
private set。 - 对外暴露语义明确的原子修改方法。
- 在
OnCreate()设置默认值或加载缓存。 - 在
OnDestroy()清理集合和敏感数据。
避免:
- 直接发送网络请求。
- 引用 UI 或场景对象。
- 编排跨多个 Data 的复杂流程。
- 将可变集合直接暴露给外部修改。
6.2 Msg Layer
Msg 层是当前模块网络协议、SDK 或外部消息的适配边界。
推荐流程:
Msg 层应在 OnDestroy() 中解除:
- 网络协议监听。
- 全局事件总线订阅。
- SDK 回调。
- 异步 CancellationToken。
6.3 Logic Layer
Logic 层负责当前模块的用例和业务规则,例如背包使用物品、任务提交、榜单请求与战斗技能释放。
Logic 优先组合本模块 Data 和 Msg;需要跨模块协作时,应调用目标模块 Logic 的公开接口。Logic 不应直接操作 UI,推荐通过 UI 层主动查询或项目事件总线通知表现层刷新。
7. 多 World 管理
7.1 在不同游戏流程间切换
WorldManager.CreateWorld<HallWorld.HallWorld>();
// 玩家从大厅进入扑克子游戏
WorldManager.DestroyWorld<HallWorld.HallWorld>();
WorldManager.CreateWorld<PokerWorld.PokerWorld>();
// 退出扑克并回到大厅
WorldManager.DestroyWorld<PokerWorld.PokerWorld>();
WorldManager.CreateWorld<HallWorld.HallWorld>();
DefaultGameWorld 指向首个创建的 World;CurWorldType 记录最近创建或销毁后回退的当前类型。
7.2 多 World 并存
框架允许多个 World 同时激活,适合确有并存需求的流程。但常规 Scene 式流程更推荐明确销毁旧 World 后创建新 World,以避免状态、更新与类型路由歧义。
7.3 获取指定 World
如果目标 World 未激活,接口会输出错误日志并返回 null。
7.4 静态 Behaviour 访问的路由规则
框架按活跃 World 列表顺序查找第一个持有 T 的实例。
类型唯一性
多 World 并存时,每个模块 Behaviour 类型应明确归属于一个 World。若多个 World 持有相同类型,静态访问会返回列表中的第一个实例,容易产生隐式歧义。
8. 更新策略
WorldUpdater 是框架唯一自动创建的 MonoBehaviour。它在 Unity Update() 中调用 WorldManager.Update(),随后遍历所有活跃 World 的 OnUpdate()。
public override void OnUpdate()
{
// 仅调度确实需要逐帧执行的系统
mInputSystem.Tick();
mPresentationSystem.Tick();
}
框架没有在 ILogicBehaviour 等接口中强制定义 OnUpdate(),这是有意设计:
- 避免每个对象默认参与逐帧循环。
- 让高频更新入口集中可审计。
- 允许项目自行实现 Tick、FixedTick、LogicFrame 等策略。
商业项目建议
将战斗逻辑更新放入独立固定帧调度器,不要直接依赖渲染帧 Update()。大厅等事件驱动业务通常无需逐帧更新。
9. Editor 自动化工具
9.1 打开工具
Unity 菜单:
窗口提供:
- 脚本保存路径配置。
- World 名称与命名空间映射。
- 按某项业务功能分别生成 Data / Msg / Logic 脚本。
- 一键创建 World 与首组 Behaviour 脚本,再按功能继续生成三类脚本。
9.2 GeneratorModuleConfig
GeneratorModuleConfig 是编辑器配置类的既有名称。这里的 Module 表示生成器中的 World 配置项,不代表框架存在运行时 Module 脚本。
默认配置路径:
| 字段 | 说明 |
|---|---|
savePath |
相对于 Assets 的脚本保存目录 |
modules |
World 名称与 C# 命名空间映射数组 |
moduleName |
World 配置名,例如 HallWorld |
moduleNamespace |
例如 HallWorld 或 Game.Hall |
9.3 快捷键
选中任意 GameObject 后可按名称生成脚本:
| 快捷键 | 菜单 | 生成类型 |
|---|---|---|
| Shift+D | GameObject / 生成数据层脚本 | *DataMgr |
| Shift+L | GameObject / 生成业务逻辑层脚本 | *LogicCtrl |
| Shift+N | GameObject / 生成网络层脚本 | *MsgMgr |
覆盖行为
生成器写入目标文件时,如果文件已经存在会删除并重建。已加入业务代码的文件不要再次强制生成,建议先通过版本控制确认差异。
10. HybridCLR 与热更新
框架支持从指定程序集和完整类型名创建 World:
处理流程:
- 使用
assembly.GetType(worldFullName)查找 World 类型。 - 获取泛型
CreateWorld方法。 - 通过
MakeGenericMethod(worldType)构造运行时泛型。 - 调用与常规创建完全相同的装配流水线。
热更约束
完整类型名必须包含正确命名空间;热更程序集需要引用框架 Runtime Assembly;World 与其监管的各业务模块三层实现必须位于同一程序集和相同命名空间,且类型元数据已正确加载。
11. Runtime API Reference
11.1 WorldManager
状态属性
| 成员 | 类型 | 说明 |
|---|---|---|
Builder |
bool |
是否至少成功构建过 World |
WorldUpdater |
WorldUpdater |
常驻更新驱动器 |
DefaultGameWorld |
World |
首个创建的默认 World |
CurWorldType |
Type |
当前 World 类型 |
OnCreateWorldSuccessListener |
Action<World> |
World 创建完成事件 |
CreateWorld<T>
创建并自动装配 World。同一类型如果已经是 CurWorldType,会输出重复构建错误并返回。
WorldManager.OnCreateWorldSuccessListener += world =>
{
Debug.Log($"World ready: {world.GetType().Name}");
};
WorldManager.CreateWorld<HallWorld.HallWorld>();
CreateWorldByReflection
用于 HybridCLR 或运行时程序集场景。找不到类型或创建方法时会输出明确错误日志。
DestroyWorld<T>
销毁指定类型 World,并将 args 传递给 OnDestroyPostProcess。
public sealed class BattleResult
{
public bool IsWin;
public int RewardId;
}
WorldManager.DestroyWorld<BattleWorld.BattleWorld>(
new BattleResult { IsWin = true, RewardId = 1001 }
);
GetWorld<T>
返回指定活跃 World;未找到时记录错误并返回 null。
GetLogicLayer / GetDataLayer / GetMsgLayer
public static T GetLogicLayer<T>() where T : ILogicBehaviour
public static T GetDataLayer<T>() where T : IDataBehaviour
public static T GetMsgLayer<T>() where T : IMsgBehaviour
遍历活跃 World 并返回第一个匹配实例。通常使用 World.Get*Layer<T>() 快捷入口即可。
Update
调用全部活跃 World 的 OnUpdate()。正常情况下由 WorldUpdater 自动调用。
OnRelease
清理全部 World、更新器、状态和创建回调。适用于应用退出、框架重启或测试用例清场。
当前 OnRelease 行为
OnRelease() 直接调用各 World 的 OnDestroy(),不会逐个调用层对象的 OnDestroy()。业务中优先正常 DestroyWorld<T>();全局释放前应确认层资源已正确释放。
11.2 World
生命周期
| API | 调用时机 | 用途 |
|---|---|---|
OnCreate() |
全部 Data / Msg / Logic 创建完成之后 | World 级流程启动逻辑 |
OnUpdate() |
每个 Unity Update | 显式调度必要更新 |
OnDestroy() |
全部 Data / Msg / Logic 销毁之后 | World 级流程释放逻辑 |
OnDestroyPostProcess(args) |
从活跃列表移除之后 | 页面或 World 切换 |
GetBehaviourExecution() |
反射装配之前 | 返回层内执行顺序配置 |
静态访问
public static T GetLogicLayer<T>() where T : ILogicBehaviour
public static T GetDataLayer<T>() where T : IDataBehaviour
public static T GetMsgLayer<T>() where T : IMsgBehaviour
手动注册
public void AddLogicCtrl(ILogicBehaviour behaviour)
public void AddDataMgr(IDataBehaviour behaviour)
public void AddMsgMgr(IMsgBehaviour behaviour)
这些接口主要由自动装配器调用。手动注册同类型对象会覆盖该 World 字典中的旧实例,不会自动销毁旧实例。
DestroyWorld
释放 World 内全部 Data、Msg、Logic 对象并调用 World OnDestroy()。通常应通过 WorldManager.DestroyWorld<T>() 使用,确保同时更新全局状态和活跃列表。
11.3 Behaviour 接口
11.4 IBehaviourExecution
public interface IBehaviourExecution
{
string[] GetLogicBehaviourExecution();
string[] GetDataBehaviourExecution();
string[] GetMsgBehaviourExecution();
}
数组元素填写类型短名称,越靠前越早初始化。配置只影响同一层内部排序,不改变全局 Data → Msg → Logic 顺序。
11.5 WorldTypeManager
负责扫描程序集、过滤命名空间、分类排序、反射实例化和触发初始化。属于框架基础设施,业务层通常不应直接调用。
11.6 WorldUpdater
唯一的 Unity MonoBehaviour 驱动器。第一次创建 World 时自动生成名为 WorldUpdater 的常驻对象,并通过 DontDestroyOnLoad 跨场景保留。
12. 生命周期时序
12.1 创建时序
GameLauncher
│
└─ CreateWorld<HallWorld>()
├─ new HallWorld()
├─ GetBehaviourExecution()
├─ 扫描同程序集 + 同命名空间
├─ new 全部 Data → AddDataMgr → Data.OnCreate
├─ new 全部 Msg → AddMsgMgr → Msg.OnCreate
├─ new 全部 Logic→ AddLogicCtrl→ Logic.OnCreate
├─ HallWorld.OnCreate
├─ Add active world
├─ OnCreateWorldSuccessListener
└─ InitWorldUpdater(仅首次)
12.2 销毁时序
DestroyWorld<HallWorld>(args)
├─ Logic.OnDestroy
├─ Data.OnDestroy
├─ Msg.OnDestroy
├─ Clear dictionaries
├─ HallWorld.OnDestroy
├─ Remove active world
└─ HallWorld.OnDestroyPostProcess(args)
13. 商业项目最佳实践
13.1 World、模块与三层依赖方向
推荐:
World(真实运行时容器,负责最高流程与生命周期)
└─ 某项功能的三类脚本(概念上称为模块,无 Module 对象)
├─ Presentation → TaskLogicCtrl → TaskDataMgr
└─ TaskLogicCtrl → TaskMsgMgr → Network / SDK
禁止形成:
- 模块 Data 引用模块 Logic。
- Msg 直接修改多个模块 Data 并完成业务流程。
- Logic 直接持有具体 UI 窗口。
- 一个模块越过目标 Logic 直接修改其他模块 Data。
- 不同 World 通过静态字段相互强耦合。
13.2 接口返回值
Logic 公开用例建议返回结构化结果,而不是只打日志:
public readonly struct ClaimRewardResult
{
public readonly bool Success;
public readonly string ErrorCode;
public ClaimRewardResult(bool success, string errorCode)
{
Success = success;
ErrorCode = errorCode;
}
}
13.3 异步安全
- World 销毁后,异步回调不得继续写入其 Data。
- 在 Msg 层维护取消令牌或请求版本号。
- 回调执行前确认 World 仍处于活跃状态。
- 不要在
OnDestroyPostProcess前启动依赖旧 World 的新异步流程。
13.4 测试策略
各业务模块的三层对象都是普通 C# 类,适合按模块单元测试。建议测试:
- Data 原子修改和边界值。
- Logic 用例的成功、失败及重复调用。
- Msg 响应到 Logic 的路由。
- World 创建/销毁后的事件与资源数量。
- 同一 World 中各模块初始化、协作和释放是否正确。
- 多 World 并存时类型访问是否唯一。
14. 性能说明
14.1 反射成本
反射扫描只发生在 World 创建阶段,不在逐帧热路径。商业项目中仍建议:
- 避免频繁创建和销毁同一 World。
- 将大型常驻域与临时玩法域合理拆分。
- IL2CPP 项目确认反射类型不会被裁剪,必要时配置
link.xml。 - 热更程序集加载完成后再创建对应 World。
14.2 Update 成本
WorldManager.Update() 每帧遍历所有活跃 World,但每层不会自动 Update。保持 World.OnUpdate() 简洁,并优先使用事件驱动、计时器中心或批量 Tick。
14.3 内存与事件
最常见泄漏来源不是 World 字典,而是全局事件、网络回调和异步闭包。在 OnDestroy() 中对称解除注册,是框架稳定运行的关键。
15. 常见问题与排错
创建 World 后为什么获取不到 Data / Logic / Msg?
检查实现类是否与 World 位于同一程序集、完全相同的 namespace,是否为非抽象类并具有可用的无参构造函数,同时确认它实现了正确接口。
为什么 OnCreate 顺序不稳定?
未配置在 IBehaviourExecution 数组中的类型排序值都是 999。存在依赖关系的类型必须显式列出,不要依赖反射返回顺序。
为什么第二次 CreateWorld 没有创建?
如果 CurWorldType 已经等于目标类型,框架会阻止重复构建。先确认是否应该复用现有实例,或先调用 DestroyWorld<T>()。
为什么静态 GetDataLayer 返回了错误实例?
多个活跃 World 持有相同 Behaviour 类型时,框架返回第一个匹配实例。为每个 World 下的业务模块定义独立类型,或先使用 GetWorld<T>() 明确获取目标 World。
为什么生成器找不到配置?
确认配置位于 Assets/ZMPackages/ZMWorld/Editor/GeneratorEditor/GeneratorModuleConfig.asset,或在窗口中手动选择正确的 GeneratorModuleConfig。
为什么生成的脚本覆盖了已有代码?
当前生成器检测到同名目标文件时会删除并重新创建。生成前提交版本控制,避免对已进入开发阶段的脚本再次强制生成。
为什么退出游戏时仍有监听回调?
检查每个 Behaviour 的 OnDestroy() 是否对称解绑。另请注意全局 OnRelease() 当前不逐层调用 Behaviour 销毁,正常业务退出应优先逐个 DestroyWorld<T>()。
IL2CPP 下反射类型缺失怎么办?
为 World 及其全部业务模块三层实现添加保留配置或 link.xml,确保构造函数和类型元数据不被裁剪;同时验证热更程序集引用和完整类型名。
16. 上线前检查清单
- [ ] 每个 World 对应明确的最高层游戏流程,而不是单项业务功能。
- [ ] World 内背包、任务、榜单等功能已拆成独立业务模块。
- [ ] 每个业务模块拥有配套的 Data、Logic、Msg,而非共享万能三层。
- [ ] 同一 World 的实现类型 namespace 完全一致。
- [ ] 所有自动装配类型具备无参构造函数。
- [ ] 有依赖的 Behaviour 已配置初始化顺序。
- [ ] Logic 不直接操作 UI,Data 不发送网络请求,跨模块不越层修改 Data。
- [ ] 所有事件、计时器和网络监听均能在销毁时解除。
- [ ] 多 World 并存时不存在重复 Behaviour 类型歧义。
- [ ] IL2CPP / HybridCLR 已完成反射保留验证。
- [ ] 生成器输出目录与命名空间配置已纳入版本控制。
- [ ] World 创建、销毁和重复进入流程已通过自动化测试。