docs: update scripting API docs

This commit is contained in:
2026-04-02 22:23:29 +08:00
parent ec2891b16b
commit 3f9e286637
25 changed files with 776 additions and 76 deletions

View File

@@ -10,13 +10,14 @@
## 概览
`IScriptRuntime``ScriptEngine` 唯一应该依赖的脚本后端接口。它把脚本后端抽象成三类能力:
`IScriptRuntime``ScriptEngine` 唯一应该直接依赖的脚本后端接口。当前这份契约覆盖四类能力:
- 运行时启停。
- 托管类/字段元数据查询与字段读写
- 脚本实例创建销毁与生命周期方法调用
- 可用脚本类发现、字段元数据和默认值读取
- 托管字段读写与字段同步
- 脚本实例创建/销毁与生命周期方法调用。
种设计让 `ScriptEngine` 能专注于“调度”,而把 Mono、GCHandle、程序集加载这些实现细节留给具体后端
使得 `ScriptEngine` 只负责“什么时候该调什么”,而把 Mono 域、程序集加载、GCHandle 和 internal call 之类的细节留给具体实现
## 公开概念
@@ -33,6 +34,18 @@
- `OnDisable`
- `OnDestroy`
### ScriptClassDescriptor
`ScriptClassDescriptor` 用来表示一个可绑定脚本类:
| 字段 | 说明 |
|------|------|
| `assemblyName` | 所属程序集,如默认的 `GameScripts`。 |
| `namespaceName` | 托管命名空间,可为空。 |
| `className` | 托管类名。 |
`GetFullName()` 会把命名空间和类名拼成 `Namespace.Class` 形式;`ScriptEngine::TryGetAvailableScriptClasses()` 会直接返回这一结构的排序结果。
### ScriptRuntimeContext
`ScriptRuntimeContext` 是后端执行脚本实例时的最小上下文:
@@ -56,7 +69,9 @@
|------|------|
| [OnRuntimeStart](OnRuntimeStart.md) | 运行时开始时的后端入口。 |
| [OnRuntimeStop](OnRuntimeStop.md) | 运行时停止时的后端入口。 |
| [TryGetAvailableScriptClasses](TryGetAvailableScriptClasses.md) | 返回后端当前可绑定的脚本类列表。 |
| [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) | 查询脚本类字段元数据。 |
| [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md) | 查询脚本类字段默认值。 |
| [TrySetManagedFieldValue](TrySetManagedFieldValue.md) | 向托管实例写字段。 |
| [TryGetManagedFieldValue](TryGetManagedFieldValue.md) | 从托管实例读字段。 |
| [SyncManagedFieldsToStorage](SyncManagedFieldsToStorage.md) | 把托管字段同步回本地存储。 |
@@ -64,6 +79,12 @@
| [DestroyScriptInstance](DestroyScriptInstance.md) | 销毁脚本实例。 |
| [InvokeMethod](InvokeMethod.md) | 调用生命周期方法。 |
## 当前契约边界
- 接口不承诺线程安全;当前调用点默认都在主线程。
- 返回 `false` 可能代表类不存在、后端未初始化,或实现根本不支持该能力;调用方需要按方法语义区分。
- `TryGetClassFieldDefaultValues()` 的默认值应反映托管类构造后的初始字段状态,而不只是原生类型零值。
## 相关文档
- [ScriptEngine](../ScriptEngine/ScriptEngine.md)

View File

@@ -0,0 +1,30 @@
# IScriptRuntime::TryGetAvailableScriptClasses
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/IScriptRuntime.h`
## 签名
```cpp
virtual bool TryGetAvailableScriptClasses(
std::vector<ScriptClassDescriptor>& outClasses) const = 0;
```
## 作用
返回当前后端已经发现、可以绑定给 `ScriptComponent` 的脚本类描述列表。
## 返回值语义
- 返回 `true`:后端支持类发现,且当前返回的数据可用。
- 返回 `false`:后端未初始化、当前没有这项能力,或发现流程失败。
调用前实现应清空 `outClasses``ScriptEngine::TryGetAvailableScriptClasses()` 会在此基础上继续过滤空类名并排序。
## 相关文档
- [IScriptRuntime](IScriptRuntime.md)
- [ScriptEngine::TryGetAvailableScriptClasses](../ScriptEngine/TryGetAvailableScriptClasses.md)

View File

@@ -0,0 +1,40 @@
# IScriptRuntime::TryGetClassFieldDefaultValues
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/IScriptRuntime.h`
## 签名
```cpp
virtual bool TryGetClassFieldDefaultValues(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldDefaultValue>& outFields) const = 0;
```
## 作用
查询某个脚本类在初始状态下的字段默认值。
## 设计意义
这个接口不是简单返回“每种类型的零值”。当前 `ScriptEngine` 用它来:
- 构建 `ScriptFieldModel``defaultValue`
-`ClearScriptFieldOverrides()` 时把活体托管字段重置回类默认值
因此更合理的实现应该尽量反映托管类构造后的真实字段状态。
## 返回值语义
- 返回 `true`:后端确认类存在,并成功返回默认值列表。
- 返回 `false`:类不存在、后端未初始化,或后端不支持默认值提取。
## 相关文档
- [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md)
- [ScriptEngine::TryGetScriptFieldModel](../ScriptEngine/TryGetScriptFieldModel.md)

View File

@@ -8,15 +8,15 @@
## 概览
`docs/api/XCEngine/Scripting/Mono` 对应的是 `engine/include/XCEngine/Scripting/Mono` 子目录。它不是独立命名空间,而是脚本模块下按实现后端划分出的子目录。
`docs/api/XCEngine/Scripting/Mono` 对应 `engine/include/XCEngine/Scripting/Mono` 子目录。它不是独立命名空间,而是脚本模块按后端实现拆出来的子目录。
当前这里的核心类型只有 [MonoScriptRuntime](MonoScriptRuntime/MonoScriptRuntime.md)。它负责:
当前这里唯一的公开类型是 [MonoScriptRuntime](MonoScriptRuntime/MonoScriptRuntime.md)。它负责:
- 初始化 Mono root domain 和 app domain。
- 加载脚本核心程序集游戏程序集。
- 发现可用脚本类和公共实例字段
- `ScriptComponent``GameObject` 与托管对象实例绑定起来
- 在生命周期调用后把托管字段同步回本地存储
- 解析 `MonoScriptRuntime::Settings`,推导程序集目录、脚本核心程序集游戏程序集路径
- 加载 `XCEngine.ScriptCore.dll``GameScripts.dll`
- 发现应用程序集中的非抽象 `MonoBehaviour` 子类,并缓存支持的公共实例字段与生命周期方法
- `ScriptComponent` 创建托管实例,注入 `GameObject`/`ScriptComponent` 上下文,并在生命周期后同步字段
## 为什么单独分目录
@@ -25,11 +25,21 @@
- 可以清楚区分“脚本系统公共契约”和“具体后端实现”。
- 以后如果接入别的脚本后端,这里天然就是平行扩展点。
## 程序集来源
当前 Mono 后端既支持测试程序集目录,也支持项目资产脚本程序集目录:
- `managed/CMakeLists.txt` 会构建基础 `XCEngine.ScriptCore.dll``GameScripts.dll`
- 同一份 CMake 还会把 `project/Assets/**/*.cs` 编译到 `project/Library/ScriptAssemblies/GameScripts.dll`
- `tests/scripting/test_project_script_assembly.cpp` 已验证运行时能从项目程序集目录发现 `ProjectScripts.ProjectScriptProbe` 及其默认字段值。
## 当前实现边界
- 当前只实现了 Mono 后端,没有并列的 IL2CPP、Lua 或自研 VM 后端。
- 目录里只有一个 public header说明当前重点仍然是把单条托管脚本链路先跑顺
- 内部调用注册已覆盖 `GameObject``Transform``Camera``Light``MeshFilter``MeshRenderer` 和基础日志/时间桥接,但远不是完整编辑器级 API 面
- 目录里只有一个 public header说明当前重点仍然是把一条 Mono 托管链路跑通
- 类发现只扫描当前应用程序集里的非抽象 `MonoBehaviour` 子类,不会把工具类或抽象基类暴露给脚本绑定 UI
- 字段发现只接受受支持类型的 `public``static` 实例字段。
- internal call 目前已覆盖 `GameObject``Transform``Camera``Light``MeshFilter``MeshRenderer`、日志和时间桥接,但远不是完整编辑器级脚本 API 面。
## 头文件
@@ -38,6 +48,7 @@
## 相关指南
- [Scripting Runtime And Field Model](../../../_guides/Scripting/Scripting-Runtime-And-Field-Model.md)
- [Project Script Assembly And Field Sync](../../../_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md)
## 相关文档

