// // 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.12f); // 弹头碰撞半径 m // 半径 0.12 的反隧穿账:物理离散检测(无 CCD),检测带 = // 半径 + 障碍半厚;薄板(如 0.2m 墙)窗口 2*(0.12+0.1)=0.44m // > 每步进位移 0.42m(25 m/s @ 1/60),垂直命中保证检出。 // 半径越小高速弹越易整步跨过薄碰撞体(无接触=无事件) } 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