234 lines
11 KiB
C++
234 lines
11 KiB
C++
//
|
||
// ThirdPersonCamera.cpp:第三人称环绕相机(UE SpringArm 同位)。
|
||
//
|
||
// 挂 primary 相机实体。鼠标增量 → yaw/pitch,相机位置 = 目标实体
|
||
// (按名解析,默认 "player")+ 球面偏移,朝向锁定目标。WASDMove 的
|
||
// 移动参考 = 本相机朝向——WASD 即"相机水平朝向的前后左右"(UE
|
||
// gameplay 语义)。枢轴水平硬跟、竖直带阻尼(FollowResponse):XZ
|
||
// 与角色零滞后(跑动不错位),Y 滤掉脚底每物理步的微弹跳(踩碎块/
|
||
// 撞墙)避免画面抖动。
|
||
//
|
||
// OnStart 从编辑器摆的机位反推 yaw/pitch/dist:编辑器摆哪,游戏就
|
||
// 从哪个角度开场。Esc 释放光标(调参),左键重捕继续玩。
|
||
//
|
||
// 防穿墙:枢轴→期望机位做静态几何射线(UE SpringArm 同位),命中则拉近;
|
||
// 拉近瞬时、拉远平滑,贴墙最小距离可配。
|
||
//
|
||
|
||
#include <Engine/Scripting/Script.h>
|
||
#include <Engine/Core/Application.h>
|
||
#include <Engine/Core/Input.h>
|
||
#include <Engine/Core/Log.h>
|
||
#include <Engine/ECS/Components.h>
|
||
#include <Engine/ECS/Scene.h>
|
||
#include <Engine/Physics/PhysicsRay.h> // RayHit(防穿墙射线结果)
|
||
#include <Engine/Physics/PhysicsWorld.h> // App().GetPhysics().RayCastStatic 门面
|
||
|
||
#include <entt/entt.hpp>
|
||
|
||
#include <glm/glm.hpp>
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
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<Engine::ECS::NameComponent>();
|
||
for (auto e : byName) {
|
||
if (byName.get<Engine::ECS::NameComponent>(e).name ==
|
||
m_TargetName) {
|
||
m_Target = e;
|
||
break;
|
||
}
|
||
}
|
||
// 名称未命中(未命名/改名)→ 回退第一个角色控制器实体
|
||
if (m_Target == entt::null) {
|
||
auto view = reg.view<Engine::ECS::CharacterControllerComponent>();
|
||
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
|