View File

@@ -6,7 +6,7 @@
**头文件**: `XCEngine/Scripting/Mono/MonoScriptRuntime.h`
**描述**: 基于 Mono 的脚本运行时实现,负责程序集加载、类发现、实例创建、internal call 桥接和生命周期调用。
**描述**: 基于 Mono 的脚本运行时实现,负责程序集解析与加载、类发现、实例创建、字段桥接、默认值读取和生命周期调用。
## 概览
@@ -14,10 +14,28 @@
- 初始化 root domain 和 app domain。
- 加载脚本核心程序集与游戏程序集。
- 发现继承 `MonoBehaviour` 的可用脚本类。
- 构建类元数据缓存
- 发现继承 `MonoBehaviour` 的可用脚本类,并缓存生命周期方法与字段元数据
- 读取类字段默认值
- 创建和销毁脚本实例。
- 通过 internal call 让托管脚本访问原生 `GameObject``Transform``Camera` 等能力。
- 把本地字段覆盖写入托管实例,并在生命周期之后同步回 `ScriptFieldStorage`
## Settings
构造函数接收一个 `Settings` 结构体,当前公开字段如下:
| 字段 | 说明 |
|------|------|
| `assemblyDirectory` | 程序集目录;若 `coreAssemblyPath` / `appAssemblyPath` 缺失,会以它为基准推导 DLL 路径。 |
| `corlibDirectory` | Mono 解析 `mscorlib.dll` 的目录;为空时会回退到 `assemblyDirectory``coreAssemblyPath` 所在目录。 |
| `coreAssemblyPath` | `XCEngine.ScriptCore.dll` 的完整路径。 |
| `appAssemblyPath` | `GameScripts.dll` 的完整路径。 |
| `coreAssemblyName` | 脚本核心程序集名,默认 `XCEngine.ScriptCore`。 |
| `appAssemblyName` | 应用程序集名,默认 `GameScripts`。 |
| `baseNamespace` | 核心托管 API 的命名空间,默认 `XCEngine`。 |
| `baseClassName` | 作为脚本基类查找入口的类型名,默认 `MonoBehaviour`。 |
`ResolveSettings()` 会在构造时和 `Initialize()` 前再次运行,补全目录和程序集路径。因此只提供 `assemblyDirectory` 也是合法用法;`tests/scripting/test_project_script_assembly.cpp` 就是基于项目脚本程序集目录这样初始化的。
## 生命周期
@@ -26,26 +44,22 @@
- [Shutdown](Shutdown.md) 会销毁 app domain、清空类缓存与实例缓存。
- 析构会自动调用 `Shutdown()`
## 常用访问器
- `IsInitialized()`
- `GetSettings()`
- `GetLastError()`
这些访问器主要用于测试、诊断和工具层。当前文档对 `GetLastError()` 单独补页,因为它直接关系到失败排查。
## 设计要点
- `ScriptEngine` 不直接碰 Mono API所有后端细节收敛在这个类里。
- 类缓存和实例缓存都基于稳定键,便于场景重建和脚本回绑。
- internal call 注册集中在本实现里,说明当前托管 API 面是围绕 Mono 后端组织的,而不是独立脚本 ABI
- 类缓存和实例缓存都基于稳定键,便于场景重建、类切换和实例回绑。
- `TryGetClassFieldDefaultValues()` 会创建一个临时托管对象并读取字段值,因此脚本字段默认值可以反映 C# 初始化表达式,而不只是原生零值
- `CreateScriptInstance()` 会先注入上下文 UUID再把本地存储里同名且类型匹配的字段写入托管实例。
- `SyncManagedFieldsToStorage()` 只回写本地已有字段,保证运行时不会把临时字段偷偷持久化到场景。
## 当前实现边界
- 当前只发现应用程序集中的非抽象 `MonoBehaviour` 子类。
- 支持的公共字段类型只覆盖 `float / double / bool / int32 / uint64 / string / Vector2 / Vector3 / Vector4 / GameObject`
- `SyncManagedFieldsToStorage()` 只会回写已经存在于 `ScriptFieldStorage` 中的字段;运行时临时字段不会自动持久化
- `OnRuntimeStop()` 只清理当前活动场景与实例,不会做完整 `Shutdown()`
- `TryGetAvailableScriptClasses()``GetScriptClassNames()` 和字段元数据查询都要求运行时已经初始化完成
- `SyncManagedFieldsToStorage()` 只会回写已经存在于 `ScriptFieldStorage` 中且类型仍匹配的字段;运行时临时字段不会自动持久化
- `OnRuntimeStop()` 只清理当前活动场景与实例,不会做完整 `Shutdown()`;程序集和类缓存会保留到显式 `Shutdown()` 或析构。
- 目前没有程序集热重载、增量编译监听或多场景并发运行支持。
## 公开方法
@@ -58,7 +72,11 @@
| [GetLastError](GetLastError.md) | 读取最近一次错误描述。 |
| [IsClassAvailable](IsClassAvailable.md) | 查询脚本类是否已发现。 |
| [GetScriptClassNames](GetScriptClassNames.md) | 返回已发现脚本类名列表。 |
| `IsInitialized()` | 判断运行时是否已完成初始化。 |
| `GetSettings()` | 返回已解析的运行时设置。 |
| [TryGetAvailableScriptClasses](TryGetAvailableScriptClasses.md) | 返回完整脚本类描述列表。 |
| [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) | 读取脚本类字段元数据。 |
| [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md) | 读取脚本类字段默认值。 |
| [HasManagedInstance](HasManagedInstance.md) | 判断某脚本组件是否已有托管实例。 |
| [GetManagedInstanceCount](GetManagedInstanceCount.md) | 返回当前托管实例数。 |
| [GetManagedInstanceObject](GetManagedInstanceObject.md) | 读取托管对象裸指针。 |
@@ -77,6 +95,7 @@
- `engine/src/Scripting/Mono/MonoScriptRuntime.cpp`
- `tests/scripting/test_mono_script_runtime.cpp`
- `tests/scripting/test_project_script_assembly.cpp`
## 相关文档
@@ -84,3 +103,4 @@
- [IScriptRuntime](../../IScriptRuntime/IScriptRuntime.md)
- [ScriptEngine](../../ScriptEngine/ScriptEngine.md)
- [Scripting Runtime And Field Model](../../../../_guides/Scripting/Scripting-Runtime-And-Field-Model.md)
- [Project Script Assembly And Field Sync](../../../../_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md)

