新增脚本实体底座与碰撞事件:Spawn/Destroy + OnCollisionEnter/Exit + TestShoot 验收脚本

- ScriptBehavior::Spawn/Destroy:Scene::CreateEntity / 延迟销毁队列直达(实现下沉 ScriptRuntime.cpp,避 Application 不完整类型)
- ScriptRuntime::DispatchCollisions:帧首把 PhysicsWorld::GetEvents 派发到涉事实例(Persisted 跳过、禁用不收;回调内 spawn/destroy 安全)
- TestShoot:左键 spawn 动态球刚体子弹,OnCollisionEnter 命中点生成 ImpactFx 特效实体并自毁,超时防泄漏
This commit is contained in:
2026-10-08 21:45:10 +08:00
parent 26e222a9cf
commit b89f431896
4 changed files with 299 additions and 0 deletions
+215
View File
@@ -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 <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Input.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
#include <glm/glm.hpp>
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<Engine::ECS::Entity>(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<Engine::ECS::TransformComponent>()
.SetPosition(point);
auto& sc = fx.AddComponent<Engine::ECS::ScriptComponent>();
Engine::ECS::ScriptComponent::Item item;
item.name = "ImpactFx";
sc.items.push_back(std::move(item));
// 自毁:延迟队列,帧尾统一执行(事件派发循环安全)
Destroy(static_cast<Engine::ECS::Entity>(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<Engine::ECS::Entity>(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<Engine::ECS::TransformComponent>();
tr.SetPosition(muzzle);
tr.SetRotationQuat(cam.GetRotationQuat());
// Dynamic 球刚体:参数给默认档(轻质量/低弹性,命中即停)
auto& rb = bullet.AddComponent<Engine::ECS::RigidBodyComponent>();
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>();
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