Files
XCEngine/docs/api/XCEngine/Scripting/Scripting.md

116 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Scripting
**命名空间**: `XCEngine::Scripting`
**类型**: `module`
**描述**: 提供脚本组件数据模型、字段覆盖存储、运行时调度器以及可替换的托管脚本后端抽象。
## 概览
当前脚本模块已经形成一条明确的“场景数据层 -> 运行时调度层 -> 托管后端层”链路:
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) 在没有真实托管后端时兜底,让脚本数据层和编辑流程仍然可工作。
5. [Mono](Mono/Mono.md) 子目录中的 [MonoScriptRuntime](Mono/MonoScriptRuntime/MonoScriptRuntime.md) 是当前唯一的真实托管后端,负责加载 `XCEngine.ScriptCore.dll``GameScripts.dll`、发现脚本类、创建托管实例并完成字段桥接。
如果从商业级游戏引擎常见的分层方式理解:
- `ScriptComponent + ScriptFieldStorage` 是可序列化的数据层。
- `ScriptEngine` 是场景运行时的调度层。
- `IScriptRuntime` / `MonoScriptRuntime` 是托管执行后端。
这种拆分和 Unity 一类引擎的设计取向很接近。核心目的不是“为了抽象而抽象”,而是把“场景持久化”“运行时生命周期”“托管语言桥接”三件本来容易互相缠死的事拆开。
## 为什么当前要这样设计
- 场景、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` 会先注入 `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`
除了测试用的 `build/managed` 输出,同一套 CMake 还会把项目 `project/Assets/**/*.cs` 编译到 `project/Library/ScriptAssemblies/GameScripts.dll`。如果项目暂时没有任何 `.cs` 文件,还会生成占位源文件,确保 `GameScripts.dll` 仍然存在。
`MonoScriptRuntime::Settings` 可以直接指向 `project/Library/ScriptAssemblies`,因此项目资产脚本、它们的字段默认值、类发现结果,已经进入当前文档的真实行为范围,而不是“未来规划”。
## 当前实现边界
- 当前公开支持的脚本字段类型是有限集合: 标量、字符串、`Vector2/3/4``GameObject` 引用。
- 生命周期覆盖 `Awake / OnEnable / Start / FixedUpdate / Update / LateUpdate / OnDisable / OnDestroy`
- `NullScriptRuntime` 只是桥接占位,不会真正执行脚本代码,也不会返回脚本类列表或字段默认值。
- `MonoScriptRuntime` 目前只发现应用程序集中的非抽象 `MonoBehaviour` 子类,不做热重载、域增量刷新或完整编辑器脚本生态。
- `Time.fixedDeltaTime` 的配置值与 `OnFixedUpdate(fixedDeltaTime)` 的回调参数当前仍是两条通道,上层时间系统需要自己保持一致。
- 字段同步目前不会自动把“运行中新增但本地没有声明”的字段持久化下来。
## 头文件
- [IScriptRuntime](IScriptRuntime/IScriptRuntime.md) - `IScriptRuntime.h`
- [NullScriptRuntime](NullScriptRuntime/NullScriptRuntime.md) - `NullScriptRuntime.h`
- [ScriptComponent](ScriptComponent/ScriptComponent.md) - `ScriptComponent.h`
- [ScriptEngine](ScriptEngine/ScriptEngine.md) - `ScriptEngine.h`
- [ScriptField](ScriptField/ScriptField.md) - `ScriptField.h`
- [ScriptFieldStorage](ScriptFieldStorage/ScriptFieldStorage.md) - `ScriptFieldStorage.h`
- [Mono](Mono/Mono.md) - `Mono/`
## 相关指南
- [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 当前所继承的原生输入帧语义。
## 相关文档
- [SceneRuntime](../Scene/SceneRuntime/SceneRuntime.md)
- [Scene](../Scene/Scene.md)
- [上级目录](../XCEngine.md)
- [API 总索引](../../main.md)