View File

@@ -0,0 +1,32 @@
# MonoScriptRuntime::TryGetAvailableScriptClasses
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/Mono/MonoScriptRuntime.h`
## 签名
```cpp
bool TryGetAvailableScriptClasses(
std::vector<ScriptClassDescriptor>& outClasses) const override;
```
## 当前实现流程
1. 清空 `outClasses`
2. 若运行时尚未初始化,直接返回 `false`
3. 遍历 `m_classes` 缓存,把每个条目转换成 `ScriptClassDescriptor`
4.`assemblyName -> namespaceName -> className` 排序后返回。
## 返回内容边界
- 只来自当前已发现的应用程序集脚本类缓存。
- 不包含抽象类。
- 不包含非 `MonoBehaviour` 子类。
## 相关文档
- [MonoScriptRuntime](MonoScriptRuntime.md)
- [ScriptEngine::TryGetAvailableScriptClasses](../../ScriptEngine/TryGetAvailableScriptClasses.md)

View File

@@ -0,0 +1,48 @@
# MonoScriptRuntime::TryGetClassFieldDefaultValues
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/Mono/MonoScriptRuntime.h`
## 签名
```cpp
bool TryGetClassFieldDefaultValues(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldDefaultValue>& outFields) const override;
```
## 当前实现流程
1. 清空 `outFields`
2.`(assemblyName, namespaceName, className)` 查找缓存的类元数据。
3. 切换到当前 app domain。
4. 创建一个临时托管对象并执行默认构造。
5. 遍历缓存字段,逐个读取字段值并写入 `ScriptFieldDefaultValue`
6.`fieldName` 排序后返回。
## 为什么它重要
这里返回的是托管类真实初始化后的字段值,所以它能反映:
- C# 字段初始化表达式
- 默认构造后留下的初始状态
`ScriptEngine::TryGetScriptFieldModel()``ClearScriptFieldOverrides()` 都依赖这条数据。
## 失败路径
- 类找不到
- Mono 无法创建临时对象
- 任意字段读取失败
这些情况都会返回 `false`,并清空输出。
## 相关文档
- [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md)
- [Project Script Assembly And Field Sync](../../../../_guides/Scripting/Project-Script-Assembly-And-Field-Sync.md)

View File

@@ -10,18 +10,20 @@
## 概览
`NullScriptRuntime`作用不是执行脚本,而是让整个脚本系统在“没有真实脚本后端”的情况下仍然保持接口闭合:
`NullScriptRuntime`职责不是执行脚本,而是在“没有真实托管环境”时维持脚本系统接口闭合:
- `ScriptEngine` 仍然可以安全持有一个运行时对象
- 场景和脚本组件的原生数据层仍然可以工作。
- 测试或工具链不需要为“没有运行时时指针为空”额外兜底
- `ScriptEngine` 默认就持有它,因此不会出现运行时指针为空的问题
- `ScriptComponent``ScriptFieldStorage`、场景序列化和字段编辑流程都仍然可以工作。
- 编辑器或测试可以先走完整的脚本数据链路,再按需接入真实 Mono 后端
这是一种很典型的 Null Object 模式,在商业引擎底层模块中很常见
这是标准的 Null Object 模式,但它的“成功”语义必须按真实实现理解
## 当前实现行为
- 运行时启停是 no-op。
- 可用脚本类查询总是失败,并清空输出数组。
- 元数据查询总是失败,并清空输出数组。
- 默认值查询总是失败,并清空输出数组。
- 托管写字段总是返回 `true`,相当于“我接受这个请求,但没有实际后端可写”。
- 托管读字段总是返回 `false`
- 同步字段是 no-op。
@@ -43,7 +45,9 @@
|------|------|
| [OnRuntimeStart](OnRuntimeStart.md) | 空实现。 |
| [OnRuntimeStop](OnRuntimeStop.md) | 空实现。 |
| [TryGetAvailableScriptClasses](TryGetAvailableScriptClasses.md) | 始终失败并清空输出。 |
| [TryGetClassFieldMetadata](TryGetClassFieldMetadata.md) | 始终失败并清空输出。 |
| [TryGetClassFieldDefaultValues](TryGetClassFieldDefaultValues.md) | 始终失败并清空输出。 |
| [TrySetManagedFieldValue](TrySetManagedFieldValue.md) | 始终返回 `true`。 |
| [TryGetManagedFieldValue](TryGetManagedFieldValue.md) | 始终返回 `false`。 |
| [SyncManagedFieldsToStorage](SyncManagedFieldsToStorage.md) | 空实现。 |

View File

@@ -0,0 +1,28 @@
# NullScriptRuntime::TryGetAvailableScriptClasses
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/NullScriptRuntime.h`
## 签名
```cpp
bool TryGetAvailableScriptClasses(
std::vector<ScriptClassDescriptor>& outClasses) const override;
```
## 当前实现行为
- 清空 `outClasses`
- 直接返回 `false`
## 含义
空运行时不会伪造脚本类列表。需要可绑定脚本类时,调用方必须接入真实后端,如 `MonoScriptRuntime`
## 相关文档
- [NullScriptRuntime](NullScriptRuntime.md)
- [ScriptEngine::TryGetAvailableScriptClasses](../ScriptEngine/TryGetAvailableScriptClasses.md)

View File

@@ -0,0 +1,32 @@
# NullScriptRuntime::TryGetClassFieldDefaultValues
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/NullScriptRuntime.h`
## 签名
```cpp
bool TryGetClassFieldDefaultValues(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldDefaultValue>& outFields) const override;
```
## 当前实现行为
- 忽略输入参数
- 清空 `outFields`
- 直接返回 `false`
## 含义
空运行时不会提供类默认值,因此 `ScriptEngine` 在这种模式下会回退到按字段类型生成默认值。
## 相关文档
- [NullScriptRuntime](NullScriptRuntime.md)
- [IScriptRuntime::TryGetClassFieldDefaultValues](../IScriptRuntime/TryGetClassFieldDefaultValues.md)

View File

