docs: add C# scripting design and editor issues
This commit is contained in:
756
docs/plan/C#脚本模块的设计与实现.md
Normal file
756
docs/plan/C#脚本模块的设计与实现.md
Normal file
@@ -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<T>` 思路对齐
|
||||
- 更容易在未来做 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<T>`
|
||||
- 自定义托管结构体
|
||||
- 嵌套对象图
|
||||
- 泛型容器
|
||||
- 资源引用对象选择器
|
||||
|
||||
---
|
||||
|
||||
## 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<T>`
|
||||
- `GameObject.GetComponent<T>`
|
||||
- `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 集成阶段,工程风险会显著更低。
|
||||
Reference in New Issue
Block a user