Files
HelloVulkan/App/scripts/ThirdPersonCamera.cpp
T

234 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.
//
// 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