@@ -0,0 +1,32 @@
# ScriptComponent::ClearScriptClass
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/ScriptComponent.h`
## 签名
```cpp
void ClearScriptClass();
```
## 作用
清空当前脚本组件的命名空间和类名绑定。
## 当前实现行为
- 内部等价于调用 `SetScriptClass(m_assemblyName, "", "")`
- 当前 `assemblyName` 会被保留。
- 如果运行时正在运行且该组件原本已有脚本类,`ScriptEngine` 会收到类变化通知,并销毁旧跟踪实例。
## 设计含义
“清空绑定”不是单纯改两个字符串。对运行时来说,它意味着这个组件不再有可执行脚本类,应该停止继续调度生命周期。
## 相关文档
- [SetScriptClass](SetScriptClass.md)
- [ScriptEngine::OnScriptComponentClassChanged](../ScriptEngine/OnScriptComponentClassChanged.md)

View File

@@ -6,22 +6,25 @@
**头文件**: `XCEngine/Scripting/ScriptComponent.h`
**描述**: 挂在 `GameObject` 上的脚本绑定组件,负责保存脚本类标识、组件 UUID 和字段存储。
**描述**: 挂在 `GameObject` 上的脚本绑定组件,负责保存脚本类标识、组件 UUID 和字段覆盖存储。
## 概览
`ScriptComponent` 是脚本系统的数据入口。它本身不执行脚本逻辑,而是保存三类信息:
`ScriptComponent` 是脚本系统的数据入口。它本身不执行托管代码,而是保存三类关键信息:
- 这个组件绑定的是哪个程序集、命名空间和类。
- 这个脚本组件自己的稳定 UUID。
- 这份脚本实例的可持久化字段缓存 `ScriptFieldStorage`
这和 Unity 场景里挂着的 `MonoBehaviour` 序列化槽位很接近,但当前实现更明确地把“数据层”和“运行时实例层”拆开了
这和 Unity 场景里挂着的 `MonoBehaviour` 槽位很接近,但当前实现更明确地区分了“原生数据层”和“运行时实例层”。
## 生命周期
- 构造时会生成一个非零随机 `scriptComponentUUID`
- 默认程序集名是 `GameScripts`
- 首次绑定脚本类时,会通知 `ScriptEngine::OnScriptComponentEnabled()`
- 已绑定脚本类发生变化时,会通知 `ScriptEngine::OnScriptComponentClassChanged()`,触发当前运行时实例停机并按新类重建。
- `ClearScriptClass()` 会保留当前 `assemblyName`,只清空命名空间和类名。
- 启用、禁用、销毁回调会直接转发给 `ScriptEngine`
- 序列化/反序列化会持久化 UUID、脚本类绑定和字段存储内容。
@@ -32,9 +35,9 @@
## 当前实现边界
- `SetScriptClass()` 只有在“之前没有脚本类,现在有了”这个转换点,才会主动通知 `ScriptEngine::OnScriptComponentEnabled()`
- 单纯修改已有脚本类名,不会自动触发一轮完整的重绑定流程。
- 只有 `SetScriptClass()` / `ClearScriptClass()` 会通知 `ScriptEngine``SetAssemblyName()``SetNamespaceName()``SetClassName()` 是纯字段写入,不会自动触发重绑定
- 反序列化使用引擎私有的分号分隔文本格式,不是通用 JSON/YAML。
- `SetFieldStorage()` 直接整体覆盖本地字段缓存,不会自动把活体托管实例同步到同一状态。
## 常用访问器
@@ -43,7 +46,7 @@
- `GetClassName()` / `SetClassName()`
- `GetScriptComponentUUID()`
这些访问器大多是简单内联函数,因此当前文档重点放在会改变生命周期或序列化语义的核心方法上。
这些访问器大多是简单内联函数,因此文档重点放在会影响运行时重建、字段语义或序列化行为的方法上。
## 公开方法
@@ -51,6 +54,7 @@
|------|------|
| [Constructor](Constructor.md) | 创建组件并生成 UUID。 |
| [SetScriptClass](SetScriptClass.md) | 设置脚本类绑定。 |
| [ClearScriptClass](ClearScriptClass.md) | 清空脚本类绑定。 |
| [HasScriptClass](HasScriptClass.md) | 判断当前是否已经绑定脚本类。 |
| [GetFullClassName](GetFullClassName.md) | 返回带命名空间的完整类名。 |
| [GetFieldStorage](GetFieldStorage.md) | 访问持久化字段缓存。 |
@@ -64,6 +68,7 @@
- `engine/src/Scripting/ScriptComponent.cpp`
- `tests/scripting/test_script_component.cpp`
- `tests/scripting/test_script_engine.cpp`
## 相关文档

View File

@@ -27,13 +27,17 @@ void SetScriptClass(
- 两个重载都会先记录“之前是否已经有脚本类”。
- 然后覆盖程序集名、命名空间和类名。
- 只有在“之前没有脚本类,设置后有脚本类”时,才会主动调用 `ScriptEngine::Get().OnScriptComponentEnabled(this)`
- 如果“之前没有脚本类,设置后有脚本类”,会调用 `ScriptEngine::Get().OnScriptComponentEnabled(this)`
- 如果之前已经绑定脚本类,并且程序集名 / 命名空间 / 类名发生变化,会调用 `ScriptEngine::Get().OnScriptComponentClassChanged(this)`
## 设计含义
当前实现把“首次绑定脚本类”视作一个启用事件,但并没有把“换到另一个脚本类”也当成完整重建流程。这是一个当前版本的真实边界,用户不应该误以为修改类名会自动完成热切换
- 当前实现把“首次绑定脚本类”视作启用事件。
- 已绑定类发生变化时,`ScriptEngine` 会停掉旧实例并按新类重新跟踪,这已经是当前实现的一部分。
-`SetAssemblyName()` / `SetNamespaceName()` / `SetClassName()` 这些原始 setter 不会触发同样的流程;需要真正重绑定时应走 `SetScriptClass()`
## 相关文档
- [ClearScriptClass](ClearScriptClass.md)
- [HasScriptClass](HasScriptClass.md)
- [OnEnable](OnEnable.md)
- [ScriptEngine::OnScriptComponentClassChanged](../ScriptEngine/OnScriptComponentClassChanged.md)

View File

@@ -0,0 +1,45 @@
# ScriptEngine::ApplyScriptFieldWrites
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/ScriptEngine.h`
## 签名
```cpp
bool ApplyScriptFieldWrites(
ScriptComponent* component,
const std::vector<ScriptFieldWriteRequest>& requests,
std::vector<ScriptFieldWriteResult>& outResults);
```
## 作用
批量写脚本字段,并为每个请求返回独立状态。
## 当前实现流程
1. 先调用 `TryGetScriptFieldModel()` 建立字段模型。
2. 针对每个请求逐项校验:
- 字段名不能为空
- 值必须和声明类型兼容
- 字段必须存在于当前模型
- 如果类元数据可用,不能给 `StoredOnly` 遗留字段写值
- 类型必须与模型字段类型一致
3. 对已声明字段走 `TrySetScriptFieldValue()`,保证活体实例和本地存储同步更新。
4. 对“类元数据缺失但本地仍有字段”的场景,允许直接写 `ScriptFieldStorage`
5. 把每条结果写入 `outResults`
## 返回值语义
- 只有所有请求状态都是 `Applied` 时,返回 `true`
- 只要出现一项失败或诊断状态,就返回 `false`
但无论整体布尔值如何,`outResults` 都是逐项结果的权威来源。
## 相关文档
- [ClearScriptFieldOverrides](ClearScriptFieldOverrides.md)
- [ScriptField](../ScriptField/ScriptField.md)

View File

