diff --git a/App/editor/panels/InspectorPanel.cpp b/App/editor/panels/InspectorPanel.cpp index 87b2782..eb83239 100644 --- a/App/editor/panels/InspectorPanel.cpp +++ b/App/editor/panels/InspectorPanel.cpp @@ -17,6 +17,7 @@ #include "Engine/Renderer/MaterialAsset.h" // 材质覆盖槽(.material 资产) #include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面) #include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names) +#include "Engine/Scripting/ScriptRuntime.h" // 参数编辑 → RefreshParams #include "Engine/Core/Log.h" // Create Override 失败告警 #include @@ -38,6 +39,28 @@ namespace // 材质槽循环索引用(u32 定义于 namespace Engine,App 层显式引入) using Engine::u32; + // 脚本参数对齐 schema:缺失 → 声明默认补齐;类型不符 → 重置默认 + //(换脚本类/手编档防呆)。返回对齐后的参数项指针(每帧幂等) + Engine::Scripting::ScriptParam* EnsureParam( + Engine::ECS::ScriptComponent::Item& item, + const Engine::Scripting::ParamBuilder::Desc& d) + { + auto it = std::find_if( + item.params.begin(), item.params.end(), + [&](const Engine::Scripting::ScriptParam& p) + { return p.name == d.name; }); + if (it == item.params.end()) + { + item.params.push_back({d.name, d.def}); + return &item.params.back(); + } + if (it->value.Type() != d.type) + { + it->value = d.def; + } + return &*it; + } + // 递归收集 assets/ 下模型容器文件(模型槽与动画 attach 槽共用; // 无法从扩展名区分"模型 fbx"与"纯动画 fbx",一并列出)。 // 会话级缓存:目录条目几十量级,避免每帧递归扫描 @@ -1156,6 +1179,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) ImGui::SeparatorText("Movement"); ImGui::DragFloat("Max Slope (deg)", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f); ImGui::DragFloat("Step Height", &cc->stepHeight, 0.02f, 0.0f, 2.0f); + ImGui::DragFloat("Gravity Scale", &cc->gravityScale, 0.1f, 0.0f, 10.0f); // ---- Debug:遥测(只读,运行时状态在 PhysicsWorld)---- if (auto& phy = ctx.app->GetPhysics(); phy.IsEnabled()) @@ -1249,6 +1273,102 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) { removeIdx = i; } + // ---- 参数区(按脚本 schema 自动生成;缺省补齐 + 类型 + // 防呆;编辑 → RefreshParams 重灌实例成员,PIE 当帧生效)---- + if (!item.name.empty() && + !Engine::Scripting::ScriptRegistry::Schema(item.name) + .empty()) + { + const auto& schema = + Engine::Scripting::ScriptRegistry::Schema(item.name); + ImGui::Indent(); + bool anyEdited = false; + for (const auto& d : schema) + { + auto* p = EnsureParam(item, d); + bool changed = false; + // 局部变量禁用 f32(glm::f32 歧义前科),用 float + switch (d.type) + { + case Engine::Scripting::ParamType::F32: + { + float v = std::get(p->value.v); + if (ImGui::DragFloat(d.name.c_str(), &v, 0.1f)) + { + p->value = Engine::Scripting::ParamValue{v}; + changed = true; + } + break; + } + case Engine::Scripting::ParamType::I32: + { + int v = std::get(p->value.v); + if (ImGui::DragInt(d.name.c_str(), &v)) + { + p->value = Engine::Scripting::ParamValue{ + static_cast(v)}; + changed = true; + } + break; + } + case Engine::Scripting::ParamType::Bool: + changed = ImGui::Checkbox( + d.name.c_str(), &std::get(p->value.v)); + break; + case Engine::Scripting::ParamType::String: + { + char buf[256] = {}; + std::snprintf(buf, sizeof(buf), "%s", + std::get(p->value.v) + .c_str()); + if (ImGui::InputText(d.name.c_str(), buf, + sizeof(buf))) + { + p->value = Engine::Scripting::ParamValue{ + std::string(buf)}; + changed = true; + } + break; + } + case Engine::Scripting::ParamType::Vec2: + { + const glm::vec2 g = + std::get(p->value.v); + float v[2] = {g.x, g.y}; + if (ImGui::DragFloat2(d.name.c_str(), v, 0.1f)) + { + p->value = Engine::Scripting::ParamValue{ + glm::vec2(v[0], v[1])}; + changed = true; + } + break; + } + case Engine::Scripting::ParamType::Vec3: + { + const glm::vec3 g = + std::get(p->value.v); + float v[3] = {g.x, g.y, g.z}; + if (ImGui::DragFloat3(d.name.c_str(), v, 0.1f)) + { + p->value = Engine::Scripting::ParamValue{ + glm::vec3(v[0], v[1], v[2])}; + changed = true; + } + break; + } + } + if (changed) + { + anyEdited = true; + } + } + if (anyEdited) + { + ctx.app->GetScripts().RefreshParams( + static_cast(entt::to_integral(selected))); + } + ImGui::Unindent(); + } ImGui::PopID(); // 已填名但注册表查无此类:场景加载早于脚本注册/改名都会走到这 if (!item.name.empty() && cur == 0) diff --git a/App/scripts/OrbitMove.cpp b/App/scripts/OrbitMove.cpp new file mode 100644 index 0000000..10efe07 --- /dev/null +++ b/App/scripts/OrbitMove.cpp @@ -0,0 +1,46 @@ +// +// OrbitMove.cpp:参数管线验证样本——实体绕中心点圆周运动。 +// +// 参数全链路:DeclareParams 声明(Speed/Radius/Center)→ Inspector +// 自动出控件 → 随场景序列化 → 实例化/RefreshParams 灌值。PIE 中拖 +// Radius 当帧生效;Stop 由快照天然回滚。 +// 未声明成员(m_Angle)= 纯运行时状态,不进 Inspector 不落盘。 +// + +#include + +#include + +#include + +namespace { + + class OrbitMove final : public Engine::Scripting::ScriptBehavior { + public: + const char* Name() const override { return "OrbitMove"; } + + // 参数声明(Inspector 控件 + 序列化 + 灌值共用 schema) + void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { + pb.Field("Speed", &m_Speed, 1.0f); // 角速度(弧度/秒) + pb.Field("Radius", &m_Radius, 3.0f); // 轨道半径 + pb.Field("Center", &m_Center, glm::vec3{0.0f}); // 轨道中心 + } + + void OnUpdate(float dt) override { + m_Angle += m_Speed * dt; + // 直接驱动 Transform(相对父空间的圆周;无父 = 世界) + Transform().SetPosition( + m_Center + glm::vec3(std::cos(m_Angle) * m_Radius, 0.0f, + std::sin(m_Angle) * m_Radius)); + } + + private: + float m_Speed = 1.0f; + float m_Radius = 3.0f; + glm::vec3 m_Center{0.0f}; + float m_Angle = 0.0f; // 运行时相位(非参数) + }; + + ENGINE_REGISTER_SCRIPT(OrbitMove); + +} // namespace diff --git a/App/scripts/Spinner.cpp b/App/scripts/Spinner.cpp index 7ba2710..2d2de5c 100644 --- a/App/scripts/Spinner.cpp +++ b/App/scripts/Spinner.cpp @@ -1,10 +1,12 @@ // // Spinner.cpp:C++ 原生脚本示范——Y 轴匀速自转。 // -// 写脚本三步: +// 写脚本四步: // 1. 新建 .cpp 放本目录(CMake 自动进构建) // 2. 继承 ScriptBehavior,覆写 Name/OnCreate/OnUpdate/OnDestroy -// 3. 文件级 ENGINE_REGISTER_SCRIPT(类名) 自注册——Inspector 下拉 +// 3. DeclareParams 声明可调参数(Inspector 自动出控件 + 随场景 +// 序列化 + 实例化灌值) +// 4. 文件级 ENGINE_REGISTER_SCRIPT(类名) 自注册——Inspector 下拉 // 与场景序列化按该名解析 // @@ -18,6 +20,10 @@ namespace { public: const char* Name() const override { return "Spinner"; } + void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { + pb.Field("Speed", &m_Speed, 90.0f); // 度/秒 + } + void OnCreate() override { // 实体与组件已就位;需要别的组件时用 Get()(不存在会炸) } @@ -31,7 +37,7 @@ namespace { } private: - float m_Speed = 90.0f; // 度/秒 + float m_Speed = 90.0f; // 度/秒(声明默认;实例化期被存档值覆盖) }; ENGINE_REGISTER_SCRIPT(Spinner); diff --git a/Engine/include/Engine/ECS/Components.h b/Engine/include/Engine/ECS/Components.h index c6ba98d..6620498 100644 --- a/Engine/include/Engine/ECS/Components.h +++ b/Engine/include/Engine/ECS/Components.h @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -233,6 +234,10 @@ namespace Engine::ECS { f32 fadeElapsed = 0.0f; // 已过渡时长 f32 fadeDuration = 0.3f; // 过渡总时长(秒) + // 按名播放的运行时反指(瞬态,不落盘不序列化):AnimationSystem + // 每帧回填(同 RigidBody::bodyValid 模式)。空 = 模型未就绪 + const Model* runtimeModel = nullptr; + // ---- 资产引用(序列化落盘)---- // 外部动画资产(编辑器 attach 的动画容器文件,随场景持久化)。 // clips 本体运行时并入 Model 资产(共享);重启后由 Inspector @@ -253,6 +258,16 @@ namespace Engine::ECS { std::unordered_map boolParams; std::vector pendingTriggers; // 消费即清(瞬态) + // 阻尼参数推进状态(瞬态,不落盘——重启从 floatParams 当前值 + // 重新起步):current 逐帧指数逼近 target,写回 floatParams 供 + // 状态机条件统一读取 + struct DampedParam { + float current = 0.0f; + float target = 0.0f; + float response = 8.0f; // 逼近速率(1/s,越大越快) + }; + std::unordered_map dampedParams; + // 状态机瞬态(不落盘,重启从 entry state 开始) std::string currentState; // 空 = 未初始化(System 首帧置 entry) f32 stateTime = 0.0f; // 当前状态播放头(秒) @@ -266,6 +281,22 @@ namespace Engine::ECS { { floatParams[name] = value; } + // 阻尼变体(Unity SetFloat(name, target, dampTime) 同构):登记 + // 目标值,AnimationSystem Controller 驱动期逐帧指数逼近(帧率 + // 无关)。首次登记从 floatParams 当前值起步(无则取目标), + // 避免跳变。与 SetFloat 同名混用 = 直接 set 会被下一帧阻尼值 + // 覆盖,别混 + void SetFloatDamped(const std::string& name, float target, float response) + { + auto [it, inserted] = dampedParams.try_emplace(name); + it->second.target = target; + it->second.response = glm::max(response, 0.01f); + if (inserted) { + const auto fp = floatParams.find(name); + it->second.current = + fp != floatParams.end() ? fp->second : target; + } + } void SetBool(const std::string& name, bool value) { boolParams[name] = value; @@ -309,6 +340,13 @@ namespace Engine::ECS { time = 0.0f; playing = true; } + + // 按名播放(Unity Animator.Play("Walk") 同形,推荐):clip 名经 + // runtimeModel 的 clip 表解析,消掉下标硬耦合(attach 顺序变了 + // 脚本不改)。未命中/模型未就绪返回 false。实现住 + // AnimationSystem.cpp——需要 Model 完整类型,组件头保持零资产 + // 依赖(Model.h 拖 vulkan/assimp,进本头会污染全部包含者) + bool Play(const std::string& clipName, f32 fadeSeconds = 0.3f); }; // 相机。一个场景中 primary=true 的实体是当前渲染视角。 @@ -430,6 +468,9 @@ namespace Engine::ECS { f32 mass = 70.0f; f32 maxSlopeAngle = 45.0f; // 度:可站立的最大坡角 f32 stepHeight = 0.4f; // 台阶自动攀爬高(WalkStairs) + // 重力倍率(UE CharacterMovement.GravityScale 同位):真实 9.81 + // 对人形角色偏"棉花糖"(滞空 1.2s+),游戏手感普遍 2~3 倍 + f32 gravityScale = 2.0f; bool operator==(const CharacterControllerComponent&) const = default; // 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯 @@ -444,6 +485,11 @@ namespace Engine::ECS { struct Item { std::string name; bool active = true; // 禁用 = 实例保留但停 OnUpdate + // 参数(权威数据,随场景序列化)。脚本成员 = 运行时真值, + // 实例化期由 ScriptRuntime 按 schema 灌入:键缺失 → 声明 + // 默认,多余键忽略;运行时 SetParam 写穿回这里(PIE 实时 + // 反映,Stop 由快照天然回滚) + std::vector params; bool operator==(const Item&) const = default; }; diff --git a/Engine/include/Engine/Scripting/Script.h b/Engine/include/Engine/Scripting/Script.h index 6324ddb..bbad8be 100644 --- a/Engine/include/Engine/Scripting/Script.h +++ b/Engine/include/Engine/Scripting/Script.h @@ -6,6 +6,13 @@ // 基类接口——将来接 Lua/C# 时各实现一种 foreign behavior(内部持 // VM 状态),引擎零改动。 // +// 瘦基类(对齐 Unity6/UE5 现役形态):基类只供生命周期 + 参数 + +// 实体访问器,零领域 API——物理住 PhysicsWorld(App().GetPhysics() +// 直调),动画住 AnimationComponent(GetComponent 直调,Animator +// 同形)。DeclareParams = C# 反射缺失下 UPROPERTY 的等价物:Unity +// 里你从不写声明是因为语言替你做了,C++ 里每个引擎都得补这一环 +//(UE 让你标 UPROPERTY,这里让你写一行 Declare)。 +// // 组件只存 scriptName 字符串(POD,序列化天然支持:Lua/C# 同样按名 // 解析);实例由 ScriptRuntime 对账式管理(照 PhysicsWorld 模式), // 不进组件——组件可拷贝/可比较。 @@ -21,10 +28,12 @@ #include #include #include +#include #include #include #include +#include namespace Engine { @@ -39,11 +48,27 @@ namespace Engine { // 注册表键(= 组件 scriptName),须与 ENGINE_REGISTER_SCRIPT 名一致 virtual const char* Name() const = 0; - // 生命周期(吃 scaled dt:编辑器暂停 timeScale=0 时自然停摆) + // 生命周期(吃 scaled dt:编辑器暂停 timeScale=0 时自然停摆)。 + // 顺序对齐 Unity:OnCreate(= Awake,实例化即调)→ OnEnable + // → OnStart(首次 OnUpdate 前一次)→ OnUpdate* → OnDisable → + // OnDestroy(可能因实体销毁触发,句柄已失效只做自身清理)。 + // 跨实体 OnEnable 顺序不保证(实例表无序),与 Unity 一致 virtual void OnCreate() {} + virtual void OnEnable() {} + virtual void OnStart() {} virtual void OnUpdate(f32 dt) {} + virtual void OnDisable() {} virtual void OnDestroy() {} + // 参数声明(实例化灌值与注册表 schema 缓存共用入口;外语绑定 + // 同键接入)。契约:只写成员指针与常量,禁触场景/应用—— + // schema 缓存路径用未 Bind 的临时实例调用 + virtual void DeclareParams(ParamBuilder& pb) { (void)pb; } + + // 参数灌值后回调(外语侧收值接缝:C++ 成员已直写,VM 绑定 + // 在此按名取值) + virtual void OnParamsApplied() {} + protected: // 便捷访问:绑定实体的 TransformComponent ECS::TransformComponent& Transform() { @@ -60,7 +85,8 @@ namespace Engine { return m_Scene->GetRegistry().try_get(m_Entity); } - // 绑定实体的 entityBits(物理门面按键值寻址) + // 绑定实体的 entityBits(PhysicsWorld 按此键寻址:直调 + // App().GetPhysics() 的各 API 时作第一个参数) u32 EntityBits() const { return static_cast(entt::to_integral(m_Entity)); } @@ -70,6 +96,66 @@ namespace Engine { bool Valid() const { return m_Scene != nullptr && m_Entity != entt::null; } + // ---- 组件启停(Unity SetActive / Behaviour.enabled 语义)---- + // 自身组件总开关(整个脚本组件停 tick,实例保留) + void SetActive(bool on) { + if (auto* sc = m_Scene->GetRegistry().try_get(m_Entity)) { + sc->active = on; + } + } + bool IsActive() const { + const auto* sc = + m_Scene->GetRegistry().try_get(m_Entity); + return sc != nullptr && sc->active; + } + + // ---- 自身参数(写成员 + 写穿 Item.params:PIE Inspector + // 实时反映,Stop 由快照天然回滚;首次写新名一次分配)---- + // 返回成员直写是否命中(未声明参数 false,存档仍记录——前向 + // 兼容)。跨脚本/跨实体访问走 App().GetScripts().SetParam/GetParam + bool SetParam(const std::string& name, const ParamValue& v) { + const bool hit = WriteParam(name, v); + if (auto* sc = Valid() + ? m_Scene->GetRegistry().try_get(m_Entity) + : nullptr) { + const auto item = std::find_if( + sc->items.begin(), sc->items.end(), + [&](const ECS::ScriptComponent::Item& it) { + return it.name == Name(); + }); + if (item != sc->items.end()) { + const auto pit = std::find_if( + item->params.begin(), item->params.end(), + [&](const ScriptParam& p) { return p.name == name; }); + if (pit != item->params.end()) { + pit->value = v; + } else { + item->params.push_back({name, v}); + } + } + } + return hit; + } + template + bool SetParam(const std::string& name, const T& v) { + return SetParam(name, ParamValue{v}); + } + bool GetParam(const std::string& name, ParamValue& out) const { + return ReadParam(name, out); + } + template + bool GetParam(const std::string& name, T& out) const { + ParamValue v; + if (!ReadParam(name, v)) { + return false; + } + if (auto* p = std::get_if(&v.v)) { + out = *p; + return true; + } + return false; // 类型不符 + } + private: friend class ScriptRuntime; void Bind(ECS::Scene& scene, entt::entity e, Application* app) { @@ -77,10 +163,19 @@ namespace Engine { m_Entity = e; m_App = app; } + // 参数绑定表交接(实例化/RefreshParams 时由运行时填充) + void SetParamBinds(std::vector binds) { + m_ParamBinds = std::move(binds); + } + // 按名写/读成员(经绑定表;未声明或类型不符返回 false)。 + // 写穿 Item.params 由运行时侧完成(需查组件项) + bool WriteParam(const std::string& name, const ParamValue& v); + bool ReadParam(const std::string& name, ParamValue& out) const; ECS::Scene* m_Scene = nullptr; entt::entity m_Entity = entt::null; Application* m_App = nullptr; + std::vector m_ParamBinds; }; // ---- 按名注册表:脚本类文件级自注册,运行时按名实例化 ---- @@ -108,6 +203,28 @@ namespace Engine { return v; } + // 参数 schema(惰性缓存):临时实例调 DeclareParams 取声明 + // ——每脚本类一次性(注册先于 main,加载期必就绪)。未注册 + // 脚本 = 空 schema(读端未知键全忽略,弱前向兼容) + static const std::vector& + Schema(const std::string& name) { + static const std::vector kEmpty; + static std::unordered_map> + cache; + const auto it = cache.find(name); + if (it != cache.end()) { + return it->second; + } + std::vector descs; + if (auto behavior = Create(name)) { + ParamBuilder pb; + behavior->DeclareParams(pb); + descs = pb.Descs(); + } + return cache.emplace(name, std::move(descs)).first->second; + } + private: static std::unordered_map& Map() { static std::unordered_map m; diff --git a/Engine/include/Engine/Scripting/ScriptParam.h b/Engine/include/Engine/Scripting/ScriptParam.h new file mode 100644 index 0000000..d134afc --- /dev/null +++ b/Engine/include/Engine/Scripting/ScriptParam.h @@ -0,0 +1,215 @@ +// +// ScriptParam.h:脚本参数——按名变体值 + 声明式 schema 构建器。 +// +// 设计本质(UE/Unity 共识):参数数据按名存在序列化层(组件 +// ScriptComponent::Item.params,随场景持久化),C++ 脚本成员只是运行时 +// 真值缓存——这条边界正是将来 Lua/C# 外语绑定的接缝: +// - C++ 脚本:DeclareParams 里 Field(name, &成员, 默认值) 双写声明 +// (灌值直写成员,零回收代码); +// - 外语脚本:Declare(name, 类型, 默认值) 无指针声明,值经 +// OnParamsApplied 回收(VM 侧按名取)。 +// +// schema 由注册表按脚本类名惰性缓存(ScriptRegistry::Schema)——描述 +// 里不含指针,缓存无悬垂;成员指针(Bind)只随实例存亡,不进缓存。 +// + +#ifndef HELLOVULKAN_SCRIPT_PARAM_H +#define HELLOVULKAN_SCRIPT_PARAM_H + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace Engine { + namespace Scripting { + + // 参数类型(序列化分支 + Inspector 控件选择)。枚举序 = variant + // 臂序(ParamValue::Type() 直转);新类型 = 枚举尾插 + 追加臂, + // 旧档位前向兼容 + enum class ParamType : u8 { F32, I32, Bool, String, Vec2, Vec3 }; + + // 类型 → 枚举映射(Field 重载与消费端共用) + template struct ParamTypeOf; + template<> struct ParamTypeOf { static constexpr ParamType value = ParamType::F32; }; + template<> struct ParamTypeOf { static constexpr ParamType value = ParamType::I32; }; + template<> struct ParamTypeOf { static constexpr ParamType value = ParamType::Bool; }; + template<> struct ParamTypeOf { static constexpr ParamType value = ParamType::String; }; + template<> struct ParamTypeOf { static constexpr ParamType value = ParamType::Vec2; }; + template<> struct ParamTypeOf { static constexpr ParamType value = ParamType::Vec3; }; + + // 单参数的存储形态(类型标签由 variant 臂序自持) + struct ParamValue { + std::variant + v{f32{0.0f}}; + + ParamType Type() const { return static_cast(v.index()); } + + template + static constexpr ParamType TypeOf() { return ParamTypeOf::value; } + + bool operator==(const ParamValue&) const = default; + }; + + // 单参数(组件 Item.params 的元素,随场景序列化) + struct ScriptParam { + std::string name; + ParamValue value; + + bool operator==(const ScriptParam&) const = default; + }; + + // 参数声明构建器:DeclareParams(pb) 里逐项 Field/Declare,产出 + // 描述列表(schema,持久)+ 绑定表(成员指针,瞬态)。 + // Field 即把声明默认写进成员——未灌值前成员就是默认值 + class ParamBuilder { + public: + // ---- C++ 侧:声明 + 绑定成员(灌值直写)---- + void Field(std::string_view name, f32* dst, f32 def) { + *dst = def; + AddDesc(name, ParamType::F32, ParamValue{def}); + m_Binds.push_back({std::string(name), ParamType::F32, dst}); + } + void Field(std::string_view name, i32* dst, i32 def) { + *dst = def; + AddDesc(name, ParamType::I32, ParamValue{def}); + m_Binds.push_back({std::string(name), ParamType::I32, dst}); + } + void Field(std::string_view name, bool* dst, bool def) { + *dst = def; + AddDesc(name, ParamType::Bool, ParamValue{def}); + m_Binds.push_back({std::string(name), ParamType::Bool, dst}); + } + void Field(std::string_view name, std::string* dst, + const std::string& def) { + *dst = def; + AddDesc(name, ParamType::String, ParamValue{def}); + m_Binds.push_back({std::string(name), ParamType::String, dst}); + } + void Field(std::string_view name, glm::vec2* dst, + const glm::vec2& def) { + *dst = def; + AddDesc(name, ParamType::Vec2, ParamValue{def}); + m_Binds.push_back({std::string(name), ParamType::Vec2, dst}); + } + void Field(std::string_view name, glm::vec3* dst, + const glm::vec3& def) { + *dst = def; + AddDesc(name, ParamType::Vec3, ParamValue{def}); + m_Binds.push_back({std::string(name), ParamType::Vec3, dst}); + } + + // ---- 外语侧:仅声明不绑定(值经 OnParamsApplied 回收)---- + void Declare(std::string_view name, ParamType t, ParamValue def) { + AddDesc(name, t, std::move(def)); + } + + // 声明描述(持久——进注册表 schema 缓存,无指针无悬垂) + struct Desc { + std::string name; + ParamType type; + ParamValue def; + }; + // 成员绑定(瞬态——指针随声明实例存亡,只进 ScriptBehavior) + struct Bind { + std::string name; + ParamType type; + void* ptr; + }; + + const std::vector& Descs() const { return m_Descs; } + std::vector TakeBinds() { return std::move(m_Binds); } + + // 单绑定写值:类型不符返回 false(SetParam/跨脚本写共用) + static bool WriteBind(const Bind& b, const ParamValue& v) { + if (b.type != v.Type()) { + return false; + } + switch (b.type) { + case ParamType::F32: + *static_cast(b.ptr) = std::get(v.v); + return true; + case ParamType::I32: + *static_cast(b.ptr) = std::get(v.v); + return true; + case ParamType::Bool: + *static_cast(b.ptr) = std::get(v.v); + return true; + case ParamType::String: + *static_cast(b.ptr) = + std::get(v.v); + return true; + case ParamType::Vec2: + *static_cast(b.ptr) = std::get(v.v); + return true; + case ParamType::Vec3: + *static_cast(b.ptr) = std::get(v.v); + return true; + } + return false; + } + + // 单绑定读值(按绑定类型出值;GetParam/跨脚本读共用) + static bool ReadBind(const Bind& b, ParamValue& out) { + switch (b.type) { + case ParamType::F32: + out = ParamValue{*static_cast(b.ptr)}; + return true; + case ParamType::I32: + out = ParamValue{*static_cast(b.ptr)}; + return true; + case ParamType::Bool: + out = ParamValue{*static_cast(b.ptr)}; + return true; + case ParamType::String: + out = ParamValue{*static_cast(b.ptr)}; + return true; + case ParamType::Vec2: + out = ParamValue{*static_cast(b.ptr)}; + return true; + case ParamType::Vec3: + out = ParamValue{*static_cast(b.ptr)}; + return true; + } + return false; + } + + // 按名灌值进绑定成员:命中→直写;未绑定(外语声明)→跳过; + // 类型不符/多余键→忽略(读端已校正,这里兜底)。 + // 缺失键不写——Field 已把声明默认写进成员 + void Apply(const std::vector& params) const { + for (const auto& p : params) { + const auto b = std::find_if( + m_Binds.begin(), m_Binds.end(), + [&](const Bind& it) { return it.name == p.name; }); + if (b != m_Binds.end()) { + WriteBind(*b, p.value); + } + } + } + + private: + void AddDesc(std::string_view name, ParamType t, ParamValue def) { + assert(!name.empty() && "参数名不能为空"); + assert(std::none_of(m_Descs.begin(), m_Descs.end(), + [&](const Desc& d) { + return d.name == name; + }) && + "参数重名"); + m_Descs.push_back({std::string(name), t, std::move(def)}); + } + + std::vector m_Descs; + std::vector m_Binds; + }; + + } // namespace Scripting +} // namespace Engine + +#endif //HELLOVULKAN_SCRIPT_PARAM_H diff --git a/Engine/include/Engine/Scripting/ScriptRuntime.h b/Engine/include/Engine/Scripting/ScriptRuntime.h index cb1ea70..31b7439 100644 --- a/Engine/include/Engine/Scripting/ScriptRuntime.h +++ b/Engine/include/Engine/Scripting/ScriptRuntime.h @@ -1,7 +1,11 @@ // // ScriptRuntime.h:脚本生命周期驱动。对账式管理(组件项加/删/改名 ↔ -// 实例建/毁),照 PhysicsWorld.SyncScene 模式——每帧对账,编辑器操作 -// 即时生效,场景加载/实体销毁自动消化。 +// 实例建/毁),照 PhysicsWorld.SyncScene 模式——Running 中每帧对账, +// PIE 中组件增删/改名即时生效,场景加载/实体销毁自动消化。 +// +// Play Mode 语义(Unity/UE PIE 对齐):编辑态不实例化任何脚本—— +// 实例生命周期 = 一次 PIE 局,Play 后首次 Update 对账重建,Stop 时 +// EndPlay 统一终局,成员状态不跨局泄漏。 // // 多脚本:一个实体可挂任意多个脚本项(ScriptComponent::items),实例 // 按名字与 items 对账——重命名/删除走 OnDestroy 清理,未动的实例不重建。 @@ -23,10 +27,13 @@ namespace Engine { namespace Scripting { - // 实例簿记:行为对象 + OnCreate 是否已调 + // 实例簿记:行为对象 + 生命周期位(created = OnCreate 已调; + // enabled = 当前启停态,翻转检测基准;started = OnStart 已发) struct ScriptInstance { Scope behavior; bool created = false; + bool enabled = false; + bool started = false; }; class ScriptRuntime { @@ -34,9 +41,29 @@ namespace Engine { explicit ScriptRuntime(ECS::Scene& scene, Application* app = nullptr) : m_Scene(scene), m_App(app) {} - // 每帧调用(吃 scaled dt):对账 → OnCreate(新实例)→ OnUpdate + // 每帧调用(吃 scaled dt):对账 → OnCreate(新实例)→ + // 启停翻转(OnEnable/OnDisable)→ OnStart(首次)→ OnUpdate。 + // 仅 Running 态被调(Application 门控) void Update(f32 dt); + // 终局(GameFlow →Stopped 广播):全部实例 OnDisable → + // OnDestroy 并清空簿记。Unity 退出 Play 语义——成员状态 + // (动量/边沿/当前 clip)不跨局泄漏,下次 Play 对账重建 + void EndPlay(); + + // 参数热刷新(编辑期一次性):实体全部实例重新 DeclareParams + // + 按组件存档灌值(Inspector 编辑参数后调) + void RefreshParams(u32 entityBits); + + // 跨脚本按名参数访问(本实体兄弟项或跨实体):SetParam 写 + // 目标实例成员 + 写穿 Item.params(PIE Inspector 实时反映); + // GetParam 读目标实例成员。实体无组件/项不存在/实例未建 + // 返回 false + bool SetParam(u32 entityBits, const std::string& scriptName, + const std::string& param, const ParamValue& value); + bool GetParam(u32 entityBits, const std::string& scriptName, + const std::string& param, ParamValue& out) const; + private: void Sync(); diff --git a/Engine/src/Animation/AnimationEvaluator.cpp b/Engine/src/Animation/AnimationEvaluator.cpp index 20b926d..f9d29d9 100644 --- a/Engine/src/Animation/AnimationEvaluator.cpp +++ b/Engine/src/Animation/AnimationEvaluator.cpp @@ -131,10 +131,11 @@ namespace Engine::Animation { std::vector& outGlobals) { // crossfade 双层 = N 层混合的特例(Direct 模式与 Controller - // 过渡共用同一套加权数学)。src 层权重 = 1 - wCur 与历史 - // mix(src, cur, wCur) 语义等价 + // 过渡共用同一套加权数学)。两权重和恒为 1,渐进归一化后精确 + // 等价 mix(src, cur, wCur):起点全 src、终点全 cur,全程平滑 + //(cur 恒 1.0 的话起点就瞬间跳 50%,fade 有效时长减半——割裂感) PoseLayer layers[2]; - layers[0] = {&cur, curTime, 1.0f}; + layers[0] = {&cur, curTime, xf.wCur}; u32 count = 1; if (xf.src != nullptr) { layers[1] = {xf.src, xf.srcTime, 1.0f - xf.wCur}; diff --git a/Engine/src/Animation/AnimationSystem.cpp b/Engine/src/Animation/AnimationSystem.cpp index 64eb841..67fb545 100644 --- a/Engine/src/Animation/AnimationSystem.cpp +++ b/Engine/src/Animation/AnimationSystem.cpp @@ -20,8 +20,40 @@ #include #include -namespace Engine::Animation { +// ---- AnimationComponent::Play(按名) 实现(声明在 Components.h)---- +// 住本 TU:需要 Model 完整类型做 clip 表查找,组件头保持零资产依赖 +//(Model.h 拖 vulkan/assimp)。定义放全局全限定(类所属命名空间的 +// 外层才允许成员函数定义) +bool Engine::ECS::AnimationComponent::Play(const std::string& clipName, + f32 fadeSeconds) { + if (!runtimeModel) { + return false; // 模型未就绪(首帧前/无渲染组件/换模型间隙) + } + const auto& clips = runtimeModel->GetClips(); + for (u32 i = 0; i < clips.size(); ++i) { + // 同 clip = 确保在播即可(播放头不动,防下方相位覆写跳帧) + if (clips[i].name == clipName) { + if (static_cast(i) == clipIndex) { + playing = true; + return true; + } + // 相位对齐(Unity 状态机过渡的默认语义:按归一化时间同 + // 步)——不消毒的话 walk 播到左脚在前、run 却从 0(右脚 + // 在前)起步,混合的两脚错相,fade 再长也割裂。按下标 + // Play 不同步(编辑器预览要从头看) + const AnimationClip& src = clips[clipIndex]; + const f32 norm = src.duration > 0.0f + ? glm::mod(time, src.duration) / src.duration + : 0.0f; + Play(static_cast(i), fadeSeconds); // 内部 time 归 0 + time = clips[i].duration * norm; // 覆写为同相位起步 + return true; + } + } + return false; // 名字未命中(是否告警由调用侧决定) +} +namespace Engine::Animation { void AnimationSystem::Update(ECS::Scene& scene, Assets::AssetManager& assets, f32 dt, FrameSceneData& frame) { @@ -76,10 +108,11 @@ namespace Engine::Animation { ECS::AnimationComponent& anim) { if (anim.mode != ECS::AnimationComponent::AnimMode::Direct) return; // Controller 实体走段 2 - if (!anim.active) return; // 组件禁用 = 暂停采样(播放头不动) if (!mr.model.Valid()) return; // 未指派/手清 = 无效句柄(Get 对无效 FATAL) const Model* model = assets.GetModel(mr.model); if (!model || !model->HasAnimations()) return; + anim.runtimeModel = model; // 按名播放反指(暂停态也刷新) + if (!anim.active) return; // 组件禁用 = 暂停采样(播放头不动) const auto& clips = model->GetClips(); // clip 下标合法性(模型资产可能已换) @@ -144,10 +177,11 @@ namespace Engine::Animation { ECS::AnimationComponent::AnimMode::Controller) { return; } - if (!anim.active) return; if (!mr.model.Valid()) return; const Model* model = assets.GetModel(mr.model); if (!model || !model->HasAnimations()) return; + anim.runtimeModel = model; // 按名播放反指(暂停态也刷新) + if (!anim.active) return; const auto* ctrl = assets.FindController(anim.controllerPath); if (!ctrl) return; // 未加载/路径无效 → rest 兜底 const auto& states = ctrl->GetStates(); @@ -173,6 +207,18 @@ namespace Engine::Animation { } } + // 阻尼参数推进(帧率无关指数平滑):current 逐帧逼近 + // target,写回 floatParams 供条件评估统一读取;收敛贴齐 + // 消除渐近尾噪(AnimSetFloatDamped 的引擎侧推进端) + for (auto& [dname, d] : anim.dampedParams) { + const float k = 1.0f - std::exp(-d.response * dt); + d.current += (d.target - d.current) * k; + if (std::abs(d.target - d.current) < 1e-4f) { + d.current = d.target; + } + anim.floatParams[dname] = d.current; + } + // clip 名 → 下标表 auto& lookup = clipLookup[model]; if (lookup.empty()) { diff --git a/Engine/src/Scripting/ScriptRuntime.cpp b/Engine/src/Scripting/ScriptRuntime.cpp index ae44136..5cd3409 100644 --- a/Engine/src/Scripting/ScriptRuntime.cpp +++ b/Engine/src/Scripting/ScriptRuntime.cpp @@ -4,6 +4,7 @@ #include +#include #include #include @@ -18,6 +19,28 @@ namespace Engine { return *m_App; } + // ---- 参数按名读写(ScriptBehavior 声明,经绑定表;写穿 + // Item.params 由运行时 SetParam 完成)---- + bool ScriptBehavior::WriteParam(const std::string& name, + const ParamValue& v) { + for (const auto& b : m_ParamBinds) { + if (b.name == name) { + return ParamBuilder::WriteBind(b, v); + } + } + return false; + } + + bool ScriptBehavior::ReadParam(const std::string& name, + ParamValue& out) const { + for (const auto& b : m_ParamBinds) { + if (b.name == name) { + return ParamBuilder::ReadBind(b, out); + } + } + return false; + } + void ScriptRuntime::Update(f32 dt) { Sync(); @@ -26,40 +49,73 @@ namespace Engine { const auto e = static_cast(bits); const ECS::ScriptComponent* sc = reg.valid(e) ? reg.try_get(e) : nullptr; - if (!sc) { - continue; // 延迟销毁队列里,下帧 Sync 清理 - } - if (!sc->active) { - continue; // 组件总开关:全部实例停 tick(Unity disable) - } for (auto& inst : list) { if (!inst.behavior) { continue; } - // per-script 启停:按名查项(对账保证存在;新项要 - // 下帧 Sync 才有实例,这里查不到就跳过) - const auto item = std::find_if( - sc->items.begin(), sc->items.end(), - [&](const ECS::ScriptComponent::Item& it) { - return it.name == inst.behavior->Name(); - }); - if (item == sc->items.end() || !item->active) { + // 启停翻转检测:wantOn = 组件总开关 && 项开关(组件 + // 关闭时零查找;项按名查找沿用每帧既有的那一次)。 + // 与上帧 inst.enabled 比较出沿:true→OnEnable, + // false→OnDisable(Unity enabled 翻转语义) + bool wantOn = false; + if (sc && sc->active) { + const auto item = std::find_if( + sc->items.begin(), sc->items.end(), + [&](const ECS::ScriptComponent::Item& it) { + return it.name == inst.behavior->Name(); + }); + wantOn = item != sc->items.end() && item->active; + } + if (!wantOn) { + if (inst.enabled) { + inst.enabled = false; + inst.behavior->OnDisable(); + } continue; // 禁用 = 实例保留停 OnUpdate } if (!inst.created) { inst.behavior->OnCreate(); inst.created = true; } + if (!inst.enabled) { + inst.enabled = true; + inst.behavior->OnEnable(); + } + if (!inst.started) { + inst.started = true; + inst.behavior->OnStart(); // 首次 OnUpdate 前一次 + } inst.behavior->OnUpdate(dt); } } } + // ---- 终局(GameFlow →Stopped 广播):全实例 OnDisable → + // OnDestroy。与 Sync 的孤儿清理同序,范围 = 全部且不重建; + // Application 的监听先于编辑器回滚注册(组合根构造序), + // OnDestroy 里读到的仍是上局实体 ---- + void ScriptRuntime::EndPlay() { + for (auto& [bits, list] : m_Instances) { + for (auto& inst : list) { + if (!inst.behavior) { + continue; + } + if (inst.enabled) { + inst.enabled = false; + inst.behavior->OnDisable(); + } + inst.behavior->OnDestroy(); + } + } + m_Instances.clear(); + } + void ScriptRuntime::Sync() { auto& reg = m_Scene.GetRegistry(); // 1. 孤儿/改名清理:实体或组件没了 → 整组 OnDestroy; - // 实例名不在 items 里(改名/移除)→ 单个 OnDestroy + // 实例名不在 items 里(改名/移除)→ 单个 OnDestroy。 + // 销毁次序 = OnDisable(守卫防双发)→ OnDestroy for (auto it = m_Instances.begin(); it != m_Instances.end();) { const auto e = static_cast(it->first); const ECS::ScriptComponent* sc = @@ -67,9 +123,14 @@ namespace Engine { : nullptr; if (!sc) { for (auto& inst : it->second) { - if (inst.behavior) { - inst.behavior->OnDestroy(); + if (!inst.behavior) { + continue; } + if (inst.enabled) { + inst.enabled = false; + inst.behavior->OnDisable(); + } + inst.behavior->OnDestroy(); } it = m_Instances.erase(it); continue; @@ -86,6 +147,10 @@ namespace Engine { continue; } if (li->behavior) { + if (li->enabled) { + li->enabled = false; + li->behavior->OnDisable(); + } li->behavior->OnDestroy(); } li = list.erase(li); @@ -98,7 +163,9 @@ namespace Engine { } // 2. 缺失实例化:逐项按名查注册表(未知名警告——场景含未注册 - // 脚本时弱前向兼容,跳过不影响其余脚本) + // 脚本时弱前向兼容,跳过不影响其余脚本)。灌值次序对齐 + // Unity:声明(成员=声明默认)→ 组件存档覆盖 → 收值回调 + // → OnCreate(= Awake,拿到最终参数值) const auto view = reg.view(); for (auto [e, sc] : view.each()) { const u32 bits = static_cast(entt::to_integral(e)); @@ -124,6 +191,12 @@ namespace Engine { } behavior->Bind(m_Scene, e, m_App); + ParamBuilder pb; + behavior->DeclareParams(pb); + pb.Apply(item.params); + behavior->SetParamBinds(pb.TakeBinds()); + behavior->OnParamsApplied(); + ScriptInstance inst; inst.behavior = std::move(behavior); inst.behavior->OnCreate(); // Unity Awake 语义:实例化即回调 @@ -133,5 +206,98 @@ namespace Engine { } } + // ---- 参数热刷新(编辑期一次性):重新声明 + 按存档灌值 ---- + void ScriptRuntime::RefreshParams(u32 entityBits) { + const auto it = m_Instances.find(entityBits); + if (it == m_Instances.end()) { + return; + } + auto& reg = m_Scene.GetRegistry(); + const auto e = static_cast(entityBits); + const ECS::ScriptComponent* sc = reg.valid(e) + ? reg.try_get(e) : nullptr; + if (!sc) { + return; + } + for (auto& inst : it->second) { + if (!inst.behavior) { + continue; + } + ParamBuilder pb; + inst.behavior->DeclareParams(pb); + const auto item = std::find_if( + sc->items.begin(), sc->items.end(), + [&](const ECS::ScriptComponent::Item& it2) { + return it2.name == inst.behavior->Name(); + }); + if (item != sc->items.end()) { + pb.Apply(item->params); + } + inst.behavior->SetParamBinds(pb.TakeBinds()); + inst.behavior->OnParamsApplied(); + } + } + + // ---- 跨脚本按名参数访问 ---- + bool ScriptRuntime::SetParam(u32 entityBits, + const std::string& scriptName, + const std::string& param, + const ParamValue& value) { + auto& reg = m_Scene.GetRegistry(); + const auto e = static_cast(entityBits); + ECS::ScriptComponent* sc = reg.valid(e) + ? reg.try_get(e) : nullptr; + if (!sc) { + return false; + } + const auto item = std::find_if( + sc->items.begin(), sc->items.end(), + [&](const ECS::ScriptComponent::Item& it) { + return it.name == scriptName; + }); + if (item == sc->items.end()) { + return false; + } + + // 目标实例成员直写(实例未建/未声明则跳过,存档仍写穿) + const auto ii = m_Instances.find(entityBits); + if (ii != m_Instances.end()) { + for (auto& inst : ii->second) { + if (inst.behavior && + inst.behavior->Name() == scriptName) { + inst.behavior->WriteParam(param, value); + break; + } + } + } + + // 写穿 Item.params:首次写新名一次分配,此后零分配 + const auto pit = std::find_if( + item->params.begin(), item->params.end(), + [&](const ScriptParam& p) { return p.name == param; }); + if (pit != item->params.end()) { + pit->value = value; + } else { + item->params.push_back({param, value}); + } + return true; + } + + bool ScriptRuntime::GetParam(u32 entityBits, + const std::string& scriptName, + const std::string& param, + ParamValue& out) const { + const auto ii = m_Instances.find(entityBits); + if (ii == m_Instances.end()) { + return false; + } + for (const auto& inst : ii->second) { + if (inst.behavior && inst.behavior->Name() == scriptName) { + return inst.behavior->ReadParam(param, out); + } + } + return false; + } + } // namespace Scripting } // namespace Engine diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp index c3f32c7..9076c1d 100644 --- a/Engine/src/Serialization/SceneSerializer.cpp +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include @@ -50,6 +51,11 @@ namespace Engine::Serialization { << YAML::EndSeq; } + void EmitVec2(YAML::Emitter& e, const glm::vec2& v) + { + e << YAML::Flow << YAML::BeginSeq << v.x << v.y << YAML::EndSeq; + } + void EmitVec4(YAML::Emitter& e, const glm::vec4& v) { e << YAML::Flow << YAML::BeginSeq << v.x << v.y << v.z << v.w @@ -71,6 +77,71 @@ namespace Engine::Serialization { n[2].as(def.z)}; } + glm::vec2 ReadVec2(const YAML::Node& n, const glm::vec2& def) + { + if (!n || !n.IsSequence() || n.size() < 2) return def; + return {n[0].as(def.x), n[1].as(def.y)}; + } + + // ---- 脚本参数读端:按声明 schema 收值(调用方保证键已知)---- + // 数值臂互转(YAML 标量无类型:"3" 作 f32/i32 均自然,i32 声明 + // 遇浮点值截断);形不符回声明默认 + 警告(加载期一次,不刷屏) + template + bool TryAs(const YAML::Node& n, T& out) + { + try { + out = n.as(); + return true; + } catch (const std::exception&) { + return false; // 类型不符 / 僵尸节点硬化异常统一兜住 + } + } + + Scripting::ParamValue ReadParamValue( + const YAML::Node& n, const Scripting::ParamBuilder::Desc& d) + { + switch (d.type) { + case Scripting::ParamType::F32: { + f32 v{}; + if (TryAs(n, v)) return {v}; + i32 iv{}; + if (TryAs(n, iv)) return {static_cast(iv)}; + break; + } + case Scripting::ParamType::I32: { + i32 v{}; + if (TryAs(n, v)) return {v}; + f32 fv{}; + if (TryAs(n, fv)) return {static_cast(fv)}; + break; + } + case Scripting::ParamType::Bool: { + bool v{}; + if (TryAs(n, v)) return {v}; + break; + } + case Scripting::ParamType::String: { + if (n.IsScalar()) return {n.Scalar()}; + break; + } + case Scripting::ParamType::Vec2: { + if (n && n.IsSequence() && n.size() >= 2) { + return {ReadVec2(n, glm::vec2{0.0f})}; + } + break; + } + case Scripting::ParamType::Vec3: { + if (n && n.IsSequence() && n.size() >= 3) { + return {ReadVec3(n, glm::vec3{0.0f})}; + } + break; + } + } + ENGINE_WARN("Script param '{}' type mismatch, use default", + d.name); + return d.def; + } + glm::vec4 ReadVec4(const YAML::Node& n, const glm::vec4& def) { if (!n || !n.IsSequence() || n.size() < 4) return def; @@ -452,6 +523,8 @@ namespace Engine::Serialization { << cc->maxSlopeAngle; out << YAML::Key << "StepHeight" << YAML::Value << cc->stepHeight; + out << YAML::Key << "GravityScale" << YAML::Value + << cc->gravityScale; out << YAML::Key << "Active" << YAML::Value << cc->active; out << YAML::EndMap; } @@ -467,6 +540,39 @@ namespace Engine::Serialization { out << YAML::BeginMap; out << YAML::Key << "Name" << YAML::Value << item.name; out << YAML::Key << "Active" << YAML::Value << item.active; + if (!item.params.empty()) { + out << YAML::Key << "Params" << YAML::Value + << YAML::BeginMap; + for (const auto& p : item.params) { + switch (p.value.Type()) { + case Engine::Scripting::ParamType::F32: + out << YAML::Key << p.name << YAML::Value + << std::get(p.value.v); + break; + case Engine::Scripting::ParamType::I32: + out << YAML::Key << p.name << YAML::Value + << std::get(p.value.v); + break; + case Engine::Scripting::ParamType::Bool: + out << YAML::Key << p.name << YAML::Value + << std::get(p.value.v); + break; + case Engine::Scripting::ParamType::String: + out << YAML::Key << p.name << YAML::Value + << std::get(p.value.v); + break; + case Engine::Scripting::ParamType::Vec2: + out << YAML::Key << p.name << YAML::Value; + EmitVec2(out, std::get(p.value.v)); + break; + case Engine::Scripting::ParamType::Vec3: + out << YAML::Key << p.name << YAML::Value; + EmitVec3(out, std::get(p.value.v)); + break; + } + } + out << YAML::EndMap; + } out << YAML::EndMap; } out << YAML::EndSeq; @@ -855,6 +961,7 @@ namespace Engine::Serialization { cc.mass = n["Mass"].as(cc.mass); cc.maxSlopeAngle = n["MaxSlopeAngle"].as(cc.maxSlopeAngle); cc.stepHeight = n["StepHeight"].as(cc.stepHeight); + cc.gravityScale = n["GravityScale"].as(cc.gravityScale); cc.active = n["Active"].as(true); // 状态(速度/着地)由 PhysicsWorld 对账重建,不存 } @@ -868,6 +975,27 @@ namespace Engine::Serialization { ECS::ScriptComponent::Item item; item.name = it["Name"].as(""); item.active = it["Active"].as(true); + // 参数:按脚本 schema 对齐收值——未知键忽略(前向 + // 兼容),缺失键不存(运行时用声明默认) + const YAML::Node ps = it["Params"]; + if (ps) { + const auto& schema = + Scripting::ScriptRegistry::Schema(item.name); + for (const auto& kv : ps) { + const std::string pname = + kv.first.as(""); + const auto dit = std::find_if( + schema.begin(), schema.end(), + [&](const Scripting::ParamBuilder::Desc& d) { + return d.name == pname; + }); + if (dit == schema.end()) { + continue; + } + item.params.push_back( + {pname, ReadParamValue(kv.second, *dit)}); + } + } // 去重兜底:同名项只收第一份(UI 已杜绝重复, // 手编场景仍可能写出重复) const bool dup = std::any_of(