diff --git a/App/scripts/GameDirector.cpp b/App/scripts/GameDirector.cpp index f8a4f95..ebce86e 100644 --- a/App/scripts/GameDirector.cpp +++ b/App/scripts/GameDirector.cpp @@ -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; diff --git a/App/scripts/PlayerController.cpp b/App/scripts/PlayerController.cpp new file mode 100644 index 0000000..057d6a0 --- /dev/null +++ b/App/scripts/PlayerController.cpp @@ -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 +#include + +#include +#include + +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 diff --git a/App/scripts/Target.cpp b/App/scripts/Target.cpp index f208988..d7cf709 100644 --- a/App/scripts/Target.cpp +++ b/App/scripts/Target.cpp @@ -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(); diff --git a/App/scripts/TestShoot.cpp b/App/scripts/TestShoot.cpp index 767157a..6e4ddb1 100644 --- a/App/scripts/TestShoot.cpp +++ b/App/scripts/TestShoot.cpp @@ -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 @@ -30,8 +41,6 @@ #include #include #include -#include -#include #include @@ -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(); tr.SetPosition(muzzle); - tr.SetRotationQuat(cam.GetRotationQuat()); + tr.SetRotationQuat(rot); // Dynamic 球刚体:参数给默认档(轻质量/低弹性,命中即停) auto& rb = bullet.AddComponent();