docs: sync api and planning docs

This commit is contained in:
2026-04-08 16:07:03 +08:00
parent 08c3278e10
commit 31756847ab
1826 changed files with 44502 additions and 29645 deletions

View File

@@ -8,51 +8,87 @@
## 概览
当前脚本模块已经形成一条比较明确的“数据层 -> 调度层 -> 后端层”链路:
当前脚本模块已经形成一条明确的“场景数据层 -> 运行时调度层 -> 托管后端层”链路:
1. [ScriptComponent](ScriptComponent/ScriptComponent.md) 挂在 `GameObject` 上,保存程序集名、命名空间、类名、组件 UUID 和 [ScriptFieldStorage](ScriptFieldStorage/ScriptFieldStorage.md)。
2. [ScriptEngine](ScriptEngine/ScriptEngine.md) 在运行时收集场景内所有脚本组件,按 `(gameObjectUUID, scriptComponentUUID)` 跟踪实例状态,并统一驱动生命周期。
3. [IScriptRuntime](IScriptRuntime/IScriptRuntime.md) 定义“类发现、字段元数据、默认值读取、实例创建、生命周期调用、字段同步”的统一契约。
4. [NullScriptRuntime](NullScriptRuntime/NullScriptRuntime.md) 负责在没有真实托管后端时兜底,让脚本数据层和编辑流程仍然可工作。
4. [NullScriptRuntime](NullScriptRuntime/NullScriptRuntime.md) 在没有真实托管后端时兜底,让脚本数据层和编辑流程仍然可工作。
5. [Mono](Mono/Mono.md) 子目录中的 [MonoScriptRuntime](Mono/MonoScriptRuntime/MonoScriptRuntime.md) 是当前唯一的真实托管后端,负责加载 `XCEngine.ScriptCore.dll``GameScripts.dll`、发现脚本类、创建托管实例并完成字段桥接。
如果从引擎分层角度理解:
如果从商业级游戏引擎常见的分层方式理解:
- `ScriptComponent + ScriptFieldStorage` 是可序列化的数据层。
- `ScriptEngine` 是场景运行时的调度层。
- `IScriptRuntime`/`MonoScriptRuntime` 是托管执行后端。
- `IScriptRuntime` / `MonoScriptRuntime` 是托管执行后端。
## 设计要点
这种拆分和 Unity 一类引擎的设计取向很接近。核心目的不是“为了抽象而抽象”,而是把“场景持久化”“运行时生命周期”“托管语言桥接”三件本来容易互相缠死的事拆开。
- 场景序列化只依赖 `ScriptComponent``ScriptFieldStorage`,不依赖 Mono 是否已经初始化。
- 字段模型分成“类声明默认值、存储覆盖值、活体托管值”三层,便于 Inspector 和调试工具说明字段来源。
- `ScriptEngine` 把生命周期顺序、运行中生成对象追踪、类切换重建都集中在一处
- `IScriptRuntime` 让主流程只依赖契约,后续可以接别的托管后端,而不是把 Mono API 直接扩散到引擎各处
## 为什么当前要这样设计
- 场景、Prefab 或 Inspector 在运行时未启动时也必须能稳定保存脚本绑定和字段覆盖值,因此 `ScriptComponent` 不能直接等价于活体托管对象
- 生命周期顺序、场景内动态创建对象追踪、类切换重建这些规则需要由原生层统一控制,否则更换脚本后端时行为会漂
- 类发现、字段默认值读取和真正实例化都以编译后的程序集为事实来源能让“下拉列表里看到的类”“Inspector 里看到的默认值”“运行时真正创建出的类”尽量来自同一份元数据。
- 把 Mono API、GCHandle、internal call 注册等细节收敛在 `MonoScriptRuntime`,可以避免后端实现扩散到引擎各个模块。
## 托管桥接的当前真实语义
当前 Mono 桥接已经不是单纯“脚本能跑起来”这一层,而是直接暴露了一套和原生状态中心相连的脚本 API 面:
- `Time.deltaTime`
-`MonoScriptRuntime::InvokeMethod()` 在每次生命周期调用前写入 internal call 全局状态。
- `FixedUpdate()` 中读到的是本次 `OnFixedUpdate(fixedDeltaTime)` 传入的参数。
- `Update()` / `LateUpdate()` 中读到的是各自回调传入的逐帧 delta。
- `Time.fixedDeltaTime`
- 不读取某次 fixed tick 的历史参数,而是直接走 `ScriptEngine::GetRuntimeFixedDeltaTime()`
- 也就是说,它反映的是“当前运行时固定步长配置值”,不是“本次 `FixedUpdate()` 实参快照”。
- `Input`
- `Input.GetKey()` / `GetKeyDown()` / `GetKeyUp()``GetButton*()``GetAxis*()``anyKey` / `anyKeyDown` 都是对 [InputManager](../Input/InputManager/InputManager.md) 当前状态的直接转发。
- `GetKeyUp()` 当前对应的是释放边沿语义,也就是原生 `IsKeyReleased()`,不是“当前键没有按住”。
- `anyKey` / `anyKeyDown` 也会把鼠标按钮状态算进去,并继承输入系统的帧边界语义。
如果要进一步理解 `Pressed` / `Released` / `anyKeyDown` 的帧语义,应同时阅读 [Input Flow And Frame Semantics](../../_guides/Input/Input-Flow-and-Frame-Semantics.md)。
## 当前运行链路
1. `SceneRuntime` 启动场景时调用 `ScriptEngine::OnRuntimeStart(scene)`
2. `ScriptEngine` 先通知当前运行时启动,再递归收集场景里现有的 `ScriptComponent`,同时订阅 `Scene::OnGameObjectCreated()` 以追踪运行中创建的新对象。
3. 对满足运行条件的组件,运行时创建托管实例;`MonoScriptRuntime` 会先把 `ScriptFieldStorage` 中同名且类型匹配的字段写入实例。
4. `ScriptEngine` 统一按 `Awake -> OnEnable -> Start -> FixedUpdate/Update/LateUpdate` 驱动生命周期。
5. 每次生命周期调用后,运行时执行 `SyncManagedFieldsToStorage()`;当前 Mono 实现只会回写“本地已存在且类型仍匹配”的字段。
3. 对满足运行条件的组件,运行时创建托管实例;`MonoScriptRuntime` 会先注入 `gameObjectUUID` / `scriptComponentUUID`,再`ScriptFieldStorage` 中同名且类型匹配的字段写入实例。
4. `ScriptEngine` 统一按 `Awake -> OnEnable -> Start -> FixedUpdate / Update / LateUpdate` 驱动生命周期;其中 `Start` 会延后到第一次 `OnUpdate()` 前补发,而不是在 `OnRuntimeStart()` 中立即执行
5. 每次生命周期调用后,运行时都会执行 `SyncManagedFieldsToStorage()`;当前 Mono 实现只会回写“本地已存在且类型仍匹配”的字段。
6. 编辑器或调试工具可通过 `ScriptEngine::TryGetScriptFieldModel()` / `TryGetScriptFieldSnapshots()` 看到字段默认值、覆盖值、活体值以及 `StoredOnly` / `TypeMismatch` 等诊断状态。
## 脚本类发现与重绑定
当前“脚本类下拉列表”和“运行中改类名”的语义已经明确落在源码里:
- `MonoScriptRuntime` 只扫描应用程序集,也就是 `GameScripts.dll` 这一侧的非抽象 `MonoBehaviour` 子类,不会把 `XCEngine.ScriptCore.dll` 里的基础类型暴露成可绑定脚本类。
- `ScriptEngine::TryGetAvailableScriptClasses()` 在运行时列表之上追加两步规范化:
- 可选的按程序集过滤
-`assemblyName -> namespaceName -> className` 排序,并剔除空类名条目
- `ScriptComponent::SetScriptClass()` / `ClearScriptClass()` 是显式重绑定入口。
- 运行时里真正执行“停掉旧实例 -> 重新跟踪 -> 视情况立即创建新实例”的,是 [ScriptEngine::OnScriptComponentClassChanged](ScriptEngine/OnScriptComponentClassChanged.md)。
这套设计和商业引擎里常见的 Inspector 行为一致:类切换不是随便改几个字符串,而是走一条受控的重绑定链路。
## 项目程序集来源
`XCENGINE_ENABLE_MONO_SCRIPTING` 打开时,`managed/CMakeLists.txt` 会构建两类程序集:
- 引擎脚本核心程序集`XCEngine.ScriptCore.dll`
- 游戏脚本程序集`GameScripts.dll`
- 引擎脚本核心程序集: `XCEngine.ScriptCore.dll`
- 游戏脚本程序集: `GameScripts.dll`
除了测试用的 `build/managed` 输出CMake 还会把项目 `project/Assets/**/*.cs` 编译到 `project/Library/ScriptAssemblies/GameScripts.dll``MonoScriptRuntime::Settings` 可以直接指向该目录,因此项目资产脚本和默认字段值已经进入当前文档的真实行为范围
除了测试用的 `build/managed` 输出,同一套 CMake 还会把项目 `project/Assets/**/*.cs` 编译到 `project/Library/ScriptAssemblies/GameScripts.dll`如果项目暂时没有任何 `.cs` 文件,还会生成占位源文件,确保 `GameScripts.dll` 仍然存在
`MonoScriptRuntime::Settings` 可以直接指向 `project/Library/ScriptAssemblies`,因此项目资产脚本、它们的字段默认值、类发现结果,已经进入当前文档的真实行为范围,而不是“未来规划”。
## 当前实现边界
- 当前公开支持的脚本字段类型是有限集合标量、字符串、`Vector2/3/4``GameObject` 引用。
- 当前公开支持的脚本字段类型是有限集合: 标量、字符串、`Vector2/3/4``GameObject` 引用。
- 生命周期覆盖 `Awake / OnEnable / Start / FixedUpdate / Update / LateUpdate / OnDisable / OnDestroy`
- `NullScriptRuntime` 只是桥接占位,不会真正执行脚本代码,也不会返回脚本类列表或字段默认值。
- `MonoScriptRuntime` 目前只发现应用程序集中的非抽象 `MonoBehaviour` 子类,不做热重载、域增量刷新或完整编辑器脚本生态。
- `Time.fixedDeltaTime` 的配置值与 `OnFixedUpdate(fixedDeltaTime)` 的回调参数当前仍是两条通道,上层时间系统需要自己保持一致。
- 字段同步目前不会自动把“运行中新增但本地没有声明”的字段持久化下来。
## 头文件
@@ -67,8 +103,9 @@
## 相关指南
- [Scripting Runtime And Field Model](../../_guides/Scripting/Scripting-Runtime-And-Field-Model.md) - 解释当前脚本系统如何把场景、脚本字段缓存和 Mono 运行时衔接起来,以及为什么这样设计
- [Project Script Assembly And Field Sync](../../_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md) - 解释 `project/Assets/**/*.cs` 如何进入 `GameScripts.dll`,以及默认值、存储覆盖和活体字段如何相互覆盖。
- [Scripting Runtime And Field Model](../../_guides/Scripting/Scripting-Runtime-And-Field-Model.md) - 把场景、脚本字段缓存、运行时调度和 Mono 后端放回同一条真实链路里解释清楚,并补上设计理念
- [Project Script Assembly And Field Sync](../../_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md) - 解释 `project/Assets/**/*.cs` 如何进入 `GameScripts.dll`哪些类会被发现,以及默认值、存储覆盖和活体字段如何相互覆盖。
- [Input Flow And Frame Semantics](../../_guides/Input/Input-Flow-and-Frame-Semantics.md) - 解释托管 `Input` API 当前所继承的原生输入帧语义。
## 相关文档