From b89f43189657d5870a9bf8ff694ed94ad38dba15 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Thu, 8 Oct 2026 21:45:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=84=9A=E6=9C=AC=E5=AE=9E?= =?UTF-8?q?=E4=BD=93=E5=BA=95=E5=BA=A7=E4=B8=8E=E7=A2=B0=E6=92=9E=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=EF=BC=9ASpawn/Destroy=20+=20OnCollisionEnter/Exit=20+?= =?UTF-8?q?=20TestShoot=20=E9=AA=8C=E6=94=B6=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ScriptBehavior::Spawn/Destroy:Scene::CreateEntity / 延迟销毁队列直达(实现下沉 ScriptRuntime.cpp,避 Application 不完整类型) - ScriptRuntime::DispatchCollisions:帧首把 PhysicsWorld::GetEvents 派发到涉事实例(Persisted 跳过、禁用不收;回调内 spawn/destroy 安全) - TestShoot:左键 spawn 动态球刚体子弹,OnCollisionEnter 命中点生成 ImpactFx 特效实体并自毁,超时防泄漏 --- App/scripts/TestShoot.cpp | 215 ++++++++++++++++++ Engine/include/Engine/Scripting/Script.h | 27 +++ .../include/Engine/Scripting/ScriptRuntime.h | 4 + Engine/src/Scripting/ScriptRuntime.cpp | 53 +++++ 4 files changed, 299 insertions(+) create mode 100644 App/scripts/TestShoot.cpp diff --git a/App/scripts/TestShoot.cpp b/App/scripts/TestShoot.cpp new file mode 100644 index 0000000..8222f26 --- /dev/null +++ b/App/scripts/TestShoot.cpp @@ -0,0 +1,215 @@ +// +// TestShoot.cpp:阶段1 验收脚本——脚本侧 spawn/destroy 底座 + 碰撞事件 +// 转发的端到端链路。 +// +// 三个脚本协作(放一个文件方便对照): +// TestShoot(挂任意实体,如相机实体): +// 鼠标左键上升沿 → 从编辑器相机沿视线 spawn 子弹实体 +// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入) +// Bullet(子弹实体,由 TestShoot 运行时装配): +// OnCreate 施加枪口初速(body 已由当帧 Step 对账建好); +// OnCollisionEnter → 命中点 spawn ImpactFx + 自毁; +// 超时自毁防飞出地图泄漏;存活期画调试十字 = 弹道可视化 +// ImpactFx(特效实体,由 Bullet 运行时装配,空实体 + 调试点标): +// 扩张十字 + 白→红渐隐,寿命尽自毁——占位特效 +// +// 覆盖的引擎设施: +// - Spawn()/Destroy():ScriptBehavior 实体底座(Scene::CreateEntity/ +// DestroyEntity 延迟队列) +// - 组件运行时装配:RigidBody(PhysicsWorld 下一帧对账建 body)、 +// ScriptComponent(ScriptRuntime 下一帧对账建实例) +// - OnCollisionEnter/Exit:PhysicsWorld::GetEvents → ScriptRuntime +// 帧首派发 +// +// 验收步骤:编辑器给任意实体挂 TestShoot → Play → RMB 飞行视角瞄准 +// 墙体 → 左键射击:弹道点飞出 → 撞墙瞬间原地绽放十字特效、子弹消失。 +// + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Engine; // Input / Debug 门面 + +namespace { + + // ---- 子弹脚本(TestShoot 运行时挂载;不手动挂场景)---- + class Bullet final : public Engine::Scripting::ScriptBehavior { + public: + const char* Name() const override { return "Bullet"; } + + void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { + // 枪口初速(世界系 m/s):TestShoot spawn 时按视线方向写进 + // 脚本项参数——数据经参数层传递,与序列化同一条路 + pb.Field("Velocity", &m_Velocity, glm::vec3{0.0f}); + pb.Field("Lifetime", &m_Lifetime, 5.0f); // 超时自毁(秒) + } + + void OnCreate() override { + // body 已由本帧 Step 的 SyncScene 建好(spawn 发生在上帧 + // OnUpdate):这里施加初速,物理从下步起按弹道积分 + if (glm::length(m_Velocity) > 1e-4f) { + App().GetPhysics().SetLinearVelocity(EntityBits(), m_Velocity); + } + } + + void OnUpdate(f32 dt) override { + m_Age += dt; + if (m_Age >= m_Lifetime) { + Destroy(static_cast(EntityBits())); + return; + } + // 弹道可视化:命中点即弹着点(橙十字 = 弹头,速度线 = 尾迹) + const glm::vec3 pos = Transform().GetPosition(); + const glm::vec3 vel = + App().GetPhysics().GetLinearVelocity(EntityBits()); + Debug::Cross(pos, 0.09f, Debug::Rgb(0xFF8C1A), + /*width*/ 2.0f, /*duration*/ 0.0f, + /*depthTest*/ false); + if (glm::length(vel) > 0.5f) { + Debug::Arrow(pos, pos + vel * 0.03f, Debug::Rgb(0xFFB366), + /*headSize*/ 0.04f, /*width*/ 2.0f, + /*duration*/ 0.0f, /*depthTest*/ false); + } + } + + void OnCollisionEnter(u32 other, const glm::vec3& point, + const glm::vec3& normal) override { + (void)other; (void)normal; // 法线留给将来定向溅射 + + // 命中特效:空实体锚在命中点,ImpactFx 脚本项由下一帧对账 + // 实例化(Spawn 即时建实体,脚本实例化天然晚一帧) + auto fx = Spawn("impact"); + fx.GetComponent() + .SetPosition(point); + auto& sc = fx.AddComponent(); + Engine::ECS::ScriptComponent::Item item; + item.name = "ImpactFx"; + sc.items.push_back(std::move(item)); + + // 自毁:延迟队列,帧尾统一执行(事件派发循环安全) + Destroy(static_cast(EntityBits())); + } + + private: + glm::vec3 m_Velocity{0.0f}; + f32 m_Lifetime = 5.0f; + f32 m_Age = 0.0f; + }; + + // ---- 命中特效脚本(Bullet 运行时挂载;空实体 + 调试点标占位)---- + class ImpactFx final : public Engine::Scripting::ScriptBehavior { + public: + const char* Name() const override { return "ImpactFx"; } + + void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { + pb.Field("Duration", &m_Duration, 0.4f); // 特效寿命(秒) + } + + void OnUpdate(f32 dt) override { + m_Age += dt; + if (m_Age >= m_Duration) { + Destroy(static_cast(EntityBits())); + return; + } + // 扩张十字 + 白→红渐隐(占位:将来换 VFX/粒子系统) + const f32 t = m_Age / m_Duration; + const glm::vec3 pos = Transform().GetPosition(); + Debug::Cross(pos, 0.12f + t * 0.55f, + glm::vec4{1.0f, 1.0f - t, 1.0f - t, 1.0f}, + /*width*/ 3.0f, /*duration*/ 0.0f, + /*depthTest*/ false); + } + + private: + f32 m_Duration = 0.4f; + f32 m_Age = 0.0f; + }; + + // ---- 枪口脚本(验收主体:挂到任意实体,位置/朝向取编辑器相机)---- + class TestShoot final : public Engine::Scripting::ScriptBehavior { + public: + const char* Name() const override { return "TestShoot"; } + + void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { + pb.Field("BulletSpeed", &m_BulletSpeed, 25.0f); // 枪口初速 m/s + pb.Field("MuzzleOffset", &m_MuzzleOffset, 0.6f); // 出膛距离 m + pb.Field("FireCooldown", &m_FireCooldown, 0.15f); // 最小射击间隔 s + pb.Field("BulletRadius", &m_BulletRadius, 0.08f); // 弹头碰撞半径 m + } + + void OnUpdate(f32 dt) override { + m_Cooldown -= dt; + + // 左键上升沿触发(电平查询 + 上帧状态对比,Unity + // GetMouseButtonDown 边沿语义的手工等价物) + const bool down = + Input::IsMouseButtonDown(Input::MouseButton::Left); + const bool fire = down && !m_FireWasDown && + m_Cooldown <= 0.0f; + m_FireWasDown = down; + if (fire) { + SpawnBullet(); + m_Cooldown = m_FireCooldown; + } + } + + private: + void SpawnBullet() { + // 枪口 = 编辑器相机视线前方(编辑形态 RMB 飞行瞄准; + // 与 WASDMove 的朝向来源一致) + const auto& cam = + App().GetRenderer().GetEditorCamera().GetTransform(); + const glm::vec3 fwd = + cam.GetRotationQuat() * glm::vec3(0.0f, 0.0f, -1.0f); + const glm::vec3 muzzle = + cam.GetWorldPosition() + fwd * m_MuzzleOffset; + + // ---- 子弹实体装配(spawn 底座 + 组件运行时装配演示)---- + auto bullet = Spawn("bullet"); + auto& tr = bullet.GetComponent(); + tr.SetPosition(muzzle); + tr.SetRotationQuat(cam.GetRotationQuat()); + + // Dynamic 球刚体:参数给默认档(轻质量/低弹性,命中即停) + auto& rb = bullet.AddComponent(); + rb.type = Engine::ECS::RigidBodyComponent::BodyType::Dynamic; + rb.collider = + Engine::ECS::RigidBodyComponent::ColliderType::Sphere; + rb.sphereRadius = m_BulletRadius; + rb.mass = 0.2f; + rb.friction = 0.3f; + rb.restitution = 0.1f; + + // Bullet 脚本项:初速/寿命经参数层传入(与序列化同一条路, + // 下一帧 ScriptRuntime 对账实例化) + auto& sc = bullet.AddComponent(); + Engine::ECS::ScriptComponent::Item item; + item.name = "Bullet"; + item.params.push_back( + {"Velocity", + Engine::Scripting::ParamValue{fwd * m_BulletSpeed}}); + item.params.push_back( + {"Lifetime", Engine::Scripting::ParamValue{5.0f}}); + sc.items.push_back(std::move(item)); + } + + f32 m_BulletSpeed = 25.0f; + f32 m_MuzzleOffset = 0.6f; + f32 m_FireCooldown = 0.15f; + f32 m_BulletRadius = 0.08f; + f32 m_Cooldown = 0.0f; + bool m_FireWasDown = false; // 左键上升沿检测基准 + }; + + ENGINE_REGISTER_SCRIPT(Bullet) + ENGINE_REGISTER_SCRIPT(ImpactFx) + ENGINE_REGISTER_SCRIPT(TestShoot) + +} // namespace diff --git a/Engine/include/Engine/Scripting/Script.h b/Engine/include/Engine/Scripting/Script.h index bbad8be..8279cb7 100644 --- a/Engine/include/Engine/Scripting/Script.h +++ b/Engine/include/Engine/Scripting/Script.h @@ -28,6 +28,7 @@ #include #include #include +#include // 须在 Scene.h 之后(内联方法需完整 Scene) #include #include @@ -69,6 +70,22 @@ namespace Engine { // 在此按名取值) virtual void OnParamsApplied() {} + // ---- 碰撞回调(PhysicsWorld::GetEvents 帧首派发,ScriptRuntime + // 转发;事件语义 = 最近一次物理步进)---- + // Enter/Exit 由 Jolt Added/Removed 转发;Persisted 不回调 + //(最小 API,接触持续期无每帧噪音)。对方是触发器(sensor) + // 时同样触发——后续按需拆 OnTriggerEnter/Exit。 + // other = 对方实体位模式(还原:static_cast(other), + // 回调时对方可能已销毁,用前自行 Scene::IsAlive 校验)。 + // 派发在 Sync/OnUpdate 之前:回调里 spawn/destroy 安全 + //(destroy 入延迟队列,spawn 由下一帧对账消化) + virtual void OnCollisionEnter(u32 other, + const glm::vec3& point, + const glm::vec3& normal) { + (void)other; (void)point; (void)normal; + } + virtual void OnCollisionExit(u32 other) { (void)other; } + protected: // 便捷访问:绑定实体的 TransformComponent ECS::TransformComponent& Transform() { @@ -96,6 +113,16 @@ namespace Engine { bool Valid() const { return m_Scene != nullptr && m_Entity != entt::null; } + // ---- 场景实体 spawn/destroy 底座(Unity Instantiate/Destroy + // 同位)---- + // spawn:帧内即时创建(UUID + Transform + Name 出生即有), + // 组件由脚本自行 AddComponent 装配;RigidBody/ScriptComponent + // 分别由 PhysicsWorld/ScriptRuntime 下一帧对账消化 + ECS::EntityHandle Spawn(const std::string& name); + // destroy:延迟——入队,帧尾 FlushDestroyQueue 统一执行 + //(遍历安全);调用方持有的句柄此后悬空 + void Destroy(ECS::Entity e); + // ---- 组件启停(Unity SetActive / Behaviour.enabled 语义)---- // 自身组件总开关(整个脚本组件停 tick,实例保留) void SetActive(bool on) { diff --git a/Engine/include/Engine/Scripting/ScriptRuntime.h b/Engine/include/Engine/Scripting/ScriptRuntime.h index 31b7439..2801fb4 100644 --- a/Engine/include/Engine/Scripting/ScriptRuntime.h +++ b/Engine/include/Engine/Scripting/ScriptRuntime.h @@ -66,6 +66,10 @@ namespace Engine { private: void Sync(); + // 碰撞事件派发(Update 开头、Sync 之前):PhysicsWorld 最近 + // 一次步进的事件转发到涉事实例的 OnCollisionEnter/Exit。 + // Persisted 跳过;实例未建/已禁用不收 + void DispatchCollisions(); ECS::Scene& m_Scene; Application* m_App; // 注入脚本上下文 diff --git a/Engine/src/Scripting/ScriptRuntime.cpp b/Engine/src/Scripting/ScriptRuntime.cpp index 5cd3409..119f46f 100644 --- a/Engine/src/Scripting/ScriptRuntime.cpp +++ b/Engine/src/Scripting/ScriptRuntime.cpp @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include #include @@ -19,6 +22,16 @@ namespace Engine { return *m_App; } + // ---- 场景实体 spawn/destroy 底座(声明在 Script.h,实现下沉 + // 此处——Application 完整类型只在 cpp 可见)---- + ECS::EntityHandle ScriptBehavior::Spawn(const std::string& name) { + return App().GetScene().CreateEntity(name); + } + + void ScriptBehavior::Destroy(ECS::Entity e) { + App().GetScene().DestroyEntity(e); + } + // ---- 参数按名读写(ScriptBehavior 声明,经绑定表;写穿 // Item.params 由运行时 SetParam 完成)---- bool ScriptBehavior::WriteParam(const std::string& name, @@ -41,7 +54,47 @@ namespace Engine { return false; } + // ---- 碰撞事件派发(Update 开头、Sync 之前):PhysicsWorld 最近 + // 一次步进收集的事件转发到涉事实例。时序保证:Step 先于 + // Update(帧循环序)→ 此处读到的是本帧物理结果;回调里 + // spawn/destroy 安全——destroy 入延迟队列(帧尾 flush), + // spawn 不改 m_Instances(下一帧 Sync 对账建实例)。禁用 + // 实例不收(Unity enabled 语义对齐);Persisted 每步都来, + // 直接跳过(Enter/Exit 已够 gameplay 用) + void ScriptRuntime::DispatchCollisions() { + if (!m_App) { + return; + } + const std::vector& events = + m_App->GetPhysics().GetEvents(); + for (const auto& ev : events) { + if (ev.kind == CollisionEvent::Kind::Persisted) { + continue; + } + const bool enter = ev.kind == CollisionEvent::Kind::Added; + for (const u32 self : {ev.a, ev.b}) { + const u32 other = (self == ev.a) ? ev.b : ev.a; + const auto it = m_Instances.find(self); + if (it == m_Instances.end()) { + continue; + } + for (auto& inst : it->second) { + if (!inst.behavior || !inst.enabled) { + continue; + } + if (enter) { + inst.behavior->OnCollisionEnter( + other, ev.point, ev.normal); + } else { + inst.behavior->OnCollisionExit(other); + } + } + } + } + } + void ScriptRuntime::Update(f32 dt) { + DispatchCollisions(); Sync(); auto& reg = m_Scene.GetRegistry();