// // ThirdPersonCamera.cpp:第三人称环绕相机(UE SpringArm 同位)。 // // 挂 primary 相机实体。鼠标增量 → yaw/pitch,相机位置 = 目标实体 // (按名解析,默认 "player")+ 球面偏移,朝向锁定目标。WASDMove 的 // 移动参考 = 本相机朝向——WASD 即"相机水平朝向的前后左右"(UE // gameplay 语义)。枢轴水平硬跟、竖直带阻尼(FollowResponse):XZ // 与角色零滞后(跑动不错位),Y 滤掉脚底每物理步的微弹跳(踩碎块/ // 撞墙)避免画面抖动。 // // OnStart 从编辑器摆的机位反推 yaw/pitch/dist:编辑器摆哪,游戏就 // 从哪个角度开场。Esc 释放光标(调参),左键重捕继续玩。 // // 防穿墙:枢轴→期望机位做静态几何射线(UE SpringArm 同位),命中则拉近; // 拉近瞬时、拉远平滑,贴墙最小距离可配。 // #include #include #include #include #include #include #include // RayHit(防穿墙射线结果) #include // App().GetPhysics().RayCastStatic 门面 #include #include #include #include using namespace Engine; namespace { // 拉远回弹响应(1/s,指数逼近):离开墙角后镜头平滑归位。 // 拉近是瞬时的(见 OnUpdate)——遮挡晚一帧就会穿墙,不能平滑 constexpr float kCameraReturnResponse = 8.0f; class ThirdPersonCamera final : public Engine::Scripting::ScriptBehavior { public: const char* Name() const override { return "ThirdPersonCamera"; } void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { pb.Field("Sensitivity", &m_Sensitivity, 1.0f); pb.Field("Distance", &m_Distance, 0.0f); // 0 = 吸编辑器机位 pb.Field("HeightOffset", &m_HeightOffset, 1.2f); pb.Field("PitchMinDeg", &m_PitchMin, -35.0f); pb.Field("PitchMaxDeg", &m_PitchMax, 75.0f); pb.Field("GamepadTurnSpeed", &m_PadTurnSpeed, 180.0f); // 跟随目标按名锁定(默认 "player"):场景有多个角色控制器 // 实体(如跟随 NPC)时,按名解析保证相机锁主角而非随机命中 pb.Field("TargetName", &m_TargetName, std::string("player")); // 竖直跟随阻尼(1/s,指数逼近;0 = 硬跟=旧行为)。水平 XZ // 恒硬跟(保证跑动时相机/角色不错位、无拖影——全轴阻尼会 // 让角色在高速移动时被"甩"出画面并抖动);仅 Y 阻尼吸收 // 角色脚底每物理步的微弹跳(踩碎块/撞墙),帧率无关 pb.Field("FollowResponse", &m_FollowResponse, 12.0f); // 目标 Y 瞬移超过该距离(PIE 传送/回合复位)→ 直接吸附 pb.Field("SnapDistance", &m_SnapDistance, 3.0f); // 相机防穿墙(UE SpringArm 同位):枢轴→期望机位之间若有静态 // 几何,把相机拉到命中点前。关掉 = 旧行为(可穿墙) pb.Field("CameraCollision", &m_Collision, true); // 贴墙时相机离枢轴的最小距离(再近会切进角色/近平面) pb.Field("MinDistance", &m_MinDistance, 0.35f); // 命中点回退余量(防止相机面贴墙 → z-fighting/切面) pb.Field("CollisionSkin", &m_CollisionSkin, 0.25f); } void OnStart() override { auto& reg = App().GetScene().GetRegistry(); // ---- 找目标:优先按名(TargetName)命中主角 ---- auto byName = reg.view(); for (auto e : byName) { if (byName.get(e).name == m_TargetName) { m_Target = e; break; } } // 名称未命中(未命名/改名)→ 回退第一个角色控制器实体 if (m_Target == entt::null) { auto view = reg.view(); if (view.empty()) { ENGINE_WARN( "[ThirdPersonCamera] 场景无 CharacterController 实体,相机静止"); return; } m_Target = *view.begin(); } // ---- 从编辑器机位反推 yaw/pitch/dist(摆哪开场在哪)---- const auto& camT = Transform(); const auto& tgtT = App().GetScene().GetRegistry().get< Engine::ECS::TransformComponent>(m_Target); const glm::vec3 offset = camT.GetPosition() - (tgtT.GetPosition() + glm::vec3(0.0f, m_HeightOffset, 0.0f)); const float dist = glm::length(offset); if (dist > 1e-4f && m_Distance <= 0.0f) { m_Distance = dist; m_Pitch = glm::degrees(std::asin(std::clamp(offset.y / dist, -1.0f, 1.0f))); m_Yaw = glm::degrees(std::atan2(offset.x, offset.z)); } ENGINE_INFO( "[ThirdPersonCamera] 目标已锁定,dist={:.2f} yaw={:.1f} pitch={:.1f}", m_Distance, m_Yaw, m_Pitch); Input::SetCursorCaptured(true); } void OnUpdate(float dt) override { if (m_Target == entt::null) { return; } // 光标管理:Esc 释放(Inspector 调参),左键重捕。 // 手柄玩家不碰鼠标——光标保持捕获态不影响摇杆输入 if (Input::IsKeyDown(Input::Key::Escape)) { Input::SetCursorCaptured(false); } if (Input::IsMouseButtonDown(Input::MouseButton::Left)) { Input::SetCursorCaptured(true); } // ---- 鼠标增量 + 右摇杆 → 球面角 ---- // 鼠标 = 相对增量(GetMouseDelta);右摇杆 = 持续量 //(Look 动作,满推 m_PadTurnSpeed 度/秒),两者叠加 float dx = 0.0f, dy = 0.0f; Input::GetMouseDelta(dx, dy); const glm::vec2 padLook = Input::ActionValue2("Look"); const float k = 0.12f * m_Sensitivity; m_Yaw -= dx * k + padLook.x * m_PadTurnSpeed * dt; m_Pitch += dy * k + padLook.y * m_PadTurnSpeed * dt; m_Pitch = std::clamp(m_Pitch, m_PitchMin, m_PitchMax); m_Yaw = std::fmod(m_Yaw, 360.0f); // ---- 位置 = 目标枢轴 + 球面偏移 ---- // 水平 XZ 硬跟(与角色零滞后——跑动时相机/角色不错位、无 // 拖影),仅 Y 经阻尼逼近(FollowResponse):吸收目标脚底 // 每物理步的微弹跳(踩碎块/撞墙),相机竖直方向不再 1:1 // 抖动。首帧、瞬移(超 SnapDistance)或阻尼为 0 时直接吸附 // = 旧硬跟语义 const auto& tgtT = App().GetScene().GetRegistry().get< Engine::ECS::TransformComponent>(m_Target); const glm::vec3 rawPivot = tgtT.GetPosition() + glm::vec3(0.0f, m_HeightOffset, 0.0f); if (!m_FollowInit || m_FollowResponse <= 0.0f || std::abs(rawPivot.y - m_FollowPivotY) > m_SnapDistance) { m_FollowPivotY = rawPivot.y; m_FollowInit = true; } else { m_FollowPivotY += (rawPivot.y - m_FollowPivotY) * (1.0f - std::exp(-m_FollowResponse * dt)); } glm::vec3 pivot = rawPivot; // XZ 硬跟 pivot.y = m_FollowPivotY; // Y 阻尼 const float yawR = glm::radians(m_Yaw); const float pitchR = glm::radians(m_Pitch); const glm::vec3 dir( // 枢轴 → 相机(yaw 绕 Y,pitch 抬升) std::sin(yawR) * std::cos(pitchR), std::sin(pitchR), std::cos(yawR) * std::cos(pitchR)); // ---- 相机防穿墙:枢轴 → 期望机位射线拉近 ---- // 只打静态几何(碎块/动态物不当遮挡,避免镜头被飘过的碎片 // 乱拉);枢轴在角色胶囊内,角色是 CharacterVirtual 不进射线, // 不会自己挡自己。拉近瞬时(遮挡晚一帧就穿墙),拉远平滑 float wantDist = m_Distance; if (m_Collision) { RayHit hit; if (App().GetPhysics().RayCastStatic( pivot, dir, m_Distance + m_CollisionSkin, hit)) { wantDist = std::max(hit.distance - m_CollisionSkin, m_MinDistance); } } if (!m_DistInit) { // 首帧吸附(无回弹动画) m_CurDist = wantDist; m_DistInit = true; } else if (wantDist < m_CurDist) { m_CurDist = wantDist; // 拉近:瞬时 } else { m_CurDist += (wantDist - m_CurDist) * (1.0f - std::exp(-kCameraReturnResponse * dt)); } auto& camT = Transform(); camT.SetPosition(pivot + dir * m_CurDist); // ---- 朝向锁定目标 ---- // 引擎 YXZ 序(R = Ry·Rx)视向 = (-cosP sinY, sinP, -cosP cosY) // 反解:pitch = asin(vd.y),yaw = atan2(-vd.x, -vd.z) const glm::vec3 vd = glm::normalize(pivot - camT.GetPosition()); const float p = glm::degrees(std::asin(std::clamp(vd.y, -1.0f, 1.0f))); const float y = glm::degrees(std::atan2(-vd.x, -vd.z)); camT.SetRotation(glm::vec3(p, y, 0.0f)); } private: float m_Sensitivity = 1.0f; float m_Distance = 0.0f; // 0 = OnStart 吸编辑器机位 float m_HeightOffset = 1.2f; // 枢轴高度(瞄胸口) float m_PitchMin = -35.0f; float m_PitchMax = 75.0f; float m_PadTurnSpeed = 180.0f; // 右摇杆满推角速度(度/秒) std::string m_TargetName = "player"; // 目标实体名(NameComponent) float m_FollowResponse = 12.0f; // 竖直跟随阻尼 1/s(0 = 硬跟) float m_SnapDistance = 3.0f; // Y 瞬移吸附阈值 m float m_FollowPivotY = 0.0f; // 阻尼后的枢轴高度(XZ 硬跟) bool m_FollowInit = false; // 首帧吸附基准 bool m_Collision = true; // 相机防穿墙开关 float m_MinDistance = 0.35f; // 贴墙最小距离 float m_CollisionSkin = 0.25f; // 命中点回退余量 float m_CurDist = 0.0f; // 防穿墙后的当前距离(拉近平滑态) bool m_DistInit = false; // 距离首帧吸附基准 float m_Yaw = 0.0f; float m_Pitch = 20.0f; entt::entity m_Target = entt::null; }; ENGINE_REGISTER_SCRIPT(ThirdPersonCamera) } // namespace