DeclareParams/ParamBuilder 声明字段,运行时反射读写; Inspector 按类型生成控件,SceneSerializer 直通存取; 动画系统接入参数化裁剪;新增 OrbitMove 示例脚本
251 lines
11 KiB
C++
251 lines
11 KiB
C++
//
|
||
// Script.h:C++ 原生脚本设施(行为基类 + 按名注册表)。
|
||
//
|
||
// 设计参照 UE:脚本 = 挂在实体上的行为对象 + 统一基类生命周期
|
||
//(OnCreate/OnUpdate/OnDestroy,BeginPlay/Tick 语义)。引擎帧循环只认
|
||
// 基类接口——将来接 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 模式),
|
||
// 不进组件——组件可拷贝/可比较。
|
||
//
|
||
// 脚本作者注意:
|
||
// OnCreate/OnUpdate 阶段实体与组件已就位(假定所需组件存在);
|
||
// OnDestroy 只做自身清理——可能因实体销毁触发,句柄已失效。
|
||
//
|
||
|
||
#ifndef HELLOVULKAN_SCRIPT_H
|
||
#define HELLOVULKAN_SCRIPT_H
|
||
|
||
#include <Engine/Core/Types.h>
|
||
#include <Engine/ECS/Components.h>
|
||
#include <Engine/ECS/Scene.h>
|
||
#include <Engine/Scripting/ScriptParam.h>
|
||
|
||
#include <string>
|
||
#include <unordered_map>
|
||
#include <vector>
|
||
#include <algorithm>
|
||
|
||
namespace Engine {
|
||
|
||
class Application; // 宿主门面(Window/Renderer/Physics 入口),实现见 ScriptRuntime.cpp
|
||
|
||
namespace Scripting {
|
||
|
||
class ScriptBehavior {
|
||
public:
|
||
virtual ~ScriptBehavior() = default;
|
||
|
||
// 注册表键(= 组件 scriptName),须与 ENGINE_REGISTER_SCRIPT 名一致
|
||
virtual const char* Name() const = 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() {
|
||
return m_Scene->GetRegistry().get<ECS::TransformComponent>(m_Entity);
|
||
}
|
||
|
||
template<typename T>
|
||
T& GetComponent() {
|
||
return m_Scene->GetRegistry().get<T>(m_Entity);
|
||
}
|
||
|
||
template<typename T>
|
||
T* TryGetComponent() {
|
||
return m_Scene->GetRegistry().try_get<T>(m_Entity);
|
||
}
|
||
|
||
// 绑定实体的 entityBits(PhysicsWorld 按此键寻址:直调
|
||
// App().GetPhysics() 的各 API 时作第一个参数)
|
||
u32 EntityBits() const {
|
||
return static_cast<u32>(entt::to_integral(m_Entity));
|
||
}
|
||
|
||
// 宿主应用(GetWindow/GetRenderer/GetPhysics/GetScene……)
|
||
Application& App() const;
|
||
|
||
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<ECS::ScriptComponent>(m_Entity)) {
|
||
sc->active = on;
|
||
}
|
||
}
|
||
bool IsActive() const {
|
||
const auto* sc =
|
||
m_Scene->GetRegistry().try_get<ECS::ScriptComponent>(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<ECS::ScriptComponent>(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<typename T>
|
||
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<typename T>
|
||
bool GetParam(const std::string& name, T& out) const {
|
||
ParamValue v;
|
||
if (!ReadParam(name, v)) {
|
||
return false;
|
||
}
|
||
if (auto* p = std::get_if<T>(&v.v)) {
|
||
out = *p;
|
||
return true;
|
||
}
|
||
return false; // 类型不符
|
||
}
|
||
|
||
private:
|
||
friend class ScriptRuntime;
|
||
void Bind(ECS::Scene& scene, entt::entity e, Application* app) {
|
||
m_Scene = &scene;
|
||
m_Entity = e;
|
||
m_App = app;
|
||
}
|
||
// 参数绑定表交接(实例化/RefreshParams 时由运行时填充)
|
||
void SetParamBinds(std::vector<ParamBuilder::Bind> 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<ParamBuilder::Bind> m_ParamBinds;
|
||
};
|
||
|
||
// ---- 按名注册表:脚本类文件级自注册,运行时按名实例化 ----
|
||
class ScriptRegistry {
|
||
public:
|
||
using Factory = Scope<ScriptBehavior> (*)();
|
||
|
||
static void Register(const std::string& name, Factory f) {
|
||
Map()[name] = f;
|
||
Names().push_back(name); // Inspector 下拉用(保持注册序)
|
||
}
|
||
|
||
static Scope<ScriptBehavior> Create(const std::string& name) {
|
||
auto& m = Map();
|
||
const auto it = m.find(name);
|
||
return it != m.end() ? it->second() : nullptr;
|
||
}
|
||
|
||
static bool Contains(const std::string& name) {
|
||
return Map().count(name) != 0;
|
||
}
|
||
|
||
static std::vector<std::string>& Names() {
|
||
static std::vector<std::string> v;
|
||
return v;
|
||
}
|
||
|
||
// 参数 schema(惰性缓存):临时实例调 DeclareParams 取声明
|
||
// ——每脚本类一次性(注册先于 main,加载期必就绪)。未注册
|
||
// 脚本 = 空 schema(读端未知键全忽略,弱前向兼容)
|
||
static const std::vector<ParamBuilder::Desc>&
|
||
Schema(const std::string& name) {
|
||
static const std::vector<ParamBuilder::Desc> kEmpty;
|
||
static std::unordered_map<std::string,
|
||
std::vector<ParamBuilder::Desc>>
|
||
cache;
|
||
const auto it = cache.find(name);
|
||
if (it != cache.end()) {
|
||
return it->second;
|
||
}
|
||
std::vector<ParamBuilder::Desc> 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<std::string, Factory>& Map() {
|
||
static std::unordered_map<std::string, Factory> m;
|
||
return m;
|
||
}
|
||
};
|
||
|
||
} // namespace Scripting
|
||
} // namespace Engine
|
||
|
||
// 文件级自注册:脚本 cpp 里写一行即可,Inspector 下拉与序列化按
|
||
// 类名(字符串)解析。T 须默认可构造并继承 ScriptBehavior
|
||
#define ENGINE_REGISTER_SCRIPT(T) \
|
||
static const bool s_##T##_registered = [] { \
|
||
::Engine::Scripting::ScriptRegistry::Register( \
|
||
#T, []() -> ::Engine::Scope< \
|
||
::Engine::Scripting::ScriptBehavior> { \
|
||
return ::Engine::CreateScope<T>(); \
|
||
}); \
|
||
return true; \
|
||
}();
|
||
|
||
#endif //HELLOVULKAN_SCRIPT_H
|