diff --git a/docs/issues/C#脚本系统_Editor前置缺口.md b/docs/issues/C#脚本系统_Editor前置缺口.md new file mode 100644 index 00000000..261d2cc2 --- /dev/null +++ b/docs/issues/C#脚本系统_Editor前置缺口.md @@ -0,0 +1,364 @@ +# C#脚本系统 Editor 前置缺口 + +## 1. 目的 + +本文档只记录 **C# 脚本系统接入 editor 时需要补齐的 editor 侧缺口**。 + +这些内容 **不是脚本模块第一阶段的实现范围**。 +第一阶段仍以: + +- 运行时闭环 +- 单元测试闭环 +- 最小场景级验证 + +为主。 + +本文档的作用是防止后续把 editor 集成需求误混到第一阶段里。 + +--- + +## 2. 当前策略 + +当前策略明确如下: + +- 脚本系统第一阶段先不依赖 editor +- editor 只作为后续集成目标 +- editor 相关工作拆分为独立 issue + +因此,下面所有条目均属于: + +- 后续阶段工作 +- 或 editor / runtime 边界层工作 + +--- + +## 3. Issue 列表 + +## E-001 Play / Simulate 工作流尚未落地 + +### 现状 + +editor 当前已经定义了相关事件,但完整的 Play / Simulate 切换链路仍未形成统一工作流。 + +### 影响 + +没有这条链路,脚本系统无法在 editor 中以接近 Unity 的方式运行: + +- 无法从编辑态切换到运行态 +- 无法创建运行时场景副本 +- 无法触发脚本运行时启动与停止 + +### 需要补齐 + +- `EditorMode` 状态机 +- `Edit / Play / Simulate` 三态切换 +- 菜单栏或工具栏中的 Play / Stop / Pause 控件 +- 对应的事件派发和恢复流程 + +### 优先级 + +高,但属于 editor 集成阶段,不属于脚本第一阶段 + +--- + +## E-002 editor 场景与 runtime 场景切换控制未封装 + +### 现状 + +当前 scene 管理具备快照能力,但 editor 还没有一套完整的“运行时场景副本控制器”。 + +### 影响 + +没有这一层,脚本一旦接入 editor,会直接面临以下风险: + +- Play 过程污染编辑场景 +- Stop 后无法恢复编辑态 +- 运行时对象状态和 editor 面板状态错乱 + +### 需要补齐 + +- editor 场景与 runtime 场景的明确所有权 +- Play 前复制 +- Stop 后恢复 +- 运行时期间禁用部分编辑操作 +- 选中对象和层级面板的状态同步策略 + +### 优先级 + +高 + +--- + +## E-003 Inspector 缺少 `ScriptComponent` 作者化 UI + +### 现状 + +当前 editor Inspector 还没有脚本组件专用 UI。 + +### 影响 + +即使底层脚本系统完成,用户也无法在 editor 中完成以下基础操作: + +- 添加脚本组件 +- 删除脚本组件 +- 选择脚本类 +- 查看脚本类名 +- 调整脚本组件顺序 + +### 需要补齐 + +- Add Component 菜单中的 `ScriptComponent` +- 已挂载脚本组件列表 +- 脚本类选择器 +- 脚本丢失 / 类不存在 / 程序集未加载的错误态显示 + +### 优先级 + +高 + +--- + +## E-004 Inspector 缺少脚本字段编辑能力 + +### 现状 + +当前 editor 没有基于脚本字段元数据的动态 Inspector。 + +### 影响 + +无法实现 Unity 风格脚本工作流中最关键的一环: + +- 在 Inspector 中编辑脚本字段 + +### 需要补齐 + +- 字段元数据缓存到 editor 层 +- 基础类型字段绘制 +- `Vector2/3/4` 字段绘制 +- `bool` / `string` / 数值类型编辑 +- 字段值回写 `ScriptComponent` 的字段缓存 + +### 优先级 + +高 + +--- + +## E-005 C# 项目与程序集管理缺失 + +### 现状 + +`Project` 面板目前已经有 `Assets/Scripts` 目录概念,但没有正式的 C# 项目管理机制。 + +### 影响 + +用户无法在 editor 中稳定地管理脚本编译输入和输出: + +- 没有 `GameScripts.csproj` +- 没有 `ScriptCore` 引用管理 +- 没有程序集输出路径约定 + +### 需要补齐 + +- 项目初始化时创建脚本工程模板 +- 脚本程序集输出目录规范 +- `ScriptCore` 引用自动配置 +- 脚本工程文件缺失时的恢复策略 + +### 优先级 + +中到高 + +--- + +## E-006 editor 内缺少脚本编译触发入口 + +### 现状 + +当前 editor 没有“编译脚本”入口,也没有编译状态反馈。 + +### 影响 + +后续即使 Inspector 可选择脚本,用户也无法在 editor 中完成闭环: + +- 修改脚本 +- 编译程序集 +- 重新加载类列表 + +### 需要补齐 + +- 手动编译按钮 +- 后续可扩展到自动编译或文件监听 +- 编译状态显示 +- 成功/失败结果通知 + +### 优先级 + +中到高 + +--- + +## E-007 Console 面板缺少脚本编译错误与运行时异常接入 + +### 现状 + +当前 Console 还没有专门的脚本编译/加载/执行错误通路。 + +### 影响 + +脚本系统接入 editor 后,最基本的错误反馈都会缺失: + +- 编译错误 +- 程序集加载失败 +- 类发现失败 +- 运行时异常 +- 栈信息显示 + +### 需要补齐 + +- 编译器 stdout/stderr 采集 +- 程序集加载错误显示 +- 托管异常栈转发到 Console +- 错误级别区分 +- 后续可扩展到点击跳转文件/行号 + +### 优先级 + +高 + +--- + +## E-008 脚本类数据库与类缓存刷新机制缺失 + +### 现状 + +editor 还没有“当前项目有哪些可挂载脚本类”的缓存层。 + +### 影响 + +没有这一层,Inspector 无法稳定实现: + +- 类下拉列表 +- 类是否可挂载的判断 +- 类丢失提示 +- 程序集重载后的刷新 + +### 需要补齐 + +- 脚本程序集扫描结果缓存 +- 全限定类名列表 +- 类状态标记 +- reload 后缓存刷新 + +### 优先级 + +中 + +--- + +## E-009 对象引用字段的 editor 交互控件缺失 + +### 现状 + +后续脚本字段一旦支持 `GameObject` / 组件引用,editor 目前没有相应控件。 + +### 影响 + +对象引用字段虽然不属于脚本第一阶段必需项,但属于后续可用性的关键能力。 + +### 需要补齐 + +- 场景对象选择器 +- 层级面板拖拽赋值 +- 失效引用显示 +- 类型校验 + +### 优先级 + +中 + +--- + +## E-010 脚本程序集重载的 editor UX 缺失 + +### 现状 + +editor 当前没有定义“脚本重载时如何影响当前状态”的用户体验规则。 + +### 影响 + +后续一旦接入脚本编译与重载,会出现一系列状态问题: + +- 当前选中对象是否刷新 +- Inspector 是否保留编辑状态 +- Play 中是否允许重载 +- 重载失败如何回退 + +### 需要补齐 + +- Edit 模式重载策略 +- Play 模式重载策略 +- 失败回退策略 +- 用户提示文案与状态反馈 + +### 优先级 + +中 + +--- + +## 4. 与 editor 集成直接相关的推荐顺序 + +建议的 editor 集成顺序如下: + +1. `E-001` Play / Simulate 工作流 +2. `E-002` runtime scene 切换控制 +3. `E-003` `ScriptComponent` 作者化 UI +4. `E-004` 脚本字段 Inspector +5. `E-007` Console 错误接入 +6. `E-005` C# 项目与程序集管理 +7. `E-006` 脚本编译入口 +8. `E-008` 类数据库刷新 +9. `E-009` 对象引用字段控件 +10. `E-010` 程序集重载 UX + +这个顺序的核心原则是: + +- 先让脚本能在 editor 中“跑起来” +- 再让脚本能在 editor 中“被作者化” +- 最后再补自动化体验与重载体验 + +--- + +## 5. 关联但不归 editor 的前置项 + +以下事项不是 editor issue,但会直接影响后续 editor 集成质量: + +- `GameObject UUID` 持久化 +- 运行时生命周期规范化 +- `Start` 一次性语义 +- `SetActive` 驱动 `OnEnable/OnDisable` +- 场景递归更新 +- `ScriptComponent` 字段缓存与序列化 + +这些工作应当在脚本第一阶段或 editor 集成前置阶段优先完成。 + +--- + +## 6. 结论 + +脚本系统第一阶段不应被 editor 当前进度绑定。 + +正确的推进方式是: + +- 先做运行时与单测闭环 +- 再按本文档逐项补 editor 集成能力 + +这样可以避免脚本模块在早期同时背负: + +- 运行时实现风险 +- editor 工作流风险 +- UI 设计风险 +- 重载体验风险 + +从而把复杂度拆开,逐步落地。 diff --git a/docs/plan/C#脚本模块的设计与实现.md b/docs/plan/C#脚本模块的设计与实现.md new file mode 100644 index 00000000..96774f1d --- /dev/null +++ b/docs/plan/C#脚本模块的设计与实现.md @@ -0,0 +1,756 @@ +# C#脚本模块的设计与实现 + +## 1. 背景 + +XCEngine 的整体方向是模仿传统 Unity 引擎架构,而不是 DOTS/ECS-first 路线。 +在这一目标下,脚本系统应当满足以下基本预期: + +- 脚本语言使用 `C#` +- 脚本以“挂载到 `GameObject` 上的组件”形式工作 +- 脚本与引擎核心解耦,支持独立编译和运行时加载 +- 脚本可以逐步扩展到 Inspector、场景序列化、Play/Simulate 工作流 + +当前 `editor` 仍处于基础阶段,因此脚本系统第一阶段不应依赖 editor 完整落地。 +第一阶段的目标应当收敛为: + +- 先完成原生运行时与托管运行时之间的桥接 +- 先完成 `ScriptComponent + C# MonoBehaviour` 的基本执行链路 +- 先完成单元测试和最小场景级验证 +- 将 editor 集成需求单独列为 issue,后续补齐 + +--- + +## 2. 设计目标 + +### 2.1 总体目标 + +脚本系统应当提供一条接近 Unity 的开发路径: + +1. 用户在独立的 C# 项目中编写脚本 +2. C# 脚本编译为程序集 +3. Engine Core 在运行时加载程序集 +4. `GameObject` 上挂载 `ScriptComponent` +5. `ScriptComponent` 驱动一个对应的 C# `MonoBehaviour` 实例 +6. 脚本通过引擎暴露的 API 调用原生功能 + +### 2.2 第一阶段目标 + +第一阶段只覆盖以下内容: + +- `engine` 内部的脚本运行时抽象 +- 第一套 C# 运行时实现 +- `ScriptComponent` 原生组件 +- `ScriptCore` 托管基础库 +- 最小可用的 `InternalCall` 绑定 +- 单元测试与最小运行时测试 + +### 2.3 第一阶段非目标 + +第一阶段明确不做以下内容: + +- editor Inspector 脚本字段编辑 +- editor 中的脚本类选择器 +- editor 的 Play/Simulate 集成 +- 自动编译、文件监听、热重载 +- 调试器接入 +- 发布态 AOT / IL2CPP +- 大而全的引擎 API 暴露 + +--- + +## 3. 三方对比结论 + +### 3.1 Unity + +Unity 的典型脚本模型有几个关键特征: + +- 用户脚本通常继承 `MonoBehaviour` +- 一段脚本本质上就是一个组件实例 +- 脚本字段可序列化、可在 Inspector 中编辑 +- 生命周期完整,且与 `GameObject active` / `Component enabled` 语义一致 +- Play 模式运行的是运行时场景副本,而不是直接修改编辑场景 + +### 3.2 参考项目 Fermion + +参考项目已经实现了一套 C# 脚本模块,优点主要在于: + +- 已经证明了 `Mono embedding + InternalCall + C# 程序集加载` 这条路线可行 +- 已经具备脚本类发现、脚本实例创建、字段反射、运行时调用 +- 已经有托管侧 API 包装层和原生侧 `ScriptGlue` + +但它的对象模型并不是 Unity 风格: + +- 托管脚本类继承的是 `Entity` +- 原生挂载结构是 `ScriptContainerComponent` +- 更像“一个实体挂多个脚本类名”,而不是“每个脚本本身就是一个组件” +- 生命周期目前主要聚焦 `OnCreate/OnUpdate` + +结论: + +- Fermion 适合借鉴运行时技术路线 +- Fermion 不适合作为最终 API 形态的直接模板 + +### 3.3 XCEngine 当前情况 + +XCEngine 当前已经具备以下基础: + +- 已有 `GameObject + Component + Scene` 模型 +- `Component` 已定义 Unity 风格生命周期接口 +- `Scene` 和 `GameObject` 已有序列化入口 +- `ComponentFactoryRegistry` 已支持按类型名恢复组件 + +但当前也存在会直接影响脚本系统设计的现实约束: + +- `Scene::Update/FixedUpdate/LateUpdate` 目前只遍历根对象 +- `GameObject::Update/FixedUpdate/LateUpdate` 目前不递归子对象 +- `Start` 的场景级一次性调度路径还未完整建立 +- `SetActive` 尚未真正驱动 `OnEnable/OnDisable` +- editor 的 Play/Simulate 工作流仍未落地 +- `GameObject UUID` 当前未进入场景序列化主路径 + +结论: + +- XCEngine 适合走 Unity 风格脚本模型 +- 但脚本系统第一阶段必须连同一部分运行时地基一起建设 + +--- + +## 4. 总体设计结论 + +XCEngine 的 C# 脚本模块采用以下路线: + +- **脚本语言**:C# +- **脚本挂载模型**:原生 `ScriptComponent` 对应托管 `MonoBehaviour` +- **程序集模型**:`ScriptCore` 与 `GameScripts` 分离 +- **运行时加载模式**:独立编译,运行时加载 +- **桥接方式**:`InternalCall` +- **运行时抽象策略**:先抽象接口,再优先落 Mono 实现 +- **第一阶段验证方式**:单元测试优先,不依赖 editor + +这条路线有两个核心原则: + +1. API 形态尽量接近 Unity +2. 第一阶段严格控制范围,只做运行时闭环和测试闭环 + +--- + +## 5. 运行时选型 + +### 5.1 第一阶段选型:Mono + +第一阶段建议使用 `Mono` 作为第一套 C# 运行时实现,原因如下: + +- 与现有规划文档保持一致 +- 参考项目已经证明这条技术路线可落地 +- `InternalCall` 路线成熟,适合快速建立最小可用系统 +- 便于在 Windows 环境中先做出可运行结果 + +### 5.2 选型边界 + +本设计不把 `Mono` 写死为脚本系统唯一实现,而是将其作为第一实现: + +- 对外暴露 `IScriptRuntime` / `ScriptEngine` 抽象 +- `MonoScriptRuntime` 作为第一套后端 +- 后续如有需要,可以演进到 `CoreCLR` 或其他运行时 + +### 5.3 第一阶段不做的运行时能力 + +Mono 相关的以下复杂能力不进入第一阶段: + +- 域热重载 +- 编辑器内自动重编译后重载 +- 托管调试器接入 +- 发布态 AOT + +第一阶段仅要求: + +- 初始化运行时 +- 加载核心程序集 +- 加载用户程序集 +- 发现脚本类 +- 实例化对象 +- 调用生命周期 +- 读写托管字段 + +--- + +## 6. 模块划分 + +### 6.1 原生侧模块 + +建议在 `engine` 内新增 `Scripting` 模块: + +```text +engine/ +├── include/XCEngine/Scripting/ +│ ├── ScriptEngine.h +│ ├── IScriptRuntime.h +│ ├── ScriptAssembly.h +│ ├── ScriptClass.h +│ ├── ScriptInstance.h +│ ├── ScriptField.h +│ ├── ScriptFieldStorage.h +│ ├── ScriptGlue.h +│ └── ScriptComponent.h +└── src/Scripting/ + ├── ScriptEngine.cpp + ├── ScriptGlue.cpp + ├── ScriptComponent.cpp + └── Mono/ + ├── MonoScriptRuntime.cpp + ├── MonoScriptClass.cpp + └── MonoScriptAssembly.cpp +``` + +### 6.2 托管侧模块 + +建议新增托管核心库 `ScriptCore`: + +```text +managed/ +├── XCEngine.ScriptCore/ +│ ├── XCEngine.ScriptCore.csproj +│ ├── Object.cs +│ ├── Component.cs +│ ├── Behaviour.cs +│ ├── MonoBehaviour.cs +│ ├── GameObject.cs +│ ├── Transform.cs +│ ├── Debug.cs +│ ├── Time.cs +│ └── InternalCalls.cs +└── GameScripts/ + ├── GameScripts.csproj + └── Scripts/*.cs +``` + +### 6.3 程序集分层 + +程序集分为两层: + +- `XCEngine.ScriptCore.dll` + - 由引擎维护 + - 提供托管基类和引擎 API 包装 +- `GameScripts.dll` + - 由项目侧维护 + - 编写具体游戏脚本 + +关系如下: + +```text +GameScripts.dll + └── 引用 XCEngine.ScriptCore.dll + +Engine Core + ├── 先加载 XCEngine.ScriptCore.dll + └── 再加载 GameScripts.dll +``` + +--- + +## 7. 对象模型 + +### 7.1 托管侧模型 + +托管侧应当采用接近 Unity 的对象层次: + +```text +Object +└── Component + └── Behaviour + └── MonoBehaviour +``` + +其中: + +- `Object`:基础托管对象 +- `Component`:挂载到 `GameObject` 上的托管组件基类 +- `Behaviour`:带 `enabled` 语义的组件 +- `MonoBehaviour`:用户脚本直接继承的基类 + +### 7.2 原生挂载模型 + +原生侧脚本挂载使用 `ScriptComponent`,而不是 `ScriptContainerComponent`。 + +原因如下: + +- 更符合 Unity 认知模型 +- 更容易与现有 `GameObject::AddComponent` 思路对齐 +- 更容易在未来做 Inspector 级脚本组件显示 +- 更容易把字段序列化与组件实例对应起来 + +建议 `ScriptComponent` 至少包含: + +- `scriptComponentUUID` +- `assemblyName` +- `namespaceName` +- `className` +- `enabled` +- `fieldStorage` + +推荐接口示意: + +```cpp +class ScriptComponent : public Component { +public: + std::string GetName() const override { return "Script"; } + + const std::string& GetAssemblyName() const; + const std::string& GetNamespaceName() const; + const std::string& GetClassName() const; + std::string GetFullClassName() const; + + uint64_t GetScriptComponentUUID() const; + bool IsRuntimeValid() const; + + void Serialize(std::ostream& os) const override; + void Deserialize(std::istream& is) override; + +private: + uint64_t m_scriptComponentUUID = 0; + std::string m_assemblyName = "GameScripts"; + std::string m_namespaceName; + std::string m_className; + ScriptFieldStorage m_fieldStorage; +}; +``` + +### 7.3 多脚本挂载 + +一个 `GameObject` 应允许挂载多个 `ScriptComponent`。 + +这与 Unity 保持一致: + +- 同一个对象可以挂多个不同脚本 +- 同一个脚本是否允许重复挂载,由后续属性或规则控制 +- 第一阶段不做“禁止重复挂载”的复杂策略 + +--- + +## 8. 身份模型与序列化要求 + +### 8.1 必须使用 UUID,而不是运行时 ID + +脚本系统中,托管实例与原生对象的稳定绑定必须建立在 `UUID` 之上,而不是当前的自增 `ID`。 + +原因如下: + +- 运行时场景复制不能依赖自增 ID 稳定 +- 脚本字段里的对象引用必须有稳定键 +- editor 与 runtime 之间的对象映射必须有稳定键 +- 单元测试和场景恢复也需要稳定身份 + +因此需要补齐以下要求: + +- `GameObject UUID` 进入场景序列化 +- 场景反序列化时恢复 `UUID` +- `ScriptComponent` 自身也应有持久化 UUID + +### 8.2 第一阶段字段序列化策略 + +第一阶段的脚本字段序列化原则如下: + +- 只序列化 `ScriptComponent` 的字段缓存 +- 只序列化脚本作者显式设置的字段值 +- 运行时脚本执行过程中修改的值,不自动回写场景 + +这与 Unity 的 Play 模式行为一致: + +- Play 中的运行时改动不应直接污染编辑数据 + +### 8.3 第一阶段字段支持范围 + +建议第一阶段先支持: + +- `float` +- `double` +- `bool` +- `int32` +- `uint64` +- `string` +- `Vector2` +- `Vector3` +- `Vector4` +- `GameObject` 引用 + +第一阶段不要求支持: + +- `List` +- 自定义托管结构体 +- 嵌套对象图 +- 泛型容器 +- 资源引用对象选择器 + +--- + +## 9. 生命周期设计 + +### 9.1 目标生命周期 + +脚本系统最终应支持以下 Unity 风格生命周期: + +- `Awake` +- `OnEnable` +- `Start` +- `FixedUpdate` +- `Update` +- `LateUpdate` +- `OnDisable` +- `OnDestroy` + +### 9.2 第一阶段生命周期闭环 + +第一阶段就应当把上述生命周期的原生调度链路设计好,哪怕 editor 尚未接入。 + +建议运行时流程如下: + +#### `Scene` 运行时启动 + +1. `ScriptEngine::OnRuntimeStart(scene)` +2. 遍历场景中全部激活对象 +3. 找到所有 `ScriptComponent` +4. 为每个组件创建托管 `MonoBehaviour` 实例 +5. 写入原生对象 UUID / 组件 UUID / 基础上下文 +6. 应用序列化字段缓存 +7. 调用 `Awake` +8. 若对象激活且组件启用,调用 `OnEnable` +9. 标记“等待 Start” + +#### 每帧执行 + +- 物理阶段:`FixedUpdate` +- 普通阶段:`Update` +- 后处理阶段:`LateUpdate` +- 对于尚未执行 `Start` 的脚本,在第一次普通帧前先调用 `Start` + +#### 运行时停止 + +1. 对仍处于启用状态的脚本调用 `OnDisable` +2. 对所有脚本调用 `OnDestroy` +3. 清理托管实例表 +4. 清理运行时场景上下文 + +### 9.3 对现有引擎的前置要求 + +为了让脚本生命周期符合预期,现有引擎需要补齐以下地基: + +- `Scene` 更新必须递归整个层级,而不是只更新根对象 +- `Start` 必须具备“一次且仅一次”语义 +- `SetActive` 必须驱动 `OnEnable/OnDisable` +- 运行时场景启动与停止必须显式化 +- 创建对象时不应直接把“编辑态创建”与“运行态 Awake”混为一谈 + +这些工作虽然不都属于脚本模块,但它们是脚本模块的直接运行前提。 + +--- + +## 10. 原生与托管之间的桥接 + +### 10.1 桥接方式 + +第一阶段使用 `InternalCall`: + +- 托管侧通过 `MethodImplOptions.InternalCall` 声明方法 +- 原生侧通过 `mono_add_internal_call` 注册 + +### 10.2 第一阶段最小 API 集 + +第一阶段建议只暴露最小必需 API: + +- `Debug.Log / LogWarning / LogError` +- `Time.deltaTime` +- `GameObject.GetName / SetName` +- `GameObject.GetTransform` +- `Component.GetGameObject` +- `GameObject.HasComponent` +- `GameObject.GetComponent` +- `Transform` 的本地位置 / 旋转 / 缩放 + +这套 API 足够覆盖以下测试与最小演示: + +- 变换脚本 +- 旋转/移动脚本 +- 生命周期日志验证 +- 组件访问验证 + +第一阶段不建议优先暴露: + +- 物理 API +- 渲染 API +- 音频 API +- 输入系统 +- 资源系统 + +原因很简单: + +- 第一阶段以单元测试闭环为主 +- 暴露面越大,绑定维护成本越高 +- 当前这些系统本身仍在演进 + +--- + +## 11. 类发现与实例管理 + +### 11.1 脚本类发现规则 + +用户脚本类应满足以下条件才被视为可挂载脚本: + +- 定义在 `GameScripts.dll` +- 非抽象类 +- 继承 `XCEngine.MonoBehaviour` + +### 11.2 缓存结构 + +原生运行时需要缓存以下信息: + +- 程序集表 +- 脚本类表 +- 方法句柄表 +- 字段元数据表 +- 运行时实例表 + +建议实例表键使用: + +- `GameObjectUUID + ScriptComponentUUID` + +而不是: + +- 内存地址 +- 组件在容器中的索引 +- 自增 ID + +### 11.3 方法缓存 + +每个脚本类应缓存常用生命周期方法句柄: + +- `Awake` +- `OnEnable` +- `Start` +- `FixedUpdate` +- `Update` +- `LateUpdate` +- `OnDisable` +- `OnDestroy` + +这样可以避免每帧按字符串查找方法。 + +--- + +## 12. 单元测试优先策略 + +### 12.1 原则 + +第一阶段脚本模块不依赖 editor,因此验证策略以单元测试和最小运行时测试为主。 + +### 12.2 测试目录建议 + +```text +tests/ +└── Scripting/ + ├── unit/ + │ ├── test_script_runtime.cpp + │ ├── test_script_metadata.cpp + │ ├── test_script_component.cpp + │ ├── test_script_fields.cpp + │ └── CMakeLists.txt + └── managed/ + ├── XCEngine.ScriptCore/ + └── TestScripts/ +``` + +### 12.3 第一阶段必须覆盖的测试 + +#### 运行时初始化 + +- 能初始化脚本运行时 +- 能加载 `ScriptCore` +- 能加载测试脚本程序集 + +#### 类发现 + +- 只发现继承 `MonoBehaviour` 的类 +- 忽略抽象类 +- 忽略普通工具类 + +#### 生命周期 + +- 能创建托管实例 +- 能按顺序触发 `Awake -> OnEnable -> Start -> Update` +- 能在停止时触发 `OnDisable -> OnDestroy` + +#### 组件桥接 + +- 脚本能访问 `GameObject` +- 脚本能访问 `Transform` +- 脚本能访问最小组件 API + +#### 字段系统 + +- 能发现公共字段 +- 能读写字段 +- 字段缓存可回填到托管实例 +- 场景序列化后字段值不丢失 + +#### 多脚本对象 + +- 同一 `GameObject` 上多个 `ScriptComponent` 都可实例化 +- 不同脚本实例之间不会串字段 + +#### UUID 绑定 + +- 运行时复制或反序列化后仍可稳定恢复脚本绑定键 + +### 12.4 第一阶段不要求的测试 + +- editor Inspector UI +- 热重载 +- 编译器输出面板 +- 文件监听 +- 资源拖拽 + +--- + +## 13. 建议的实现顺序 + +### 阶段 A:补运行时地基 + +先补以下基础能力: + +- `GameObject UUID` 序列化 +- 层级递归更新 +- `Start` 一次性语义 +- `SetActive` 对生命周期的影响 +- `Scene` 运行时启动/停止接口 + +### 阶段 B:脚本最小闭环 + +完成: + +- `Scripting` 模块骨架 +- `ScriptComponent` +- `MonoScriptRuntime` +- `XCEngine.ScriptCore.dll` +- `GameScripts.dll` +- 最小 `InternalCall` + +### 阶段 C:字段与序列化 + +完成: + +- 脚本字段元数据 +- 字段缓存 +- `ScriptComponent` 序列化/反序列化 +- 字段相关单元测试 + +### 阶段 D:最小运行时演示 + +在不依赖 editor 的前提下完成: + +- 纯运行时测试场景 +- 一个或两个最小脚本示例 +- Play 级别运行验证 + +### 阶段 E:editor 集成 + +后续再做: + +- 脚本组件 Inspector +- 类选择器 +- 编译按钮 +- 错误输出 +- 重载流程 + +--- + +## 14. 与 editor 的关系 + +第一阶段文档明确规定: + +- C# 脚本模块**不依赖 editor 完整落地** +- editor 相关工作全部后置 +- editor 相关缺口统一记录在 `docs/issues` + +第一阶段唯一允许与 editor 共用的内容是: + +- 场景序列化格式 +- `GameObject UUID` 语义 +- 运行时场景副本的总体设计方向 + +除此之外,不应把脚本模块第一阶段实现建立在 editor 已具备以下能力的假设上: + +- Play/Simulate 控制条 +- Inspector 自定义字段绘制 +- 项目内脚本自动编译 +- 脚本异常面板 + +--- + +## 15. 风险与权衡 + +### 15.1 Mono 依赖 + +风险: + +- Windows 环境需要显式安装或打包 Mono +- CMake 配置与发布路径管理会变复杂 + +权衡: + +- 第一阶段优先解决“能跑起来”的问题 +- 运行时抽象保留未来替换空间 + +### 15.2 生命周期与现有引擎实现的偏差 + +风险: + +- 如果继续保留当前“创建对象就立即 `Awake`”的行为,脚本生命周期会混乱 + +权衡: + +- 脚本系统建设必须带动运行时生命周期整理 + +### 15.3 字段系统范围 + +风险: + +- 一开始就追求完整序列化会让范围失控 + +权衡: + +- 第一阶段只做基础类型与少量引用类型 +- 复杂容器和高级序列化后置 + +### 15.4 editor 后置 + +风险: + +- 第一阶段用户体验不完整 + +权衡: + +- 可以显著降低实现风险 +- 可以先通过单测和运行时样例把底层做稳 + +--- + +## 16. 最终结论 + +XCEngine 的 C# 脚本系统应当采用: + +- `ScriptComponent + MonoBehaviour` +- `ScriptCore + GameScripts` 双程序集结构 +- `InternalCall` 桥接 +- `Mono` 作为第一套运行时实现 +- `UUID` 作为绑定与序列化的稳定身份 +- 第一阶段只做运行时与单元测试,不依赖 editor + +这条路线既保留了 Unity 风格的一致性,也能适配当前工程实际进度。 + +第一阶段的成功标准不是“Inspector 能编辑脚本字段”,而是: + +- 能加载脚本程序集 +- 能发现脚本类 +- 能在场景运行时驱动 `MonoBehaviour` +- 能通过单元测试验证生命周期、字段和绑定语义 + +达到这一点后,再进入 editor 集成阶段,工程风险会显著更低。