修复打靶场射击链路:发射源/玩家控制走 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 横条为靶子)
This commit is contained in:
2026-10-09 13:13:52 +08:00
parent f15b104e77
commit 334819bf97
4 changed files with 201 additions and 28 deletions
+10 -5
View File
@@ -21,9 +21,12 @@
// 跨回合复用;改参数重开局才重建。碎片几何由 FractureLib 每靶
// 现场生成(唯一代键,自管回收)。
//
// 【演示步骤】任意实体挂 GameDirector,相机实体挂 TestShoot →
// Play → RMB 飞行瞄准靶阵 → 左键射击:靶子玻璃式碎裂 + 得分点
// 亮起,倒计时条流逝,时间到结算(控制台输出得分),R 重开。
// 【演示步骤】任意实体挂 GameDirector;场景 primary 相机实体
// (CameraComponent.primary=true,PIE 渲染源)挂 PlayerController +
// TestShoot,摆到面向靶阵(如原点朝 -Z 正对 Center 默认位)→ Play:
// 光标自动捕捉(Esc 释放、点视口重捕),WASD/Space/Ctrl 移动 +
// 鼠标转向,左键射击:靶子玻璃式碎裂 + 得分点亮起,倒计时条流逝,
// 时间到结算(控制台输出得分),R 重开。
//
// 【缺失能力记录(玩法侧线的下一批依据)】
// - UI/HUD 文字(倒计时数字/得分数字只得用图元条凑)
@@ -103,10 +106,12 @@ namespace {
}
// 玻璃材质:低金属度高光洁 + alpha 混合 + 靶色自发光
// (alpha 0.85、自发光 3——曾用 alpha 0.42 近乎全透,亮背景
// 下靶子隐形;拉高保靶子在浅色环境里醒目)
mesh.name = "target";
mesh.params.factors = {0.05f, 0.15f, 0.0f, 0.0f};
mesh.params.baseColor = glm::vec4{color, 0.42f};
mesh.params.surface = {1.5f, 0.0f, 2.0f, 0.0f};
mesh.params.baseColor = glm::vec4{color, 0.85f};
mesh.params.surface = {1.5f, 0.0f, 3.0f, 0.0f};
mesh.params.emissiveFactor = glm::vec4{color * 0.85f, 1.0f};
mesh.transparent = true;
return parsed;
+151
View File
@@ -0,0 +1,151 @@
//
// PlayerController.cpp:FPS 玩家控制脚本——挂在场景 primary 相机实体
// (CameraComponent.primary=true)上,直接驱动该实体的 Transform。
//
// 为什么驱动实体而不是"编辑器相机":PIE 内嵌播放/游戏形态的渲染源
// 是 primary 相机实体(SceneRenderer::BuildFrame 分派,main.cpp 亦
// 注明"PIE 内嵌播放:渲染源已切 primary 相机,编辑器相机停驱")。
// 实体位姿一动,视口所见即玩家所见——这才是 Unity/UE 的正道;
// EditorCamera 是视口 UI 状态,不是场景实体,脚本够不着也不该够。
//
// 能力(全部走 Input 门面,零引擎改动):
// - 鼠标捕捉:OnStart 捕捉(FPS 语义:光标隐入、无限转向);
// Esc 释放(交还编辑器交互);未捕捉时点视口重新捕捉
// - 视角:GetMouseDelta 增量 → yaw 绕世界 up 左乘 + pitch 绕本地
// X 右乘(±89° 钳制,与编辑器相机 Camera::Update 同语义)
// - 移动:WASD 水平 + Space 升 / Ctrl 降(飞行式,无重力——
// 打靶场微调机位用);Shift 加速
//
// 注意:捕捉态下编辑器 UI 收不到鼠标——这是游戏体验的代价;
// Esc 是唯一退路(释放后可点 Inspector/改参数)。
//
// 配套:TestShoot(挂同一实体)射击,同样只在捕捉态开火。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Input.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
using namespace Engine; // Input 门面
using Engine::Scripting::ParamBuilder;
namespace {
class PlayerController final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "PlayerController"; }
void DeclareParams(ParamBuilder& pb) override {
pb.Field("LookSensitivity", &m_Sensitivity, 0.12f); // 度/像素
pb.Field("MoveSpeed", &m_MoveSpeed, 4.0f); // 平移速度 m/s
pb.Field("RunSpeed", &m_RunSpeed, 8.0f); // Shift 加速 m/s
pb.Field("CaptureOnStart", &m_CaptureOnStart, true);
}
void OnStart() override {
if (m_CaptureOnStart) {
Input::SetCursorCaptured(true);
}
}
void OnUpdate(f32 dt) override {
// ---- 捕捉管理:Esc 释放 / 点视口重捕 ----
bool captured = Input::IsCursorCaptured();
if (captured && Input::IsKeyDown(Input::Key::Escape)) {
Input::SetCursorCaptured(false);
captured = false;
}
const bool leftDown =
Input::IsMouseButtonDown(Input::MouseButton::Left);
if (!captured) {
// 未捕捉时点击(点在视口内才收得到——ImGui 捕获的
// 区域静默)→ 重捕进入操作态
if (leftDown && !m_LeftWasDown) {
Input::SetCursorCaptured(true);
}
m_LeftWasDown = leftDown;
return; // 释放态:不转向不移动(编辑器交互让路)
}
m_LeftWasDown = leftDown;
Look();
Move(dt);
}
private:
void Look() {
f32 dx = 0.0f, dy = 0.0f;
Input::GetMouseDelta(dx, dy);
if (dx == 0.0f && dy == 0.0f) return;
auto& tr = Transform();
const glm::quat cur = tr.GetRotationQuat();
// yaw:绕世界 up 左乘(不随俯仰改变旋转轴)
const glm::quat yaw =
glm::angleAxis(glm::radians(-dx * m_Sensitivity),
glm::vec3(0.0f, 1.0f, 0.0f));
// pitch:绕本地 X 右乘,钳制 ±89°(防过顶翻转)
const glm::vec3 fwd = cur * glm::vec3(0.0f, 0.0f, -1.0f);
const f32 pitch =
glm::asin(glm::clamp(fwd.y, -1.0f, 1.0f));
const f32 kPitchLimit = glm::radians(89.0f);
const f32 pitchTarget =
glm::clamp(pitch + glm::radians(-dy * m_Sensitivity),
-kPitchLimit, kPitchLimit);
const glm::quat pitchStep =
glm::angleAxis(pitchTarget - pitch,
glm::vec3(1.0f, 0.0f, 0.0f));
tr.SetRotationQuat(yaw * (cur * pitchStep));
}
void Move(f32 dt) {
glm::vec3 axis{0.0f};
if (Input::IsKeyDown(Input::Key::W))
axis.z += 1.0f;
if (Input::IsKeyDown(Input::Key::S))
axis.z -= 1.0f;
if (Input::IsKeyDown(Input::Key::D))
axis.x += 1.0f;
if (Input::IsKeyDown(Input::Key::A))
axis.x -= 1.0f;
if (Input::IsKeyDown(Input::Key::Space))
axis.y += 1.0f;
if (Input::IsKeyDown(Input::Key::LeftControl))
axis.y -= 1.0f;
if (glm::dot(axis, axis) < 1e-6f) return;
const glm::quat r = Transform().GetRotationQuat();
const glm::vec3 fwd = r * glm::vec3(0.0f, 0.0f, -1.0f);
const glm::vec3 right = r * glm::vec3(1.0f, 0.0f, 0.0f);
// 水平走视线投影(含俯仰 = 飞行感),升降用世界 Y
glm::vec3 move =
fwd * axis.z + right * axis.x
+ glm::vec3(0.0f, axis.y, 0.0f);
const f32 len = glm::length(move);
if (len < 1e-6f) return;
move /= len;
const f32 speed = Input::IsKeyDown(Input::Key::LeftShift)
? m_RunSpeed
: m_MoveSpeed;
auto& tr = Transform();
tr.SetPosition(tr.GetWorldPosition() + move * speed * dt);
}
f32 m_Sensitivity = 0.12f;
f32 m_MoveSpeed = 4.0f;
f32 m_RunSpeed = 8.0f;
bool m_CaptureOnStart = true;
bool m_LeftWasDown = false; // 重捕用的左键上升沿基准
};
ENGINE_REGISTER_SCRIPT(PlayerController)
} // namespace
+5 -3
View File
@@ -57,11 +57,13 @@ namespace {
void OnCollisionEnter(u32 other, const glm::vec3& point,
const glm::vec3& normal) override {
// 玻璃外观:半透明 + 靶色自发光(Fire 前套用,碎片沿用;
// 每次覆写代价可忽略,且吃 Inspector 实时改色)
m_Burst.look.baseColor = glm::vec4{m_Color, 0.45f};
// 每次覆写代价可忽略,且吃 Inspector 实时改色)。
// alpha 0.85(与靶体 BuildGlassCube 同步:0.45 时代碎片
// 在亮背景里几乎隐形)
m_Burst.look.baseColor = glm::vec4{m_Color, 0.85f};
m_Burst.look.transparent = true;
m_Burst.look.emissive = m_Color * 0.7f;
m_Burst.look.emissiveStrength = 2.0f;
m_Burst.look.emissiveStrength = 3.0f;
if (m_Burst.TryFire(HV(), other, point, normal)) {
ReportScore();
+35 -20
View File
@@ -1,11 +1,16 @@
//
// TestShoot.cpp:阶段1 验收脚本——脚本侧 spawn/destroy 底座 + 碰撞事件
// 转发的端到端链路。
// TestShoot.cpp:射击脚本(发射源 = 绑定实体位姿,挂谁从谁发)。
//
// 正确用法:挂到场景 primary 相机实体(CameraComponent.primary=true)
// 上——PIE/游戏形态渲染源就是它(SceneRenderer::BuildFrame 分派),
// 实体朝向即准星,实体位置 + MuzzleOffset 即枪口。配 PlayerController
// (FPS 转向/移动/鼠标捕捉)即完整第一人称射击。
//
// 三个脚本协作(放一个文件方便对照):
// TestShoot(挂任意实体,如相机实体):
// 鼠标左键上升沿 → 从编辑器相机沿视线 spawn 子弹实体
// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入)
// TestShoot(挂相机实体):
// 光标捕捉态下鼠标左键上升沿 → 从绑定实体沿视线 spawn 子弹实体
// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入);
// 未捕捉(编辑器交互中)不射——防点 UI 误发
// Bullet(子弹实体,由 TestShoot 运行时装配):
// OnCreate 施加枪口初速(body 已由当帧 Step 对账建好);
// OnCollisionEnter → 命中点 spawn ImpactFx + 自毁;
@@ -13,6 +18,11 @@
// ImpactFx(特效实体,由 Bullet 运行时装配,空实体 + 调试点标):
// 扩张十字 + 白→红渐隐,寿命尽自毁——占位特效
//
// 历史注:早期版本从编辑器相机(EditorCamera)发射——那是视口 UI
// 状态,不是场景实体,PIE 渲染根本不用它(渲染源 = primary 相机
// 实体),子弹会从视口外飞出。已改为绑定实体位姿(Unity 组件语义:
// 挂谁从谁发)。
//
// 覆盖的引擎设施:
// - Spawn()/Destroy():ScriptBehavior 实体底座(Scene::CreateEntity/
// DestroyEntity 延迟队列)
@@ -21,8 +31,9 @@
// - OnCollisionEnter/Exit:PhysicsWorld::GetEvents → ScriptRuntime
// 帧首派发
//
// 验收步骤:编辑器给任意实体挂 TestShoot → Play → RMB 飞行视角瞄准
// 墙体 → 左键射击:弹道点飞出 → 撞墙瞬间原地绽放十字特效、子弹消失。
// 验收步骤:primary 相机实体挂 PlayerController + 本脚本 → Play →
// 鼠标转视角 → 左键射击:弹道点飞出 → 撞击瞬间原地绽放十字特效、
// 子弹消失。
//
#include <Engine/Scripting/Script.h>
@@ -30,8 +41,6 @@
#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>
@@ -139,7 +148,7 @@ namespace {
f32 m_Age = 0.0f;
};
// ---- 枪口脚本(验收主体:挂到任意实体,位置/朝向取编辑器相机)----
// ---- 枪口脚本(挂谁从谁发:位姿取绑定实体,正挂 primary 相机)----
class TestShoot final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "TestShoot"; }
@@ -158,10 +167,18 @@ namespace {
void OnUpdate(f32 dt) override {
m_Cooldown -= dt;
// 左键上升沿触发(电平查询 + 上帧状态对比,Unity
// GetMouseButtonDown 边沿语义的手工等价物)
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;
@@ -173,20 +190,18 @@ namespace {
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);
// 枪口 = 绑定实体视线前方(挂 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 =
cam.GetWorldPosition() + fwd * m_MuzzleOffset;
Transform().GetWorldPosition() + fwd * m_MuzzleOffset;
// ---- 子弹实体装配(spawn 底座 + 组件运行时装配演示)----
auto bullet = Spawn("bullet");
auto& tr = bullet.GetComponent<Engine::ECS::TransformComponent>();
tr.SetPosition(muzzle);
tr.SetRotationQuat(cam.GetRotationQuat());
tr.SetRotationQuat(rot);
// Dynamic 球刚体:参数给默认档(轻质量/低弹性,命中即停)
auto& rb = bullet.AddComponent<Engine::ECS::RigidBodyComponent>();