Files
HelloVulkan/App/scripts/TestShoot.cpp
T
atdunbg 334819bf97 修复打靶场射击链路:发射源/玩家控制走 primary 相机实体
- TestShoot 发射源从 EditorCamera(视口 UI 状态,PIE 渲染不用它)
  改为绑定实体位姿,挂 primary 相机实体即第一人称枪口;
  仅光标捕捉态开火,防编辑器交互误射
- 新增 PlayerController:FPS 控制(鼠标捕捉/Esc 释放/点视口重捕、
  yaw+pitch 转向、WASD+Space/Ctrl 移动+Shift 加速),驱动 primary
  相机实体 Transform,PIE 所见即玩家所见
- 靶子/碎片玻璃 alpha 0.42/0.45→0.85、自发光 2→3:低不透明度在
  亮背景下靶子几乎隐形(用户误认 HUD 横条为靶子)
2026-10-09 13:13:52 +08:00

242 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// TestShoot.cpp:射击脚本(发射源 = 绑定实体位姿,挂谁从谁发)。
//
// 正确用法:挂到场景 primary 相机实体(CameraComponent.primary=true)
// 上——PIE/游戏形态渲染源就是它(SceneRenderer::BuildFrame 分派),
// 实体朝向即准星,实体位置 + MuzzleOffset 即枪口。配 PlayerController
// (FPS 转向/移动/鼠标捕捉)即完整第一人称射击。
//
// 三个脚本协作(放一个文件方便对照):
// TestShoot(挂相机实体):
// 光标捕捉态下鼠标左键上升沿 → 从绑定实体沿视线 spawn 子弹实体
// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入);
// 未捕捉(编辑器交互中)不射——防点 UI 误发
// Bullet(子弹实体,由 TestShoot 运行时装配):
// OnCreate 施加枪口初速(body 已由当帧 Step 对账建好);
// OnCollisionEnter → 命中点 spawn ImpactFx + 自毁;
// 超时自毁防飞出地图泄漏;存活期画调试十字 = 弹道可视化
// ImpactFx(特效实体,由 Bullet 运行时装配,空实体 + 调试点标):
// 扩张十字 + 白→红渐隐,寿命尽自毁——占位特效
//
// 历史注:早期版本从编辑器相机(EditorCamera)发射——那是视口 UI
// 状态,不是场景实体,PIE 渲染根本不用它(渲染源 = primary 相机
// 实体),子弹会从视口外飞出。已改为绑定实体位姿(Unity 组件语义:
// 挂谁从谁发)。
//
// 覆盖的引擎设施:
// - Spawn()/Destroy():ScriptBehavior 实体底座(Scene::CreateEntity/
// DestroyEntity 延迟队列)
// - 组件运行时装配:RigidBody(PhysicsWorld 下一帧对账建 body)、
// ScriptComponent(ScriptRuntime 下一帧对账建实例)
// - OnCollisionEnter/Exit:PhysicsWorld::GetEvents → ScriptRuntime
// 帧首派发
//
// 验收步骤:primary 相机实体挂 PlayerController + 本脚本 → Play →
// 鼠标转视角 → 左键射击:弹道点飞出 → 撞击瞬间原地绽放十字特效、
// 子弹消失。
//
#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 <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 {
// 防重入:Destroy 是帧尾延迟执行,命中帧内子弹与碎片群
// 可产生多对接触事件(碎片云一块挨一块),无标志会一帧
// 爆出 N 个特效(同 WallFracture m_Fired 的一次性触发模式)
if (m_Hit) return;
m_Hit = true;
(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;
bool m_Hit = false; // 首次命中置位(防同帧多次接触重复特效)
};
// ---- 命中特效脚本(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;
};
// ---- 枪口脚本(挂谁从谁发:位姿取绑定实体,正挂 primary 相机)----
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;
const bool down =
Input::IsMouseButtonDown(Input::MouseButton::Left);
// 捕捉态门控:光标未捕捉 = 用户在跟编辑器 UI 交互,不射
// (m_FireWasDown 照常对账,重捕按下的那一下不误发)
if (!Input::IsCursorCaptured()) {
m_FireWasDown = down;
return;
}
// 左键上升沿触发(电平查询 + 上帧状态对比,Unity
// GetMouseButtonDown 边沿语义的手工等价物)
const bool fire = down && !m_FireWasDown &&
m_Cooldown <= 0.0f;
m_FireWasDown = down;
if (fire) {
SpawnBullet();
m_Cooldown = m_FireCooldown;
}
}
private:
void SpawnBullet() {
// 枪口 = 绑定实体视线前方(挂 primary 相机实体 = 第一人称
// 射击;PIE 渲染源就是这台相机,所见即所射)
const glm::quat rot = Transform().GetWorldRotationQuat();
const glm::vec3 fwd = rot * glm::vec3(0.0f, 0.0f, -1.0f);
const glm::vec3 muzzle =
Transform().GetWorldPosition() + fwd * m_MuzzleOffset;
// ---- 子弹实体装配(spawn 底座 + 组件运行时装配演示)----
auto bullet = Spawn("bullet");
auto& tr = bullet.GetComponent<Engine::ECS::TransformComponent>();
tr.SetPosition(muzzle);
tr.SetRotationQuat(rot);
// 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