@@ -0,0 +1,40 @@
# ScriptEngine::ClearScriptFieldOverrides
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/ScriptEngine.h`
## 签名
```cpp
bool ClearScriptFieldOverrides(
ScriptComponent* component,
const std::vector<ScriptFieldClearRequest>& requests,
std::vector<ScriptFieldClearResult>& outResults);
```
## 作用
批量清理字段覆盖,并在可能时把活体托管字段恢复到类默认值。
## 当前实现流程
1. 先构建 `ScriptFieldModel`,再收集类默认值表。
2. 对每个请求校验字段名是否为空、字段是否存在于模型。
3. 如果字段属于当前脚本类且活体实例存在:
- 调用运行时 `TrySetManagedFieldValue()`
- 把托管值重置为该字段的类默认值
4. 如果本地存储中有同名覆盖项,则移除该存储项。
5. 如果既没有活体值可重置,也没有存储覆盖可删除,结果为 `NoValueToClear`
## 返回值语义
- 只有所有请求都成功清理时,返回 `true`
- 任一请求出现 `UnknownField``EmptyFieldName``NoValueToClear``ApplyFailed`,整体返回 `false`
## 相关文档
- [ApplyScriptFieldWrites](ApplyScriptFieldWrites.md)
- [ScriptEngine::TryGetScriptFieldModel](TryGetScriptFieldModel.md)

View File

@@ -0,0 +1,37 @@
# ScriptEngine::OnScriptComponentClassChanged
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/ScriptEngine.h`
## 签名
```cpp
void OnScriptComponentClassChanged(ScriptComponent* component);
```
## 作用
处理脚本组件在运行时中的类绑定变化。
## 当前实现流程
1. 忽略空指针。
2. 如果运行时未启动,直接返回。
3. 若该组件当前已被跟踪,则先执行 `StopTrackingScript(..., false)`
- 如有实例,会触发 `OnDisable -> OnDestroy -> DestroyScriptInstance`
- 然后从跟踪表移除
4. 如果组件已经没有脚本类,流程结束。
5. 否则按新类重新 `TrackScriptComponent()`
6. 若新状态满足 `ShouldScriptRun()`,立即 `EnsureScriptReady(..., true)`,从而创建新实例并触发 `Awake / OnEnable`
## 使用场景
`ScriptComponent::SetScriptClass()` 在已绑定类发生变化时会调用这里;`ClearScriptClass()` 也会走同一条停机路径。
## 相关文档
- [OnScriptComponentEnabled](OnScriptComponentEnabled.md)
- [ScriptComponent::SetScriptClass](../ScriptComponent/SetScriptClass.md)

View File

@@ -6,7 +6,7 @@
**头文件**: `XCEngine/Scripting/ScriptEngine.h`
**描述**: 当前脚本系统的总调度器,负责运行时启停、脚本实例追踪、生命周期调用字段模型拼装。
**描述**: 当前脚本系统的总调度器,负责运行时启停、脚本实例追踪、生命周期调用、脚本类发现和字段模型拼装。
## 概览
@@ -19,9 +19,11 @@
当前它的核心职责包括:
- 在运行时开始时收集场景中的脚本组件并建立追踪表。
- 订阅 `Scene::OnGameObjectCreated()`,把运行中创建的新对象也纳入脚本追踪。
- 根据对象激活状态、组件启用状态和脚本类绑定状态决定脚本是否应当运行。
- 按顺序创建实例、调用 `Awake / OnEnable / Start / Update...`
- 在运行时字段、本地字段缓存类元数据之间建立一致的读取模型。
- 在运行时字段、本地字段缓存类元数据和类默认值之间建立一致的读取模型。
- 为编辑器/Inspector 提供脚本类列表、字段模型、批量写入和清除覆盖能力。
## 生命周期
@@ -43,6 +45,17 @@
这说明当前生命周期状态是显式状态机,而不是每次调用都从托管世界反查。
## 脚本类与字段模型
除了生命周期调度,`ScriptEngine` 还负责把运行时暴露成编辑器可消费的数据接口:
- `TryGetAvailableScriptClasses()` 返回排序后的 `ScriptClassDescriptor` 列表,并可按程序集过滤。
- `TryGetScriptFieldModel()` 会把类声明字段、运行时默认值、存储覆盖值和活体托管值融合成 `ScriptFieldModel`
- `ApplyScriptFieldWrites()` 会逐项返回 `Applied / UnknownField / TypeMismatch / StoredOnlyField` 等状态。
- `ClearScriptFieldOverrides()` 会把声明字段重置回类默认值,并删除本地存储中的覆盖项。
这也是当前 Inspector 和脚本字段编辑工具最该依赖的 API 面。
## 线程语义
- 当前实现没有锁。
@@ -53,8 +66,9 @@
- 只跟踪当前运行场景里的脚本组件。
- `Start` 生命周期会在第一次 `OnUpdate()` 前补发一次,而不是在 `OnRuntimeStart()` 里立即调用。
- `TrySetScriptFieldValue()` 只有在后端能返回类字段元数据时,才会强校验字段名和类型。
- `TryGetScriptFieldModel()`把类元数据、运行时值和本地存储值融合成一份快照模型,这对调试和编辑器非常重要
- `TrySetScriptFieldValue()` 只有在后端能返回类字段元数据时,才会强校验字段名和类型;否则会退回到纯本地存储写入
- `TryGetScriptFieldModel()`优先使用运行时返回的类默认值,而不是简单地把每种类型置零
- `TryGetScriptFieldSnapshots()` 在模型成功但字段为空时会返回 `false`,调用方不能把“返回 `false`”简单等同于“接口失败”。
## 常用访问器
@@ -78,11 +92,15 @@
| [OnScriptComponentEnabled](OnScriptComponentEnabled.md) | 处理脚本组件启用事件。 |
| [OnScriptComponentDisabled](OnScriptComponentDisabled.md) | 处理脚本组件禁用事件。 |
| [OnScriptComponentDestroyed](OnScriptComponentDestroyed.md) | 处理脚本组件销毁事件。 |
| [OnScriptComponentClassChanged](OnScriptComponentClassChanged.md) | 处理脚本类绑定变化,销毁旧实例并按新类重建跟踪。 |
| [HasTrackedScriptComponent](HasTrackedScriptComponent.md) | 查询某组件是否被跟踪。 |
| [HasRuntimeInstance](HasRuntimeInstance.md) | 查询某组件是否已有运行时实例。 |
| [GetTrackedScriptCount](GetTrackedScriptCount.md) | 返回当前跟踪脚本数。 |
| [TryGetAvailableScriptClasses](TryGetAvailableScriptClasses.md) | 返回可绑定脚本类列表。 |
| [TrySetScriptFieldValue](TrySetScriptFieldValue.md) | 写脚本字段。 |
| [TryGetScriptFieldValue](TryGetScriptFieldValue.md) | 读脚本字段。 |
| [ApplyScriptFieldWrites](ApplyScriptFieldWrites.md) | 批量写脚本字段并返回逐项状态。 |
| [ClearScriptFieldOverrides](ClearScriptFieldOverrides.md) | 批量清理字段覆盖并恢复默认值。 |
| [TryGetScriptFieldModel](TryGetScriptFieldModel.md) | 构建完整字段模型。 |
| [TryGetScriptFieldSnapshots](TryGetScriptFieldSnapshots.md) | 直接返回字段快照数组。 |
@@ -91,6 +109,7 @@
- `engine/src/Scripting/ScriptEngine.cpp`
- `tests/scripting/test_script_engine.cpp`
- `tests/Scene/test_scene_runtime.cpp`
- `tests/scripting/test_mono_script_runtime.cpp`
## 相关文档

