8.8 KiB
Scripting
命名空间: XCEngine::Scripting
类型: module
描述: 提供脚本组件数据模型、字段覆盖存储、运行时调度器以及可替换的托管脚本后端抽象。
概览
当前脚本模块已经形成一条明确的“场景数据层 -> 运行时调度层 -> 托管后端层”链路:
- ScriptComponent 挂在
GameObject上,保存程序集名、命名空间、类名、组件 UUID 和 ScriptFieldStorage。 - ScriptEngine 在运行时收集场景内所有脚本组件,按
(gameObjectUUID, scriptComponentUUID)跟踪实例状态,并统一驱动生命周期。 - IScriptRuntime 定义“类发现、字段元数据、默认值读取、实例创建、生命周期调用、字段同步”的统一契约。
- NullScriptRuntime 在没有真实托管后端时兜底,让脚本数据层和编辑流程仍然可工作。
- Mono 子目录中的 MonoScriptRuntime 是当前唯一的真实托管后端,负责加载
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()实参快照”。
- 不读取某次 fixed tick 的历史参数,而是直接走
InputInput.GetKey()/GetKeyDown()/GetKeyUp()、GetButton*()、GetAxis*()、anyKey/anyKeyDown都是对 InputManager 当前状态的直接转发。GetKeyUp()当前对应的是释放边沿语义,也就是原生IsKeyReleased(),不是“当前键没有按住”。anyKey/anyKeyDown也会把鼠标按钮状态算进去,并继承输入系统的帧边界语义。
如果要进一步理解 Pressed / Released / anyKeyDown 的帧语义,应同时阅读 Input Flow And Frame Semantics。
当前运行链路
SceneRuntime启动场景时调用ScriptEngine::OnRuntimeStart(scene)。ScriptEngine先通知当前运行时启动,再递归收集场景里现有的ScriptComponent,同时订阅Scene::OnGameObjectCreated()以追踪运行中创建的新对象。- 对满足运行条件的组件,运行时创建托管实例;
MonoScriptRuntime会先注入gameObjectUUID/scriptComponentUUID,再把ScriptFieldStorage中同名且类型匹配的字段写入实例。 ScriptEngine统一按Awake -> OnEnable -> Start -> FixedUpdate / Update / LateUpdate驱动生命周期;其中Start会延后到第一次OnUpdate()前补发,而不是在OnRuntimeStart()中立即执行。- 每次生命周期调用后,运行时都会执行
SyncManagedFieldsToStorage();当前 Mono 实现只会回写“本地已存在且类型仍匹配”的字段。 - 编辑器或调试工具可通过
ScriptEngine::TryGetScriptFieldModel()/TryGetScriptFieldSnapshots()看到字段默认值、覆盖值、活体值以及StoredOnly/TypeMismatch等诊断状态。
脚本类发现与重绑定
当前“脚本类下拉列表”和“运行中改类名”的语义已经明确落在源码里:
MonoScriptRuntime只扫描应用程序集,也就是GameScripts.dll这一侧的非抽象MonoBehaviour子类,不会把XCEngine.ScriptCore.dll里的基础类型暴露成可绑定脚本类。ScriptEngine::TryGetAvailableScriptClasses()在运行时列表之上追加两步规范化:- 可选的按程序集过滤
- 按
assemblyName -> namespaceName -> className排序,并剔除空类名条目
ScriptComponent::SetScriptClass()/ClearScriptClass()是显式重绑定入口。- 运行时里真正执行“停掉旧实例 -> 重新跟踪 -> 视情况立即创建新实例”的,是 ScriptEngine::OnScriptComponentClassChanged。
这套设计和商业引擎里常见的 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.h - NullScriptRuntime -
NullScriptRuntime.h - ScriptComponent -
ScriptComponent.h - ScriptEngine -
ScriptEngine.h - ScriptField -
ScriptField.h - ScriptFieldStorage -
ScriptFieldStorage.h - Mono -
Mono/
相关指南
- Scripting Runtime And Field Model - 把场景、脚本字段缓存、运行时调度和 Mono 后端放回同一条真实链路里解释清楚,并补上设计理念。
- Project Script Assembly And Field Sync - 解释
project/Assets/**/*.cs如何进入GameScripts.dll,哪些类会被发现,以及默认值、存储覆盖和活体字段如何相互覆盖。 - Input Flow And Frame Semantics - 解释托管
InputAPI 当前所继承的原生输入帧语义。