View File

@@ -0,0 +1,37 @@
# ScriptEngine::TryGetAvailableScriptClasses
**命名空间**: `XCEngine::Scripting`
**类型**: `method`
**头文件**: `XCEngine/Scripting/ScriptEngine.h`
## 签名
```cpp
bool TryGetAvailableScriptClasses(
std::vector<ScriptClassDescriptor>& outClasses,
const std::string& assemblyName = std::string()) const;
```
## 作用
返回当前运行时可绑定的脚本类列表,并可按程序集过滤。
## 当前实现流程
1. 清空 `outClasses`
2. 调用运行时 `TryGetAvailableScriptClasses()`
3. 若传入了 `assemblyName`,只保留匹配该程序集的类。
4. 过滤掉 `className` 为空的无效描述。
5.`assemblyName -> namespaceName -> className` 排序。
## 返回值语义
- 返回 `true`:运行时支持类发现,排序/过滤后的结果可用。
- 返回 `false`:运行时不支持或当前不能返回类列表。
## 相关文档
- [ScriptEngine](ScriptEngine.md)
- [IScriptRuntime::TryGetAvailableScriptClasses](../IScriptRuntime/TryGetAvailableScriptClasses.md)

View File

@@ -30,7 +30,7 @@ bool TryGetScriptFieldModel(
对于运行时可见的每个字段:
-字段类型生成默认值。
-尝试用运行时 `TryGetClassFieldDefaultValues()` 返回的类默认值;拿不到时才回退到按字段类型生成零值/空值。
- 如果本地存储里有同名字段,则记录 `storedType / storedValue`
- 若类型不匹配,标记 `TypeMismatch`
- 若托管实例可读该字段,则当前值来源记为 `ManagedValue`
@@ -52,7 +52,7 @@ bool TryGetScriptFieldModel(
这套模型不是“为了文档好看”,而是当前脚本系统里最接近商业引擎 Inspector 数据模型的一层。它让工具或调试界面能回答这些关键问题:
- 这个字段类里声明了吗?
- 当前显示的是托管值还是存储值?
- 当前显示的是类默认值、存储值还是托管值?
- 本地存储是否已经和类定义不匹配?
## 相关文档

View File

@@ -6,7 +6,7 @@
**头文件**: `XCEngine/Scripting/ScriptField.h`
**描述**: 定义脚本字段类型系统、字段快照模型以及序列化/反序列化辅助函数。
**描述**: 定义脚本字段类型系统、字段快照模型、批量写入状态以及序列化/反序列化辅助函数。
## 概览
@@ -46,15 +46,58 @@
- `Math::Vector4`
- `GameObjectReference`
### ScriptFieldMetadata / ScriptFieldDefaultValue
- `ScriptFieldMetadata` 表示类里声明了一个什么字段。
- `ScriptFieldDefaultValue` 表示该字段在托管类初始状态下的默认值。
`MonoScriptRuntime::TryGetClassFieldDefaultValues()` 会直接构造托管对象并读取这一层数据,因此它可以反映 C# 字段初始化表达式。
### ScriptFieldSnapshot / ScriptFieldModel
它们用于字段面板、字段模型比对和“已声明字段 vs 已存储字段”诊断。`ScriptEngine::TryGetScriptFieldModel()`到这组结构。
它们用于字段面板、字段模型比对和“已声明字段 vs 已存储字段”诊断。`ScriptEngine::TryGetScriptFieldModel()`把默认值、存储值和活体托管值融合到这组结构
相关状态枚举:
- `ScriptFieldClassStatus`
`Unassigned / Available / Missing`
- `ScriptFieldValueSource`
`None / DefaultValue / StoredValue / ManagedValue`
- `ScriptFieldIssue`
`None / StoredOnly / TypeMismatch`
### ScriptFieldWriteRequest / ScriptFieldWriteResult
这组结构用于 `ScriptEngine::ApplyScriptFieldWrites()` 的批量写接口。
`ScriptFieldWriteStatus` 当前包括:
- `Applied`
- `EmptyFieldName`
- `UnknownField`
- `InvalidValue`
- `TypeMismatch`
- `StoredOnlyField`
- `ApplyFailed`
### ScriptFieldClearRequest / ScriptFieldClearResult
这组结构用于 `ScriptEngine::ClearScriptFieldOverrides()` 的批量清理接口。
`ScriptFieldClearStatus` 当前包括:
- `Applied`
- `EmptyFieldName`
- `UnknownField`
- `NoValueToClear`
- `ApplyFailed`
## 设计要点
- 字段类型集合是显式白名单,而不是任意模板反射。
- 文本序列化逻辑集中在这个头对应的实现里,保证 `ScriptFieldStorage` 和脚本组件共用同一套规则。
- `StoredOnly``TypeMismatch` 这类问题状态被直接编码进快照模型,便于编辑器或调试工具解释当前字段状态。
- `StoredOnly``TypeMismatch``DefaultValue / StoredValue / ManagedValue` 这些状态被直接编码进快照模型,便于编辑器或调试工具解释当前字段状态。
- 批量写入/清理结果也被做成显式状态枚举,避免 UI 只能拿到一个粗糙的布尔值。
## 公开函数

View File

@@ -6,11 +6,17 @@
**头文件**: `XCEngine/Scripting/ScriptFieldStorage.h`
**描述**: 以字段名为键保存脚本字段值的本地缓存容器。
**描述**: 以字段名为键保存脚本字段覆盖值的本地缓存容器。
## 概览
`ScriptFieldStorage` 是当前脚本系统的数据缓冲层。它保存的不是“任意运行时变量”,而是需要在原生侧被识别、查询、序列化和可能回写的一组字段。
`ScriptFieldStorage` 是当前脚本系统的数据缓冲层。它保存的不是“任意运行时变量”,而是需要在原生侧被识别、查询、序列化和可能回写的一组字段覆盖值
在当前实现里它承担三种职责:
- 场景序列化时持久化脚本字段覆盖值。
- 运行前为 Inspector/工具提供可编辑的本地字段落点。
- 运行中接收 `MonoScriptRuntime::SyncManagedFieldsToStorage()` 的回写。
## 设计要点
@@ -23,6 +29,7 @@
- 不是线程安全容器。
- 不做类型自动转换。
- 不负责解释字段是否仍被脚本类声明;这部分语义由 `ScriptEngine::TryGetScriptFieldModel()` 结合类元数据来判定。
- 遇到非法反序列化行时会跳过,而不是抛异常中断整个恢复流程。
## 公开方法

View File

@@ -4,39 +4,56 @@
**类型**: `module`
**描述**: 提供脚本组件数据模型、运行时调度器、字段序列化系统以及托管脚本后端抽象。
**描述**: 提供脚本组件数据模型、字段覆盖存储、运行时调度器以及可替换的托管脚本后端抽象。
## 概览
`XCEngine::Scripting` 当前已经形成一条比较清晰的运行链路:
当前脚本模块已经形成一条比较明确的“数据层 -> 调度层 -> 后端层”链路:
1. [ScriptComponent](ScriptComponent/ScriptComponent.md) 挂在 `GameObject` 上,保存脚本类绑定和序列化字段
2. [ScriptEngine](ScriptEngine/ScriptEngine.md) 在场景运行时追踪这些组件,管理实例创建、生命周期调用和字段同步
3. [IScriptRuntime](IScriptRuntime/IScriptRuntime.md) 定义脚本后端必须实现的统一桥接接口
4. [NullScriptRuntime](NullScriptRuntime/NullScriptRuntime.md) 提供无托管环境时的空实现兜底
5. [Mono](Mono/Mono.md) 子目录的 [MonoScriptRuntime](Mono/MonoScriptRuntime/MonoScriptRuntime.md) 是当前真正可运行的托管后端
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`、发现脚本类、创建托管实例并完成字段桥接
如果从 Unity 的经验去理解,这一层大致对应
如果从引擎分层角度理解
- `ScriptComponent` 类似挂在对象上的脚本实例槽位
- `ScriptEngine` 类似引擎内部的脚本生命周期调度
- `MonoScriptRuntime` 则像 C++ 引擎层和托管 `MonoBehaviour` 世界之间的桥梁
但当前实现仍然比成熟商业引擎轻很多,文档必须明确区分“已经成立的真实行为”和“未来可能扩展的方向”。
- `ScriptComponent + ScriptFieldStorage` 是可序列化的数据层
- `ScriptEngine` 是场景运行时的调度
- `IScriptRuntime`/`MonoScriptRuntime` 是托管执行后端
## 设计要点
- 把“脚本绑定数据”和“具体托管后端”拆开,可以让场景序列化不依赖 Mono
- `ScriptFieldStorage` 作为本地缓存,使脚本字段在无运行时、运行时和场景序列化三种状态间都能有统一落点
- `IScriptRuntime` 让引擎主流程只依赖抽象接口,方便以后接入别的脚本后端
- `ScriptEngine` 集中处理生命周期顺序,这比让各组件自己直接碰运行时更容易保证一致性
- 场景序列化只依赖 `ScriptComponent``ScriptFieldStorage`,不依赖 Mono 是否已经初始化
- 字段模型分成“类声明默认值、存储覆盖值、活体托管值”三层,便于 Inspector 和调试工具说明字段来源
- `ScriptEngine` 把生命周期顺序、运行中生成对象追踪、类切换重建都集中在一处
- `IScriptRuntime` 让主流程只依赖契约,后续可以接别的托管后端,而不是把 Mono API 直接扩散到引擎各处
## 当前运行链路
1. `SceneRuntime` 启动场景时调用 `ScriptEngine::OnRuntimeStart(scene)`
2. `ScriptEngine` 先通知当前运行时启动,再递归收集场景里现有的 `ScriptComponent`,同时订阅 `Scene::OnGameObjectCreated()` 以追踪运行中创建的新对象。
3. 对满足运行条件的组件,运行时创建托管实例;`MonoScriptRuntime` 会先把 `ScriptFieldStorage` 中同名且类型匹配的字段写入实例。
4. `ScriptEngine` 统一按 `Awake -> OnEnable -> Start -> FixedUpdate/Update/LateUpdate` 驱动生命周期。
5. 每次生命周期调用后,运行时执行 `SyncManagedFieldsToStorage()`;当前 Mono 实现只会回写“本地已存在且类型仍匹配”的字段。
6. 编辑器或调试工具可通过 `ScriptEngine::TryGetScriptFieldModel()` / `TryGetScriptFieldSnapshots()` 看到字段默认值、覆盖值、活体值以及 `StoredOnly` / `TypeMismatch` 等诊断状态。
## 项目程序集来源
`XCENGINE_ENABLE_MONO_SCRIPTING` 打开时,`managed/CMakeLists.txt` 会构建两类程序集:
- 引擎脚本核心程序集:`XCEngine.ScriptCore.dll`
- 游戏脚本程序集:`GameScripts.dll`
除了测试用的 `build/managed` 输出CMake 还会把项目 `project/Assets/**/*.cs` 编译到 `project/Library/ScriptAssemblies/GameScripts.dll``MonoScriptRuntime::Settings` 可以直接指向该目录,因此项目资产脚本和默认字段值已经进入当前文档的真实行为范围。
## 当前实现边界
- 当前公开支持的脚本字段类型是有限集合:标量、字符串、`Vector2/3/4``GameObject` 引用。
- 生命周期覆盖 `Awake / OnEnable / Start / FixedUpdate / Update / LateUpdate / OnDisable / OnDestroy`
- `NullScriptRuntime` 只是桥接占位,不会真正执行脚本代码。
- `MonoScriptRuntime` 当前围绕单个活动场景工作,还没有做域热重载、程序集增量刷新或完整编辑器脚本生态。
- `NullScriptRuntime` 只是桥接占位,不会真正执行脚本代码,也不会返回脚本类列表或字段默认值
- `MonoScriptRuntime` 目前只发现应用程序集中的非抽象 `MonoBehaviour` 子类,不做热重载、增量刷新或完整编辑器脚本生态。
- 字段同步目前不会自动把“运行中新增但本地没有声明”的字段持久化下来。
## 头文件
@@ -51,6 +68,7 @@
## 相关指南
- [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`,以及默认值、存储覆盖和活体字段如何相互覆盖。
## 相关文档

View File

@@ -0,0 +1,123 @@
# Project Script Assembly And Field Sync
## 这份指南解决什么问题
脚本模块这轮更新之后已经不只是“Mono 能不能跑起来”的问题,还包括两条更具体的链路:
1. `project/Assets/**/*.cs` 怎么进入当前运行时能发现的 `GameScripts.dll`
2. 字段默认值、场景存储覆盖值、活体托管值到底是怎么互相覆盖和同步的
这份指南就是把这两条链路放在一起说明。
## 项目脚本程序集是怎么生成的
`managed/CMakeLists.txt` 当前会构建两组托管输出:
- 通用脚本核心程序集:`XCEngine.ScriptCore.dll`
- 游戏脚本程序集:`GameScripts.dll`
同时它还会扫描 `project/Assets/**/*.cs`,并把这些项目资产脚本编译到:
- `project/Library/ScriptAssemblies/XCEngine.ScriptCore.dll`
- `project/Library/ScriptAssemblies/GameScripts.dll`
- `project/Library/ScriptAssemblies/mscorlib.dll`
如果项目目录下暂时没有任何 `.cs` 文件CMake 会生成一个占位源文件,保证 `GameScripts.dll` 仍然存在。
## 运行时如何接入这份项目程序集
`MonoScriptRuntime::Settings` 可以只指定:
- `assemblyDirectory`
- `corlibDirectory`
- 或更显式的 `coreAssemblyPath` / `appAssemblyPath`
`ResolveSettings()` 会根据这些字段补全剩余路径。当前 `tests/scripting/test_project_script_assembly.cpp` 的做法就是把 `assemblyDirectory` 指到 `project/Library/ScriptAssemblies`,再验证运行时能发现项目资产脚本。
已验证的真实行为包括:
- 运行时能发现 `ProjectScripts.ProjectScriptProbe`
- 能返回它的字段元数据
- 能返回它的默认字段值:
- `EnabledOnBoot = true`
- `Label = "ProjectScriptProbe"`
- `Speed = 2.5f`
项目样例脚本当前位于 [ProjectScriptProbe.cs](../../../../project/Assets/Scripts/ProjectScriptProbe.cs)。
## 字段值的三层来源
当前脚本字段至少可能来自三层:
1. 类默认值
来自 `MonoScriptRuntime::TryGetClassFieldDefaultValues()`,反映 C# 初始化后的真实默认状态。
2. 存储覆盖值
来自 `ScriptComponent::GetFieldStorage()`,会进入场景序列化。
3. 活体托管值
来自当前运行中的托管实例。
`ScriptEngine::TryGetScriptFieldModel()` 会把这三层合并成 `ScriptFieldModel`
- `defaultValue` 表示类默认值
- `storedValue` 表示场景/本地缓存中的覆盖值
- `value` + `valueSource` 表示当前真正应该展示给 UI 的值
## 创建实例时字段怎么进入托管世界
`MonoScriptRuntime::CreateScriptInstance()` 当前会按这条顺序工作:
1. 查类元数据
2. 创建托管对象
3. 注入 `gameObjectUUID``scriptComponentUUID`
4. 遍历 `ScriptFieldStorage`
5. 只把“字段名存在且类型匹配”的覆盖值写进托管实例
这意味着:
- 场景里保存的覆盖值可以覆盖类默认值
- 存储里已经遗留、但脚本类里不存在的字段,不会被写入托管实例
- 类型不匹配的字段也不会被偷偷应用
## 生命周期后为什么还要回写
`ScriptEngine` 每次调用生命周期方法后,都会紧接着调用运行时的 `SyncManagedFieldsToStorage()`
Mono 当前只会回写:
- 本地已经存在于 `ScriptFieldStorage` 的字段
- 且字段在类元数据里仍然存在
- 且存储类型与类声明类型匹配
这样设计的好处是:
- 不会把运行时临时字段自动污染到场景数据
- 类型漂移会显式表现成 `TypeMismatch`
- 存储层和托管层的职责边界更清楚
## 批量编辑和清除覆盖怎么工作
当前推荐给编辑器用的不是直接操作 `ScriptFieldStorage`,而是:
- [ScriptEngine::ApplyScriptFieldWrites](../../XCEngine/Scripting/ScriptEngine/ApplyScriptFieldWrites.md)
- [ScriptEngine::ClearScriptFieldOverrides](../../XCEngine/Scripting/ScriptEngine/ClearScriptFieldOverrides.md)
原因很直接:
- 批量写会同时校验类元数据、活体实例和本地存储
- 清除覆盖会把活体托管字段恢复到类默认值,而不是只删一份本地缓存
如果脚本类已经丢失,系统仍允许对现有存储字段做有限编辑或删除,但会用 `StoredOnly` / `Missing` 这类状态明确告诉你当前已经脱离类声明。
## 当前限制
- 只支持受支持类型的 `public``static` 实例字段
- 只发现应用程序集里的非抽象 `MonoBehaviour` 子类
- 没有热重载或程序集增量刷新
- 不会自动把“运行时新增但本地不存在”的字段持久化
## 推荐阅读
1. [Scripting](../../XCEngine/Scripting/Scripting.md)
2. [MonoScriptRuntime](../../XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md)
3. [ScriptEngine](../../XCEngine/Scripting/ScriptEngine/ScriptEngine.md)
4. [ScriptField](../../XCEngine/Scripting/ScriptField/ScriptField.md)

View File

@@ -10,7 +10,7 @@
- `IScriptRuntime`
- `MonoScriptRuntime`
这份指南的目标,是把它们放回同一条真实运行链路里解释清楚。
这份指南的目标,是把它们放回同一条真实运行链路里解释清楚,并明确“类默认值、存储覆盖值、活体托管值”这三层数据是怎么叠加的
## 一句话理解当前架构
@@ -48,12 +48,25 @@
这和 Unity Inspector 里的“脚本字段序列化层”在理念上很接近,但当前实现更轻量,也更直接。
## 运行时里字段值到底有几层
当前字段值至少可能来自三层:
1. 类默认值
来自运行时 `TryGetClassFieldDefaultValues()`Mono 实现会真实构造一个托管对象并读取字段初始化结果。
2. 存储覆盖值
来自 `ScriptFieldStorage`;它会在场景序列化、运行前编辑和生命周期回写之间持续存在。
3. 活体托管值
来自当前运行中的托管实例;`ScriptEngine::TryGetScriptFieldModel()``TryGetScriptFieldValue()` 会优先读这一层。
`ScriptFieldSnapshot` 里的 `valueSource` 就是在说明当前展示值到底来自哪一层。
## 当前生命周期是怎么串起来的
真实顺序如下:
1. `SceneRuntime::Start()` 调用 `ScriptEngine::OnRuntimeStart(scene)`
2. `ScriptEngine` 收集场景里的 `ScriptComponent`
2. `ScriptEngine` 收集场景里的 `ScriptComponent`,并订阅 `Scene::OnGameObjectCreated()`,保证运行中生成的新对象也会被继续追踪
3. 对满足运行条件的组件,调用运行时 `CreateScriptInstance()`
4. 然后按顺序补发 `Awake``OnEnable`
5. 第一次 `OnUpdate()` 前,会先补发一次 `Start`
@@ -85,7 +98,7 @@
- 初始化 Mono 域并加载核心/游戏程序集。
- 发现继承 `MonoBehaviour` 的非抽象脚本类。
- 枚举支持类型的公共实例字段。
- 枚举支持类型的公共实例字段,并读取字段默认值
- 创建托管实例,并注入 `GameObject` / `ScriptComponent` 上下文。
- 通过 internal call 桥接基础引擎 API。
- 生命周期调用后把已存储字段同步回本地缓存。
@@ -112,6 +125,16 @@
这是一种更偏工程安全的选择。
## 类切换为什么要走 `SetScriptClass()`
当前运行时重绑定逻辑并不是“任意改三个字符串都能触发”。
- `ScriptComponent::SetScriptClass()` / `ClearScriptClass()` 会通知 `ScriptEngine`
- 运行时中的 `ScriptEngine::OnScriptComponentClassChanged()` 会停掉旧实例,再按新类重新创建和跟踪。
- `SetAssemblyName()` / `SetNamespaceName()` / `SetClassName()` 只是原始字段 setter不会触发这条流程。
也就是说,真正要做脚本类切换时,必须走显式重绑定 API。
## 推荐阅读顺序
如果你第一次接触这个模块,建议按下面顺序看:
@@ -123,6 +146,7 @@
5. [ScriptEngine](../../XCEngine/Scripting/ScriptEngine/ScriptEngine.md)
6. [IScriptRuntime](../../XCEngine/Scripting/IScriptRuntime/IScriptRuntime.md)
7. [MonoScriptRuntime](../../XCEngine/Scripting/Mono/MonoScriptRuntime/MonoScriptRuntime.md)
8. [Project Script Assembly And Field Sync](Project-Script-Assembly-And-Field-Sync.md)
## 相关 API

View File

@@ -62,8 +62,8 @@
## T05 Scripting 模块内容重构
- 状态: `TODO`
- 认领人: `未认领`
- 状态: `DONE`
- 认领人: `Codex`
- 优先级: `P1`
- 写入范围: `docs/api/XCEngine/Scripting/**``docs/api/_guides/Scripting/**`
- 主要源码依据: `engine/include/XCEngine/Scripting/IScriptRuntime.h``Mono/MonoScriptRuntime.h``NullScriptRuntime.h``ScriptComponent.h``ScriptEngine.h` 及对应 `.cpp``tests/scripting/**`