以《怪物猎人》为例:深入解析 UE5 GameplayCamera 相机系统
文档版本: 1.0 | 更新日期: 2026-02-25
目录
- 1. 怪物猎人相机需求分析
- 2. GameplayCamera 核心架构回顾
- 3. 场景一:武器切换时的相机过渡
- 4. 场景二:锁敌系统与效果叠加
- 5. 场景三:连续相机切换请求
- 6. 场景四:骑乘怪物时的多相机协同
- 7. 场景五:受击震动与视觉冲击
- 8. 完整实现:怪物猎人风格相机系统
- 9. 性能优化与最佳实践
- 10. 与 Cinemachine 的深度对比
1. 怪物猎人相机需求分析
1.1 游戏中的相机场景
《怪物猎人》拥有极其复杂的相机系统,我们来看几个典型场景:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 怪物猎人相机场景概览 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 探索模式 │ │ 战斗模式 │ │ 骑乘模式 │ │
│ │ │ │ │ │ │ │
│ │ • 第三人称跟随 │ │ • 锁敌追踪 │ │ • 第一人称骑乘 │ │
│ │ • 自由视角旋转 │ │ • 侧闪避相机 │ │ • 怪物背部视角 │ │
│ │ • 距离可调 │ │ • 武器特写 │ │ • 特殊攻击镜头 │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │ │ │ │
│ └──────────────────────┼──────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ 效果叠加层 │ │
│ │ │ │
│ │ • 受击震动 │ │
│ │ • 药水喝药视角偏移 │ │
│ │ • 蹲伏时的低视角 │ │
│ │ • 斩味系统视觉反馈 │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
1.2 具体场景拆解
场景 A:大剑蓄力攻击
时间线:
T=0s T=0.5s T=1.0s T=1.5s T=2.0s
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌───┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│普通│───▶│蓄力中 │──▶│蓄力满 │──▶│释放 │──▶│恢复 │
│视角│ │相机拉近│ │微震动 │ │镜头抖动│ │普通 │
└───┘ └───────┘ └───────┘ └───────┘ └───────┘
FOV变化 特效叠加 大震动
场景 B:从探索到锁敌战斗
时间线:
T=0s T=0.3s T=0.5s T=0.8s T=1.0s
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌───┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│探索│───▶│探索→战斗│──▶│+锁敌 │──▶│+受击 │──▶│战斗 │
│相机│ │混合中 │ │效果 │ │震动 │ │相机 │
└───┘ └───────┘ └───────┘ └───────┘ └───────┘
30%进度 混合继续 多效果叠加
场景 C:骑乘怪物的复杂相机链
时间线:
T=0s T=0.5s T=1.0s T=1.5s T=2.0s
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌───┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│战斗│───▶│战斗→骑乘│──▶│骑乘→特殊│──▶│特殊→骑乘│──▶│骑乘 │
│相机│ │混合中 │ │攻击 │ │攻击 │ │相机 │
└───┘ └───────┘ └───────┘ └───────┘ └───────┘
混合被打断 新混合开始 又被打断
2. GameplayCamera 核心架构回顾
2.1 四层架构
GameplayCamera 使用四层架构来组织相机逻辑:
┌─────────────────────────────────────────────────────────────────────────────┐
│ GameplayCamera 四层架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Layer 4: Visual Layer (视觉层) │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ • 后处理效果 │ │ │
│ │ │ • 视觉反馈 (如低血量红色边框) │ │ │
│ │ │ • 始终叠加的镜头偏移 │ │ │
│ │ │ Example: 喝药时的视角微调 │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ▲ 叠加 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Layer 3: Global Layer (全局层) │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ • 全局效果 (震动、冲击) │ │ │
│ │ │ • 持久性效果 │ │ │
│ │ │ Example: 锁敌追踪、受击震动 │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ▲ 叠加 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Layer 2: Main Layer (主层) │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ • 游戏玩法相机 │ │ │
│ │ │ • 混合栈 (Blend Stack) │ │ │
│ │ │ Example: 探索相机 ↔ 战斗相机 ↔ 骑乘相机 │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ▲ 叠加 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Layer 1: Base Layer (基础层) │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ • 附加到玩家/目标 │ │ │
│ │ │ • 基础跟随逻辑 │ │ │
│ │ │ Example: 相机始终跟随玩家位置 │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 执行顺序:Base → Main → Global → Visual │
│ 每一层的结果叠加到前一层之上 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2.2 关键概念
| 概念 | 说明 | 怪物猎人对应场景 |
|---|---|---|
| Camera Rig | 相机装备,包含一组节点定义行为 | 探索Rig、战斗Rig、骑乘Rig |
| Camera Node | 单一功能节点(数据定义) | 跟随节点、瞄准节点、震动节点 |
| Node Evaluator | 节点评估器(逻辑实现) | 计算位置、计算旋转、应用震动 |
| Camera Pose | 相机姿态(位置+旋转+镜头参数) | 每帧计算的最终结果 |
| Blend Stack | 混合栈,管理相机过渡 | 武器切换时的平滑过渡 |
| Layer | 层,组织效果叠加顺序 | 基础跟随 → 战斗相机 → 锁敌 → 震动 |
2.3 评估流程
// 每帧的评估流程
void FCameraSystemEvaluator::Evaluate(float DeltaTime)
{
// 1. 准备参数
FCameraNodeEvaluationParams Params;
Params.DeltaTime = DeltaTime;
Params.VariableTable = &VariableTable;
// 2. 初始化结果
FCameraNodeEvaluationResult Result;
Result.CameraPose = FCameraPose::Identity;
// 3. 按层顺序评估
// Base Layer
if (BaseLayerEvaluator)
{
BaseLayerEvaluator->Run(Params, Result);
// Result 现在包含基础跟随的位置
}
// Main Layer (包含混合栈)
if (MainLayerEvaluator)
{
MainLayerEvaluator->Run(Params, Result);
// Result 现在包含主相机的结果(可能是混合中的状态)
}
// Global Layer (锁敌、震动等)
if (GlobalLayerEvaluator)
{
GlobalLayerEvaluator->Run(Params, Result);
// Result 现在叠加了全局效果
}
// Visual Layer (后处理、视觉反馈)
if (VisualLayerEvaluator)
{
VisualLayerEvaluator->Run(Params, Result);
// Result 现在叠加了视觉效果
}
// 4. 应用到实际相机
ApplyResultToCamera(Result);
}
3. 场景一:武器切换时的相机过渡
3.1 场景描述
玩家从大剑切换到双刀:
T=0s T=0.5s T=1.0s
│ │ │
▼ ▼ ▼
┌─────────┐ ┌────────────┐ ┌─────────┐
│大剑相机 │─────────▶│ 混合中 │────────▶│双刀相机 │
│ │ │ (50%进度) │ │ │
│• 距离远 │ │ │ │• 距离近 │
│• FOV窄 │ │ │ │• FOV宽 │
│• 侧后方 │ │ │ │• 更靠后 │
└─────────┘ └────────────┘ └─────────┘
3.2 Main Layer 的混合栈机制
┌─────────────────────────────────────────────────────────────────────────────┐
│ Main Layer Blend Stack │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ === 初始状态 === │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Blend Stack: [GreatSwordRig] │ │
│ │ │ │
│ │ 激活相机: GreatSwordRig │ │
│ │ 结果状态: GreatSwordRig 计算的姿态 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ === 请求切换到双刀 (T=0s) === │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Blend Stack: [GreatSwordRig ←──Blend──→ DualBladesRig] │ │
│ │ CamA 混合中 CamB │ │
│ │ │ │
│ │ 混合时间: 1.0 秒 │ │
│ │ 混合曲线: EaseInOut │ │
│ │ 当前权重: 0% (完全 CamA) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ === 混合进行中 (T=0.5s) === │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Blend Stack: [GreatSwordRig ←──Blend(50%)──→ DualBladesRig] │ │
│ │ │ │
│ │ 当前权重: 50% │ │
│ │ 结果状态: Lerp(GreatSword.State, DualBlades.State, 0.5) │ │
│ │ │ │
│ │ 位置: 大剑位置和双刀位置的中间 │ │
│ │ 旋转: 大剑朝向和双刀朝向的球面插值 │ │
│ │ FOV: Lerp(65, 75, 0.5) = 70 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ === 混合完成 (T=1.0s) === │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Blend Stack: [DualBladesRig] │ │
│ │ │ │
│ │ 激活相机: DualBladesRig │ │
│ │ GreatSwordRig 已从栈中移除 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
3.3 内部代码实现
// ========================================
// FTransientBlendStackCameraNodeEvaluator
// 管理 Main Layer 的混合栈
// ========================================
void FTransientBlendStackCameraNodeEvaluator::OnRun(
const FCameraNodeEvaluationParams& Params,
FCameraNodeEvaluationResult& OutResult)
{
// 1. 更新所有活动的混合
UpdateActiveBlends(Params.DeltaTime);
// 2. 移除已完成的混合
RemoveCompletedBlends();
// 3. 计算最终结果
if (BlendStack.IsEmpty())
{
// 没有活动相机
return;
}
if (BlendStack.Num() == 1)
{
// 只有一个相机,直接评估
BlendStack[0].CameraEvaluator->Run(Params, OutResult);
}
else
{
// 有混合,从栈底到栈顶依次混合
FCameraPose AccumulatedPose = FCameraPose::Identity;
for (int32 i = 0; i < BlendStack.Num(); ++i)
{
FBlendEntry& Entry = BlendStack[i];
// 评估当前相机
FCameraNodeEvaluationResult TempResult;
TempResult.CameraPose = AccumulatedPose;
Entry.CameraEvaluator->Run(Params, TempResult);
// 如果是第一个,直接使用
if (i == 0)
{
AccumulatedPose = TempResult.CameraPose;
}
else
{
// 与之前的结果混合
float Weight = Entry.BlendWeight;
AccumulatedPose = FCameraPose::Lerp(
AccumulatedPose,
TempResult.CameraPose,
Weight);
}
}
OutResult.CameraPose = AccumulatedPose;
}
}
void FTransientBlendStackCameraNodeEvaluator::PushNewCamera(
FCameraNodeEvaluator* NewCameraEvaluator,
float BlendTime,
ECameraBlendType BlendType)
{
// 创建新的混合条目
FBlendEntry NewEntry;
NewEntry.CameraEvaluator = NewCameraEvaluator;
NewEntry.BlendWeight = 0.0f;
NewEntry.TargetWeight = 1.0f;
NewEntry.BlendTimeRemaining = BlendTime;
// 添加到栈顶
BlendStack.Add(NewEntry);
// 将之前所有条目的目标权重设为 0
for (int32 i = 0; i < BlendStack.Num() - 1; ++i)
{
BlendStack[i].TargetWeight = 0.0f;
}
}
void FTransientBlendStackCameraNodeEvaluator::UpdateActiveBlends(float DeltaTime)
{
for (FBlendEntry& Entry : BlendStack)
{
if (Entry.BlendTimeRemaining > 0)
{
// 更新混合权重
float BlendProgress = 1.0f - (Entry.BlendTimeRemaining / Entry.TotalBlendTime);
Entry.BlendWeight = EvaluateBlendCurve(BlendProgress);
Entry.BlendTimeRemaining -= DeltaTime;
}
else
{
Entry.BlendWeight = Entry.TargetWeight;
}
}
}
3.4 相机混合曲线
// 不同的混合曲线类型
enum class ECameraBlendType : uint8
{
Linear, // 线性混合
EaseIn, // 慢入快出
EaseOut, // 快入慢出
EaseInOut, // 两头慢中间快
Cubic, // 三次曲线
};
float EvaluateBlendCurve(float Progress, ECameraBlendType BlendType)
{
// Progress: 0.0 ~ 1.0
// 返回: 0.0 ~ 1.0 (权重)
switch (BlendType)
{
case ECameraBlendType::Linear:
return Progress;
case ECameraBlendType::EaseIn:
// 缓入:开始慢,后面快
return Progress * Progress;
case ECameraBlendType::EaseOut:
// 缓出:开始快,后面慢
return 1.0f - (1.0f - Progress) * (1.0f - Progress);
case ECameraBlendType::EaseInOut:
// 两头慢中间快(SmoothStep)
return Progress * Progress * (3.0f - 2.0f * Progress);
case ECameraBlendType::Cubic:
// 三次曲线,更平滑
return Progress * Progress * Progress * (Progress * (6.0f * Progress - 15.0f) + 10.0f);
default:
return Progress;
}
}
混合曲线可视化:
权重 (Weight)
1.0 ┤ ╭────── EaseOut
│ ╭────╯
│ ╭────╯ EaseInOut
0.8 ┤ ╭─────╯
│ ╭────╯
│ ╭────╯ Linear
0.6 ┤╭───╯
│╯
0.5 ┤ ╭───────╮
│ ╱ ╲
0.4 ┤ ╱ ╲
│ ╱ ╲ EaseIn
0.2 ┤ ╱ ╲
│╱ ╲
0.0 ┼────────────────────╲────────────────
0 0.2 0.4 0.6 0.8 1.0
Progress (进度)
4. 场景二:锁敌系统与效果叠加
4.1 场景描述
怪物猎人锁敌系统:
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ 相机 │
│ ● │
│ \ │
│ \ 锁敌视线 │
│ \ │
│ 玩家 ▼ 怪物 │
│ ● ─────────────────────────────▶ ▲ │
│ │ │
│ │ 锁敌点 (通常是头部) │
│ ┌──┴──┐ │
│ │ │ │
│ └─────┘ │
│ │
│ 锁敌效果: │
│ 1. 相机自动转向锁敌点 │
│ 2. 保持一定程度的玩家控制 │
│ 3. 可以切换锁敌部位 │
│ 4. 怪物移动时平滑跟随 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.2 使用 Global Layer 实现锁敌
┌─────────────────────────────────────────────────────────────────────────────┐
│ 锁敌效果的层级结构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Visual Layer (空) │
│ ▲ │
│ │ 叠加 │
│ Global Layer: LockOnRig │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ LockOnAimNode: 修改相机旋转,指向锁敌目标 │ │
│ │ │ │
│ │ 输入: Main Layer 的结果 (相机位置已经确定) │ │
│ │ 处理: 计算从相机位置到锁敌点的方向 │ │
│ │ 输出: 修改后的旋转 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ 叠加 │
│ Main Layer: CombatRig (战斗相机) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ OrbitalFollow: 相机绕玩家旋转 │ │
│ │ ThirdPersonAim: 基础瞄准 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ 叠加 │
│ Base Layer: AttachRig │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ AttachToPlayer: 相机跟随玩家移动 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.3 锁敌节点实现
// ========================================
// LockOnAimCameraNode.h
// 锁敌瞄准节点
// ========================================
#pragma once
#include "Core/CameraNode.h"
#include "LockOnAimCameraNode.generated.h"
/**
* 锁敌瞄准节点
* 让相机自动转向锁定的目标
* 放在 Global Layer 使用
*/
UCLASS(MinimalAPI, meta=(CameraNodeCategories="Aim,Combat"))
class ULockOnAimCameraNode : public UCameraNode
{
GENERATED_BODY()
public:
ULockOnAimCameraNode(const FObjectInitializer& ObjInit);
protected:
virtual FCameraNodeEvaluatorPtr OnBuildEvaluator(FCameraNodeEvaluatorBuilder& Builder) const override;
public:
// === 锁敌设置 ===
// 阻尼强度:值越大跟随越快
UPROPERTY(EditAnywhere, Category="Lock On")
FFloatCameraParameter DampingStrength = 10.0f;
// 目标偏移:相对于锁定部位的偏移
UPROPERTY(EditAnywhere, Category="Lock On")
FVector3dCameraParameter TargetOffset = FVector3d(0, 0, 50); // 默认瞄准头部上方
// 最大旋转速度(度/秒)
UPROPERTY(EditAnywhere, Category="Lock On")
FFloatCameraParameter MaxRotationSpeed = 180.0f;
// 是否保持玩家控制
// 如果为 true,玩家输入可以覆盖部分锁敌旋转
UPROPERTY(EditAnywhere, Category="Lock On")
bool bAllowPlayerControl = true;
// 玩家控制权重 (0-1)
// 0 = 完全锁敌,1 = 完全玩家控制
UPROPERTY(EditAnywhere, Category="Lock On")
FFloatCameraParameter PlayerControlWeight = 0.3f;
// === 距离限制 ===
// 最大锁敌距离
UPROPERTY(EditAnywhere, Category="Limits")
FFloatCameraParameter MaxLockDistance = 5000.0f;
// 最小锁敌距离(太近时解除锁定)
UPROPERTY(EditAnywhere, Category="Limits")
FFloatCameraParameter MinLockDistance = 100.0f;
// === 视角限制 ===
// 最大仰角
UPROPERTY(EditAnywhere, Category="Limits")
FFloatCameraParameter MaxPitchAngle = 80.0f;
// 最小俯角
UPROPERTY(EditAnywhere, Category="Limits")
FFloatCameraParameter MinPitchAngle = -45.0f;
};
// ========================================
// LockOnAimCameraNodeEvaluator.h
// 锁敌瞄准节点评估器
// ========================================
#pragma once
#include "Core/CameraNodeEvaluator.h"
class FLockOnAimCameraNodeEvaluator : public FCameraNodeEvaluator
{
UE_DECLARE_CAMERA_NODE_EVALUATOR(FLockOnAimCameraNodeEvaluator)
public:
// 设置锁敌目标
void SetLockOnTarget(AActor* Target, FName BoneName = NAME_None);
// 清除锁敌目标
void ClearLockOnTarget();
// 是否正在锁敌
bool IsLockOnActive() const { return LockOnTarget.IsValid(); }
protected:
virtual void OnRun(const FCameraNodeEvaluationParams& Params,
FCameraNodeEvaluationResult& OutResult) override;
private:
// 锁敌目标
TWeakObjectPtr<AActor> LockOnTarget;
FName LockOnBoneName;
// 当前旋转(用于平滑过渡)
FRotator3d CurrentAimRotation;
// 上一帧的目标位置(用于预测)
FVector3d PreviousTargetLocation;
FVector3d TargetVelocity;
// 获取锁敌点的世界位置
FVector3d GetLockOnLocation() const;
// 计算期望的相机旋转
FRotator3d CalculateDesiredRotation(
const FVector3d& CameraLocation,
const FVector3d& TargetLocation,
const FVector3d& UpVector) const;
};
// ========================================
// LockOnAimCameraNodeEvaluator.cpp
// ========================================
#include "LockOnAimCameraNodeEvaluator.h"
#include "LockOnAimCameraNode.h"
#include "GameFramework/Character.h"
#include "Components/SkeletalMeshComponent.h"
UE_DEFINE_CAMERA_NODE_EVALUATOR(FLockOnAimCameraNodeEvaluator)
void FLockOnAimCameraNodeEvaluator::SetLockOnTarget(AActor* Target, FName BoneName)
{
LockOnTarget = Target;
LockOnBoneName = BoneName;
PreviousTargetLocation = GetLockOnLocation();
TargetVelocity = FVector3d::ZeroVector;
}
void FLockOnAimCameraNodeEvaluator::ClearLockOnTarget()
{
LockOnTarget.Reset();
LockOnBoneName = NAME_None;
TargetVelocity = FVector3d::ZeroVector;
}
FVector3d FLockOnAimCameraNodeEvaluator::GetLockOnLocation() const
{
if (!LockOnTarget.IsValid())
{
return FVector3d::ZeroVector;
}
AActor* Target = LockOnTarget.Get();
FVector3d BaseLocation = Target->GetActorLocation();
// 如果指定了骨骼,获取骨骼位置
if (LockOnBoneName != NAME_None)
{
if (ACharacter* Character = Cast<ACharacter>(Target))
{
if (USkeletalMeshComponent* Mesh = Character->GetMesh())
{
BaseLocation = Mesh->GetSocketLocation(LockOnBoneName);
}
}
}
return BaseLocation;
}
FRotator3d FLockOnAimCameraNodeEvaluator::CalculateDesiredRotation(
const FVector3d& CameraLocation,
const FVector3d& TargetLocation,
const FVector3d& UpVector) const
{
// 计算从相机到目标的方向
FVector3d Direction = TargetLocation - CameraLocation;
double Distance = Direction.Length();
if (Distance < KINDA_SMALL_NUMBER)
{
return CurrentAimRotation;
}
Direction.Normalize();
// 转换为旋转
FRotator3d DesiredRotation = Direction.Rotation();
return DesiredRotation;
}
void FLockOnAimCameraNodeEvaluator::OnRun(
const FCameraNodeEvaluationParams& Params,
FCameraNodeEvaluationResult& OutResult)
{
const ULockOnAimCameraNode* NodeData = GetCameraNodeAs<ULockOnAimCameraNode>();
// 检查是否有有效的锁敌目标
if (!LockOnTarget.IsValid())
{
// 没有锁敌目标,不修改相机
return;
}
// 获取参数值
const float Damping = NodeData->DampingStrength.GetValue(OutResult.VariableTable);
const float MaxSpeed = NodeData->MaxRotationSpeed.GetValue(OutResult.VariableTable);
const float MaxDistance = NodeData->MaxLockDistance.GetValue(OutResult.VariableTable);
const float MinDistance = NodeData->MinLockDistance.GetValue(OutResult.VariableTable);
const FVector3d Offset = NodeData->TargetOffset.GetValue(OutResult.VariableTable);
const float MaxPitch = NodeData->MaxPitchAngle.GetValue(OutResult.VariableTable);
const float MinPitch = NodeData->MinPitchAngle.GetValue(OutResult.VariableTable);
// 获取相机当前位置
const FVector3d CameraLocation = OutResult.CameraPose.GetLocation();
const FVector3d UpVector = OutResult.CameraPose.GetReferenceUp();
// 获取锁敌目标位置
FVector3d TargetLocation = GetLockOnLocation() + Offset;
// 计算距离
double Distance = FVector3d::Distance(CameraLocation, TargetLocation);
// 距离检查
if (Distance > MaxDistance || Distance < MinDistance)
{
// 超出范围,解除锁定
ClearLockOnTarget();
return;
}
// 预测目标移动
if (Params.DeltaTime > 0)
{
FVector3d CurrentVelocity = (TargetLocation - PreviousTargetLocation) / Params.DeltaTime;
// 平滑速度估计
TargetVelocity = FMath::VInterpTo(TargetVelocity, CurrentVelocity, Params.DeltaTime, 5.0f);
// 预测未来位置
TargetLocation += TargetVelocity * Params.DeltaTime * 0.5f;
PreviousTargetLocation = TargetLocation;
}
// 计算期望旋转
FRotator3d DesiredRotation = CalculateDesiredRotation(CameraLocation, TargetLocation, UpVector);
// 限制俯仰角
DesiredRotation.Pitch = FMath::Clamp(DesiredRotation.Pitch, MinPitch, MaxPitch);
// 平滑过渡到目标旋转
if (Params.DeltaTime > 0)
{
// 使用阻尼插值
CurrentAimRotation = FMath::RInterpTo(
CurrentAimRotation,
DesiredRotation,
Params.DeltaTime,
Damping);
// 限制最大旋转速度
FRotator3d DeltaRotation = CurrentAimRotation - OutResult.CameraPose.GetRotation();
DeltaRotation.Normalize();
float MaxDeltaDegrees = MaxSpeed * Params.DeltaTime;
DeltaRotation.Pitch = FMath::Clamp(DeltaRotation.Pitch, -MaxDeltaDegrees, MaxDeltaDegrees);
DeltaRotation.Yaw = FMath::Clamp(DeltaRotation.Yaw, -MaxDeltaDegrees, MaxDeltaDegrees);
DeltaRotation.Roll = 0;
CurrentAimRotation = OutResult.CameraPose.GetRotation() + DeltaRotation;
}
else
{
CurrentAimRotation = DesiredRotation;
}
// 应用旋转
OutResult.CameraPose.SetRotation(CurrentAimRotation);
// 更新参考 LookAt 点
OutResult.CameraPose.SetReferenceLookAt(TargetLocation);
}
4.4 锁敌 Rig 配置
// ========================================
// LockOnRig 资产配置
// ========================================
/*
* Camera Rig: LockOnRig
* Layer: Global
*
* RootNode: Sequence
* [0] LockOnAimNode
*/
// 在游戏代码中激活锁敌
void AMonsterHunterPlayerController::ActivateLockOn(AActor* Target, FName BoneName)
{
if (!Target) return;
// 获取锁敌 Rig 的评估器
if (UGameplayCameraComponent* CameraComponent = GetGameplayCameraComponent())
{
// 设置锁敌目标
// 这通常通过 VariableTable 或直接访问评估器实现
SetLockOnTarget(Target, BoneName);
}
// 激活锁敌 Rig
FActivateCameraRigParams Params;
Params.CameraRig = LockOnRig;
Params.Layer = ECameraRigLayer::Global;
LockOnRigInstanceID = UActivateCameraRigFunctions::ActivateCameraRig(Params);
bLockOnActive = true;
}
void AMonsterHunterPlayerController::DeactivateLockOn()
{
if (!bLockOnActive) return;
// 清除锁敌目标
ClearLockOnTarget();
// 停用锁敌 Rig
FDeactivateCameraRigParams Params;
Params.CameraRig = LockOnRig;
Params.Layer = ECameraRigLayer::Global;
UActivateCameraRigFunctions::DeactivateCameraRig(Params);
bLockOnActive = false;
}
// 切换锁敌部位
void AMonsterHunterPlayerController::SwitchLockOnBone(FName NewBoneName)
{
if (!bLockOnActive || !LockOnTarget.IsValid()) return;
SetLockOnTarget(LockOnTarget.Get(), NewBoneName);
}
4.5 锁敌与武器切换同时发生
┌─────────────────────────────────────────────────────────────────────────────┐
│ 场景:武器切换混合进行中时,玩家按下锁敌键 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ T=0s T=0.3s T=0.5s T=0.8s T=1.3s │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌───┐ ┌─────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐ │
│ │大剑│──▶│大剑→双刀│───▶│大剑→双刀 │──▶│双刀 │──▶│双刀 │ │
│ │ │ │ (30%) │ │+ 锁敌激活 │ │+ 锁敌 │ │+ 锁敌 │ │
│ └───┘ └─────────┘ └─────────────┘ └─────────────┘ └─────────┘ │
│ 混合继续 混合完成 │
│ │
│ 层级状态: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Visual Layer: (空) │ │
│ │ Global Layer: [LockOnRig] ← T=0.5s 激活 │ │
│ │ Main Layer: [大剑Rig ←Blend(30%→80%)→ 双刀Rig] │ │
│ │ Base Layer: [AttachRig] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 关键点: │
│ • Main Layer 的混合不受 Global Layer 影响 │
│ • 锁敌效果叠加在混合结果之上 │
│ • 最终结果 = Lerp(大剑, 双刀, weight) 然后应用锁敌旋转 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5. 场景三:连续相机切换请求
5.1 场景描述
怪物猎人经典场景:骑乘怪物
T=0s T=0.5s T=1.0s T=1.2s T=1.5s T=2.0s
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌───┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│战斗│───▶│战斗→骑乘│──▶│骑乘→特殊│──▶│特殊→骑乘│──▶│骑乘→终结│──▶│骑乘 │
│相机│ │混合中 │ │攻击 │ │攻击 │ │技 │ │相机 │
└───┘ └───────┘ └───────┘ └───────┘ └───────┘ └───────┘
50%进度 混合被打断 新混合 又被打断
新请求到来 开始 终结技请求
5.2 GameplayCamera 的处理方式:快照机制
┌─────────────────────────────────────────────────────────────────────────────┐
│ 连续混合的快照处理机制 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ === T=0s: 初始状态 === │
│ Main Layer Stack: [CombatRig] │
│ Result = CombatRig.State │
│ │
│ === T=0s: 请求切换到骑乘 === │
│ Main Layer Stack: [CombatRig ←Blend(0%)→ MountRig] │
│ BlendTime = 1.0s │
│ │
│ === T=0.5s: 混合进行中 (50%) === │
│ Main Layer Stack: [CombatRig ←Blend(50%)→ MountRig] │
│ CurrentState = Lerp(Combat.State, Mount.State, 0.5) │
│ │
│ === T=0.5s: 新请求!切换到特殊攻击 === │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 关键操作:创建当前状态的快照 │ │
│ │ │ │
│ │ SnapshotState = Lerp(Combat.State, Mount.State, 0.5) │ │
│ │ SnapshotRig = CreateSnapshotRig(SnapshotState) │ │
│ │ │ │
│ │ 新的混合栈: │ │
│ │ Main Layer Stack: [SnapshotRig ←Blend(0%)→ SpecialAttackRig] │ │
│ │ │ │
│ │ CombatRig 和 MountRig 被移除,它们的混合状态被"压缩"到快照中 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ === T=0.7s: 新混合进行中 (20%) === │
│ Main Layer Stack: [SnapshotRig ←Blend(20%)→ SpecialAttackRig] │
│ CurrentState = Lerp(SnapshotState, SpecialAttack.State, 0.2) │
│ │
│ 注意:SnapshotState 是静态的,不会继续从 Combat 向 Mount 混合 │
│ │
│ === T=0.8s: 又一个新请求!切换回骑乘 === │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 再次创建快照: │ │
│ │ │ │
│ │ NewSnapshotState = Lerp(SnapshotState, SpecialAttack.State, 0.4) │ │
│ │ NewSnapshotRig = CreateSnapshotRig(NewSnapshotState) │ │
│ │ │ │
│ │ 新的混合栈: │ │
│ │ Main Layer Stack: [NewSnapshotRig ←Blend(0%)→ MountRig] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.3 快照的数学原理
┌─────────────────────────────────────────────────────────────────────────────┐
│ 快照混合的数学表示 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 假设: │
│ • CombatRig 的状态为 C │
│ • MountRig 的状态为 M │
│ • SpecialAttackRig 的状态为 S │
│ │
│ T=0.5s 时请求 SpecialAttack: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Snapshot1 = Lerp(C, M, 0.5) = 0.5*C + 0.5*M │ │
│ │ │ │
│ │ T=0.7s 时的状态: │ │
│ │ State = Lerp(Snapshot1, S, 0.2) │ │
│ │ = 0.8 * Snapshot1 + 0.2 * S │ │
│ │ = 0.8 * (0.5*C + 0.5*M) + 0.2 * S │ │
│ │ = 0.4*C + 0.4*M + 0.2*S │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ T=0.8s 时再次请求 MountRig: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Snapshot2 = Lerp(Snapshot1, S, 0.4) │ │
│ │ = 0.6 * Snapshot1 + 0.4 * S │ │
│ │ = 0.6 * (0.5*C + 0.5*M) + 0.4 * S │ │
│ │ = 0.3*C + 0.3*M + 0.4*S │ │
│ │ │ │
│ │ 最终混合完成后: │ │
│ │ State = MountRig.State (M) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 可视化权重变化: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 权重 │ │
│ │ 1.0 ┤ M ────────────────────────●━━━━━━━━━━━━━━ M │ │
│ │ │ ╱ ╱ │ │
│ │ 0.5 ┤ ╱ Snapshot1 ●━━━━━━━━━━━╱ │ │
│ │ │ ╱ ╲ ╱ │ │
│ │ │ ╱ ╲ ╱ │ │
│ │ 0.0 ┤●───────────────────╲━━━━━╱──────────────────────────── S │ │
│ │ │ C ╲ ╱ │ │
│ │ │ ● │ │
│ │ └────┬────┬────┬────┬────┬────┬────┬────→ Time │ │
│ │ 0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.4 内部实现代码
// ========================================
// FTransientBlendStackCameraNodeEvaluator
// 处理连续混合请求的核心逻辑
// ========================================
void FTransientBlendStackCameraNodeEvaluator::PushCamera(
FCameraNodeEvaluator* NewCameraEvaluator,
float BlendTime,
ECameraBlendType BlendType)
{
// 检查当前是否正在混合
if (BlendStack.Num() > 1)
{
// 当前有混合正在进行
// 需要创建快照
CreateBlendSnapshot();
}
// 创建新的混合条目
FBlendEntry NewEntry;
NewEntry.CameraEvaluator = NewCameraEvaluator;
NewEntry.BlendWeight = 0.0f;
NewEntry.TargetWeight = 1.0f;
NewEntry.TotalBlendTime = BlendTime;
NewEntry.BlendTimeRemaining = BlendTime;
NewEntry.BlendType = BlendType;
// 清空栈,只保留快照和新相机
BlendStack.Empty();
BlendStack.Add(NewEntry);
}
void FTransientBlendStackCameraNodeEvaluator::CreateBlendSnapshot()
{
// 1. 计算当前的混合状态
FCameraNodeEvaluationParams Params;
FCameraNodeEvaluationResult SnapshotResult;
// 评估当前所有活动相机的混合结果
for (const FBlendEntry& Entry : BlendStack)
{
FCameraNodeEvaluationResult EntryResult;
Entry.CameraEvaluator->Run(Params, EntryResult);
// 根据权重混合
SnapshotResult.CameraPose = FCameraPose::Lerp(
SnapshotResult.CameraPose,
EntryResult.CameraPose,
Entry.BlendWeight);
}
// 2. 创建快照 Rig
// 快照 Rig 是一个特殊的 Rig,它的状态固定为当前混合结果
TSharedPtr<FSnapshotCameraNodeEvaluator> SnapshotEvaluator =
MakeShared<FSnapshotCameraNodeEvaluator>(SnapshotResult.CameraPose);
// 3. 替换混合栈
BlendStack.Empty();
FBlendEntry SnapshotEntry;
SnapshotEntry.CameraEvaluator = SnapshotEvaluator;
SnapshotEntry.BlendWeight = 1.0f;
SnapshotEntry.TargetWeight = 0.0f; // 快照的权重会递减
BlendStack.Add(SnapshotEntry);
}
// ========================================
// FSnapshotCameraNodeEvaluator
// 快照评估器:保持固定的相机状态
// ========================================
class FSnapshotCameraNodeEvaluator : public FCameraNodeEvaluator
{
public:
FSnapshotCameraNodeEvaluator(const FCameraPose& InSnapshotPose)
: SnapshotPose(InSnapshotPose)
{}
protected:
virtual void OnRun(
const FCameraNodeEvaluationParams& Params,
FCameraNodeEvaluationResult& OutResult) override
{
// 直接返回快照的状态
OutResult.CameraPose = SnapshotPose;
}
private:
FCameraPose SnapshotPose;
};
5.5 与 Cinemachine 的对比
┌─────────────────────────────────────────────────────────────────────────────┐
│ 连续混合:GPC vs Cinemachine │
├─────────────────────────────┬───────────────────────────────────────────────┤
│ GameplayCamera │ Cinemachine │
├─────────────────────────────┼───────────────────────────────────────────────┤
│ │ │
│ 【处理方式】 │ 【处理方式】 │
│ 创建静态快照 │ 创建 NestedBlendSource │
│ 快照状态固定不变 │ 内部混合继续更新 │
│ │ │
│ 【视觉效果】 │ 【视觉效果】 │
│ 原混合被"打断" │ 原混合继续进行 │
│ 从当前状态开始新过渡 │ 平滑接力到新相机 │
│ │ │
│ 【优点】 │ 【优点】 │
│ 实现简单 │ 过渡更自然流畅 │
│ 性能好 │ 视觉效果更好 │
│ 行为可预测 │ 符合电影剪辑直觉 │
│ │ │
│ 【缺点】 │ 【缺点】 │
│ 过渡可能不自然 │ 实现复杂 │
│ 可能有轻微跳跃感 │ 嵌套层次深时有性能开销 │
│ │ │
└─────────────────────────────┴───────────────────────────────────────────────┘
视觉对比:
GameplayCamera (Snapshot):
================================
T=0.5s 请求新相机时:
快照定格在此刻:Lerp(C, M, 0.5)
新混合从快照开始
T=0.6s:
Result = Lerp(快照, 新相机, 0.1)
= Lerp(Lerp(C, M, 0.5), 新相机, 0.1)
↑ 永远是 0.5
视觉上:原混合被打断,开始新的过渡
Cinemachine (NestedBlendSource):
================================
T=0.5s 请求新相机时:
包装当前混合为 NestedBlendSource
内部混合继续更新
T=0.6s:
NestedBlend.State = Lerp(C, M, 0.6) // 继续更新!
Result = Lerp(NestedBlend.State, 新相机, 0.1)
= Lerp(Lerp(C, M, 0.6), 新相机, 0.1)
↑ 变成 0.6 了
视觉上:原混合继续完成,同时向新相机过渡
6. 场景四:骑乘怪物时的多相机协同
6.1 场景描述
┌─────────────────────────────────────────────────────────────────────────────┐
│ 骑乘怪物的相机系统 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 怪物 │
│ ┌─────────┐ │
│ │ 头部 │ ← 可攻击部位 │
│ │ ● │ │
│ │ │ │ │
│ 玩家位置 ───┼────●────┼─── 相机 │
│ │ │ │ ● │
│ │ 身体 │ ╱│ │
│ │ ●────┼───╱ │ 看向头部 │
│ │ │ ╱ │
│ │ 尾巴 │ ╱ │
│ └────●────┘ │
│ │
│ 相机需求: │
│ 1. 相机附着在怪物背上(跟随怪物移动) │
│ 2. 相机可以旋转观察周围 │
│ 3. 攻击时镜头会特写攻击部位 │
│ 4. 怪物挣扎时相机震动 │
│ 5. 终结技时有特殊镜头效果 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
6.2 多层架构设计
┌─────────────────────────────────────────────────────────────────────────────┐
│ 骑乘相机的层级结构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Visual Layer │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ MountAttackFOVNode: 攻击时 FOV 变化 │ │
│ │ MountAttackShakeNode: 攻击命中时的微震动 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ Global Layer │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ MountStruggleShakeNode: 怪物挣扎时的震动 │ │
│ │ MountLockOnNode: 攻击部位的锁定瞄准 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ Main Layer │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ MountOrbitalCamera: 玩家可旋转的骑乘相机 │ │
│ │ MountAttackCamera: 攻击时的特写镜头 │ │
│ │ MountFinisherCamera: 终结技的特殊镜头 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ Base Layer │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ AttachToMonsterNode: 相机跟随怪物移动 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
6.3 Base Layer:附着到怪物
// ========================================
// AttachToMonsterCameraNode.h
// 相机附着到怪物背上
// ========================================
UCLASS(MinimalAPI, meta=(CameraNodeCategories="Transform"))
class UAttachToMonsterCameraNode : public UCameraNode
{
GENERATED_BODY()
public:
// 附着偏移(相对于怪物骨骼)
UPROPERTY(EditAnywhere, Category="Attachment")
FVector3dCameraParameter AttachmentOffset = FVector3d(0, 0, 100);
// 附着骨骼名称
UPROPERTY(EditAnywhere, Category="Attachment")
FName AttachBoneName = NAME_None;
// 是否跟随怪物旋转
UPROPERTY(EditAnywhere, Category="Attachment")
bool bFollowRotation = false;
// 位置阻尼
UPROPERTY(EditAnywhere, Category="Damping")
FVector3dCameraParameter PositionDamping = FVector3d(5, 5, 5);
};
// AttachToMonsterCameraNodeEvaluator.cpp
void FAttachToMonsterCameraNodeEvaluator::OnRun(
const FCameraNodeEvaluationParams& Params,
FCameraNodeEvaluationResult& OutResult)
{
if (!AttachedMonster.IsValid())
{
return;
}
ACharacter* Monster = AttachedMonster.Get();
const UAttachToMonsterCameraNode* NodeData = GetCameraNodeAs<UAttachToMonsterCameraNode>();
// 获取附着点位置
FVector3d TargetLocation;
if (NodeData->AttachBoneName != NAME_None)
{
// 使用骨骼位置
if (USkeletalMeshComponent* Mesh = Monster->GetMesh())
{
TargetLocation = Mesh->GetSocketLocation(NodeData->AttachBoneName);
}
}
else
{
TargetLocation = Monster->GetActorLocation();
}
// 应用偏移
FVector3d Offset = NodeData->AttachmentOffset.GetValue(OutResult.VariableTable);
if (NodeData->bFollowRotation)
{
Offset = Monster->GetActorRotation().RotateVector(Offset);
}
TargetLocation += Offset;
// 应用阻尼
FVector3d Damping = NodeData->PositionDamping.GetValue(OutResult.VariableTable);
CurrentLocation = FMath::VInterpTo(CurrentLocation, TargetLocation, Params.DeltaTime, Damping.X);
// 设置相机位置
OutResult.CameraPose.SetLocation(CurrentLocation);
// 如果跟随旋转
if (NodeData->bFollowRotation)
{
OutResult.CameraPose.SetRotation(Monster->GetActorRotation());
}
}
6.4 Main Layer:骑乘相机与攻击相机切换
// ========================================
// 骑乘相机管理器
// ========================================
UCLASS()
class UMountCameraManager : public UObject
{
GENERATED_BODY()
public:
// Camera Rig 资产
UPROPERTY(EditDefaultsOnly, Category="Rigs")
UCameraRigAsset* MountBaseRig; // 基础骑乘相机
UPROPERTY(EditDefaultsOnly, Category="Rigs")
UCameraRigAsset* MountAttackRig; // 攻击特写相机
UPROPERTY(EditDefaultsOnly, Category="Rigs")
UCameraRigAsset* MountFinisherRig; // 终结技相机
// 过渡设置
UPROPERTY(EditDefaultsOnly, Category="Transitions")
UCameraRigTransition* ToAttackTransition;
UPROPERTY(EditDefaultsOnly, Category="Transitions")
UCameraRigTransition* FromAttackTransition;
UPROPERTY(EditDefaultsOnly, Category="Transitions")
UCameraRigTransition* ToFinisherTransition;
// 切换到攻击相机
void EnterAttackMode(FName TargetBone)
{
// 设置攻击目标
CurrentAttackTarget = TargetBone;
// 激活攻击 Rig
FActivateCameraRigParams Params;
Params.CameraRig = MountAttackRig;
Params.Layer = ECameraRigLayer::Main;
Params.TransitionOverride = ToAttackTransition;
AttackRigInstanceID = UActivateCameraRigFunctions::ActivateCameraRig(Params);
bInAttackMode = true;
}
// 退出攻击模式
void ExitAttackMode()
{
if (!bInAttackMode) return;
// 激活基础骑乘 Rig(会自动混合出攻击 Rig)
FActivateCameraRigParams Params;
Params.CameraRig = MountBaseRig;
Params.Layer = ECameraRigLayer::Main;
Params.TransitionOverride = FromAttackTransition;
UActivateCameraRigFunctions::ActivateCameraRig(Params);
bInAttackMode = false;
}
// 执行终结技
void ExecuteFinisher()
{
// 激活终结技 Rig
FActivateCameraRigParams Params;
Params.CameraRig = MountFinisherRig;
Params.Layer = ECameraRigLayer::Main;
Params.TransitionOverride = ToFinisherTransition;
FinisherRigInstanceID = UActivateCameraRigFunctions::ActivateCameraRig(Params);
bInFinisher = true;
}
private:
FCameraRigInstanceID AttackRigInstanceID;
FCameraRigInstanceID FinisherRigInstanceID;
bool bInAttackMode = false;
bool bInFinisher = false;
FName CurrentAttackTarget;
};
6.5 完整的骑乘相机流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ 骑乘相机完整流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 时间线: │
│ │
│ T=0s T=1s T=2s T=3s T=4s │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌─────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │进入 │─────▶│骑乘中 │──▶│攻击特写 │──▶│终结技 │──▶│退出 │ │
│ │骑乘 │ │玩家旋转 │ │锁定部位 │ │特殊镜头 │ │骑乘 │ │
│ └─────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ Layer 状态: │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Visual Layer: │ │
│ │ T=2s: FOV 变化 + 命中震动 │ │
│ │ T=3s: 终结技 FOV 特效 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Global Layer: │ │
│ │ T=1s~: 怪物挣扎震动 │ │
│ │ T=2s: 锁定攻击部位 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Main Layer: │ │
│ │ T=0s: MountBaseRig 激活 │ │
│ │ T=2s: MountBaseRig → MountAttackRig 混合 │ │
│ │ T=3s: MountAttackRig → MountFinisherRig 混合 │ │
│ │ T=4s: MountFinisherRig → CombatRig 混合 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Base Layer: │ │
│ │ T=0s~4s: 始终附着在怪物背上 │ │
│ │ T=4s: 切换回附着到玩家 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
7. 场景五:受击震动与视觉冲击
7.1 场景描述
怪物猎人受击系统:
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ 轻击:相机轻微抖动 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 震动强度: 0.3 │ │
│ │ 持续时间: 0.2s │ │
│ │ 衰减: 快速 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 重击:相机明显抖动 + FOV 微变 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 震动强度: 0.7 │ │
│ │ 持续时间: 0.5s │ │
│ │ FOV 变化: -5° │ │
│ │ 衰减: 中等 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 大招/龙吼:强烈震动 + 时间减速 + FOV 大变 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 震动强度: 1.0 │ │
│ │ 持续时间: 1.0s │ │
│ │ FOV 变化: -15° │ │
│ │ 时间减速: 0.3x │ │
│ │ 径向模糊: 启用 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
7.2 震动服务
GameplayCamera 使用 Evaluation Service 机制实现震动:
// ========================================
// CameraShakeService.h
// 相机震动服务
// ========================================
UCLASS(MinimalAPI)
class UCameraShakeService : public UCameraEvaluationService
{
GENERATED_BODY()
public:
// 添加震动
FCameraShakeHandle AddShake(
const FCameraShakeParams& Params);
// 移除震动
void RemoveShake(FCameraShakeHandle Handle);
// 移除所有震动
void RemoveAllShakes();
protected:
// 服务执行点
virtual ECameraServiceExecutionPoint GetExecutionPoint() const override
{
return ECameraServiceExecutionPoint::PostEvaluation;
}
// 服务执行
virtual void ExecuteService(
const FCameraServiceParams& Params,
FCameraServiceResult& OutResult) override;
private:
// 活动的震动列表
TArray<TSharedPtr<FCameraShakeInstance>> ActiveShakes;
};
// 震动参数
USTRUCT(BlueprintType)
struct FCameraShakeParams
{
GENERATED_BODY()
// 震动模式
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector3f PositionAmplitude = FVector3f(1, 1, 1);
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector3f PositionFrequency = FVector3f(10, 10, 10);
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FRotator3f RotationAmplitude = FRotator3f(1, 1, 1);
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FRotator3f RotationFrequency = FRotator3f(10, 10, 10);
// 时间包络
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float AttackTime = 0.1f;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float SustainTime = 0.2f;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float DecayTime = 0.3f;
// 整体强度缩放
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Scale = 1.0f;
// 震动通道
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Channel = 1;
};
// CameraShakeService.cpp
void UCameraShakeService::ExecuteService(
const FCameraServiceParams& Params,
FCameraServiceResult& OutResult)
{
FVector3d TotalPositionOffset = FVector3d::ZeroVector;
FRotator3d TotalRotationOffset = FRotator3d::ZeroRotator;
// 遍历所有活动震动
for (int32 i = ActiveShakes.Num() - 1; i >= 0; --i)
{
TSharedPtr<FCameraShakeInstance> Shake = ActiveShakes[i];
// 检查是否过期
if (Shake->IsExpired())
{
ActiveShakes.RemoveAt(i);
continue;
}
// 更新震动
Shake->Update(Params.DeltaTime);
// 累加偏移
TotalPositionOffset += Shake->GetPositionOffset();
TotalRotationOffset += Shake->GetRotationOffset();
}
// 应用到结果
OutResult.CameraPose.SetLocation(
OutResult.CameraPose.GetLocation() + TotalPositionOffset);
OutResult.CameraPose.SetRotation(
OutResult.CameraPose.GetRotation() + TotalRotationOffset);
}
7.3 震动的层级位置
┌─────────────────────────────────────────────────────────────────────────────┐
│ 震动在层级中的位置 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 震动作为 Service 存在,在评估完成后执行: │
│ │
│ 评估流程: │
│ 1. Base Layer 评估 │
│ 2. Main Layer 评估 │
│ 3. Global Layer 评估 │
│ 4. Visual Layer 评估 │
│ 5. PostEvaluation Services 执行 ← 震动在这里! │
│ • CameraShakeService │
│ • CameraConfinerService │
│ │
│ 这样设计的好处: │
│ • 震动不影响任何层的计算 │
│ • 震动在所有层计算完成后叠加 │
│ • 震动不会参与混合计算 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
7.4 使用示例
// ========================================
// 怪物猎人受击处理
// ========================================
void AMonsterHunterCharacter::OnTakeDamage(
float Damage,
AActor* DamageCauser,
const FHitResult& HitInfo)
{
// 根据伤害计算震动强度
float ShakeIntensity = CalculateShakeIntensity(Damage);
// 创建震动参数
FCameraShakeParams ShakeParams;
ShakeParams.Scale = ShakeIntensity;
if (Damage < 20)
{
// 轻击
ShakeParams.PositionAmplitude = FVector3f(2, 2, 1);
ShakeParams.RotationAmplitude = FRotator3f(0.5f, 0.5f, 0);
ShakeParams.AttackTime = 0.05f;
ShakeParams.DecayTime = 0.15f;
}
else if (Damage < 50)
{
// 重击
ShakeParams.PositionAmplitude = FVector3f(5, 5, 3);
ShakeParams.RotationAmplitude = FRotator3f(2, 2, 0.5f);
ShakeParams.AttackTime = 0.1f;
ShakeParams.DecayTime = 0.4f;
// FOV 效果
ApplyFOVEffect(-5, 0.3f);
}
else
{
// 大招
ShakeParams.PositionAmplitude = FVector3f(10, 10, 5);
ShakeParams.RotationAmplitude = FRotator3f(5, 5, 1);
ShakeParams.AttackTime = 0.15f;
ShakeParams.DecayTime = 0.8f;
// FOV 效果
ApplyFOVEffect(-15, 0.8f);
// 时间减速
ApplyTimeDilation(0.3f, 1.0f);
}
// 添加震动
if (UGameplayCameraComponent* CameraComp = GetGameplayCameraComponent())
{
CameraComp->AddCameraShake(ShakeParams);
}
}
float AMonsterHunterCharacter::CalculateShakeIntensity(float Damage)
{
// 非线性映射
// Damage: 0-100 -> Intensity: 0-1
return FMath::Clamp(FMath::Pow(Damage / 100.0f, 0.5f), 0.0f, 1.0f);
}
8. 完整实现:怪物猎人风格相机系统
8.1 系统架构
┌─────────────────────────────────────────────────────────────────────────────┐
│ 怪物猎人风格相机系统架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ AMonsterHunterCameraManager │ │
│ │ │ │
│ │ 职责: │ │
│ │ • 管理所有 Camera Rig 的激活/停用 │ │
│ │ • 处理相机模式切换 │ │
│ │ • 协调锁敌系统 │ │
│ │ • 触发震动和视觉效果 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ UGameplayCameraComponent │ │
│ │ │ │
│ │ 内部结构: │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ FCameraSystemEvaluator │ │ │
│ │ │ │ │ │
│ │ │ Base Layer Main Layer Global Layer Visual Layer │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ ▼ ▼ ▼ ▼ │ │ │
│ │ │ AttachRig [混合栈] [效果栈] [视觉栈] │ │ │
│ │ │ • ExploreRig • LockOnRig • FOVRig │ │ │
│ │ │ • CombatRig • ShakeRig • VignetteRig│ │ │
│ │ │ • MountRig • ImpactRig │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
8.2 完整代码
// ========================================
// MonsterHunterCameraManager.h
// ========================================
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MonsterHunterCameraManager.generated.h"
// 相机模式
UENUM(BlueprintType)
enum class EMHCameraMode : uint8
{
Explore, // 探索模式
Combat, // 战斗模式
Mount, // 骑乘模式
Cinematic, // 过场动画
Dead // 死亡
};
// 武器类型(影响相机距离和 FOV)
UENUM(BlueprintType)
enum class EMHWeaponType : uint8
{
GreatSword, // 大剑
DualBlades, // 双刀
LongSword, // 太刀
Hammer, // 大锤
Bow, // 弓箭
HeavyBowgun, // 重弩
InsectGlaive, // 操虫棍
ChargeBlade, // 充能斧
SwitchAxe, // 斩击斧
Lance, // 长枪
Gunlance, // 铳枪
HuntingHorn, // 狩猎笛
LightBowgun, // 轻弩
SwordAndShield // 单手剑
};
// 相机 Rig 资产配置
USTRUCT(BlueprintType)
struct FMHCameraRigConfig
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigAsset* ExploreRig;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigAsset* CombatBaseRig;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TMap<EMHWeaponType, UCameraRigAsset*> WeaponSpecificRigs;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigAsset* MountBaseRig;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigAsset* MountAttackRig;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigAsset* MountFinisherRig;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigAsset* LockOnRig;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigAsset* ShakeRig;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigAsset* FOVEffectRig;
};
// 过渡配置
USTRUCT(BlueprintType)
struct FMHTransitionConfig
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigTransition* ExploreToCombat;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigTransition* CombatToExplore;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigTransition* ToMount;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigTransition* FromMount;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigTransition* ToLockOn;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigTransition* FromLockOn;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UCameraRigTransition* WeaponSwitch;
};
UCLASS()
class AMonsterHunterCameraManager : public AActor
{
GENERATED_BODY()
public:
AMonsterHunterCameraManager();
// ========== 配置 ==========
UPROPERTY(EditDefaultsOnly, Category="Config")
FMHCameraRigConfig RigConfig;
UPROPERTY(EditDefaultsOnly, Category="Config")
FMHTransitionConfig TransitionConfig;
// ========== 相机模式 ==========
// 设置相机模式
UFUNCTION(BlueprintCallable, Category="Camera")
void SetCameraMode(EMHCameraMode NewMode);
// 获取当前相机模式
UFUNCTION(BlueprintPure, Category="Camera")
EMHCameraMode GetCameraMode() const { return CurrentMode; }
// ========== 武器系统 ==========
// 切换武器
UFUNCTION(BlueprintCallable, Category="Camera|Weapon")
void SetWeaponType(EMHWeaponType WeaponType);
// ========== 锁敌系统 ==========
// 激活锁敌
UFUNCTION(BlueprintCallable, Category="Camera|LockOn")
void ActivateLockOn(AActor* Target, FName BoneName = NAME_None);
// 取消锁敌
UFUNCTION(BlueprintCallable, Category="Camera|LockOn")
void DeactivateLockOn();
// 切换锁敌部位
UFUNCTION(BlueprintCallable, Category="Camera|LockOn")
void SwitchLockOnTarget(AActor* NewTarget, FName BoneName = NAME_None);
// 是否正在锁敌
UFUNCTION(BlueprintPure, Category="Camera|LockOn")
bool IsLockOnActive() const { return bLockOnActive; }
// ========== 骑乘系统 ==========
// 进入骑乘
UFUNCTION(BlueprintCallable, Category="Camera|Mount")
void EnterMountMode(AActor* Monster);
// 退出骑乘
UFUNCTION(BlueprintCallable, Category="Camera|Mount")
void ExitMountMode();
// 进入攻击模式
UFUNCTION(BlueprintCallable, Category="Camera|Mount")
void EnterMountAttack(FName TargetBone);
// 退出攻击模式
UFUNCTION(BlueprintCallable, Category="Camera|Mount")
void ExitMountAttack();
// 执行终结技
UFUNCTION(BlueprintCallable, Category="Camera|Mount")
void ExecuteFinisher();
// ========== 震动和效果 ==========
// 添加受击震动
UFUNCTION(BlueprintCallable, Category="Camera|Effects")
void AddHitShake(float Intensity, float Duration);
// 添加攻击震动
UFUNCTION(BlueprintCallable, Category="Camera|Effects")
void AddAttackShake(float Intensity);
// 添加 FOV 效果
UFUNCTION(BlueprintCallable, Category="Camera|Effects")
void AddFOVEffect(float FOVOffset, float Duration);
// ========== 状态查询 ==========
UFUNCTION(BlueprintPure, Category="Camera")
bool IsBlending() const;
UFUNCTION(BlueprintPure, Category="Camera")
float GetBlendProgress() const;
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
private:
// 当前状态
EMHCameraMode CurrentMode = EMHCameraMode::Explore;
EMHWeaponType CurrentWeapon = EMHWeaponType::GreatSword;
// 锁敌状态
bool bLockOnActive = false;
TWeakObjectPtr<AActor> LockOnTarget;
FName LockOnBoneName;
// 骑乘状态
bool bInMountMode = false;
bool bInMountAttack = false;
bool bInFinisher = false;
TWeakObjectPtr<AActor> MountedMonster;
// Instance IDs
FCameraRigInstanceID ExploreRigInstanceID;
FCameraRigInstanceID CombatRigInstanceID;
FCameraRigInstanceID MountRigInstanceID;
FCameraRigInstanceID LockOnRigInstanceID;
FCameraRigInstanceID ShakeRigInstanceID;
FCameraRigInstanceID FOVRigInstanceID;
// 内部方法
void TransitionToMode(EMHCameraMode NewMode);
UCameraRigAsset* GetWeaponSpecificRig() const;
};
// ========================================
// MonsterHunterCameraManager.cpp
// ========================================
#include "MonsterHunterCameraManager.h"
#include "GameFramework/ActivateCameraRigFunctions.h"
#include "GameFramework/GameplayCameraComponent.h"
AMonsterHunterCameraManager::AMonsterHunterCameraManager()
{
PrimaryActorTick.bCanEverTick = true;
}
void AMonsterHunterCameraManager::BeginPlay()
{
Super::BeginPlay();
// 初始化:激活探索相机
FActivateCameraRigParams Params;
Params.CameraRig = RigConfig.ExploreRig;
Params.Layer = ECameraRigLayer::Main;
ExploreRigInstanceID = UActivateCameraRigFunctions::ActivateCameraRig(Params);
}
void AMonsterHunterCameraManager::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 更新锁敌目标位置(如果需要)
if (bLockOnActive && LockOnTarget.IsValid())
{
// 可以在这里更新 VariableTable 中的锁敌位置
}
}
// ==================== 相机模式 ====================
void AMonsterHunterCameraManager::SetCameraMode(EMHCameraMode NewMode)
{
if (NewMode == CurrentMode) return;
TransitionToMode(NewMode);
CurrentMode = NewMode;
}
void AMonsterHunterCameraManager::TransitionToMode(EMHCameraMode NewMode)
{
UCameraRigAsset* TargetRig = nullptr;
UCameraRigTransition* Transition = nullptr;
switch (NewMode)
{
case EMHCameraMode::Explore:
TargetRig = RigConfig.ExploreRig;
Transition = TransitionConfig.CombatToExplore;
break;
case EMHCameraMode::Combat:
TargetRig = GetWeaponSpecificRig();
Transition = TransitionConfig.ExploreToCombat;
break;
case EMHCameraMode::Mount:
TargetRig = RigConfig.MountBaseRig;
Transition = TransitionConfig.ToMount;
break;
default:
return;
}
if (TargetRig)
{
FActivateCameraRigParams Params;
Params.CameraRig = TargetRig;
Params.Layer = ECameraRigLayer::Main;
Params.TransitionOverride = Transition;
CombatRigInstanceID = UActivateCameraRigFunctions::ActivateCameraRig(Params);
}
}
// ==================== 武器系统 ====================
void AMonsterHunterCameraManager::SetWeaponType(EMHWeaponType WeaponType)
{
if (WeaponType == CurrentWeapon) return;
EMHWeaponType OldWeapon = CurrentWeapon;
CurrentWeapon = WeaponType;
// 如果在战斗模式,切换到对应武器的相机
if (CurrentMode == EMHCameraMode::Combat)
{
UCameraRigAsset* NewRig = GetWeaponSpecificRig();
FActivateCameraRigParams Params;
Params.CameraRig = NewRig;
Params.Layer = ECameraRigLayer::Main;
Params.TransitionOverride = TransitionConfig.WeaponSwitch;
CombatRigInstanceID = UActivateCameraRigFunctions::ActivateCameraRig(Params);
UE_LOG(LogTemp, Log, TEXT("[Camera] Weapon switch: %s -> %s"),
*UEnum::GetValueAsString(OldWeapon),
*UEnum::GetValueAsString(WeaponType));
}
}
UCameraRigAsset* AMonsterHunterCameraManager::GetWeaponSpecificRig() const
{
if (UCameraRigAsset* const* Found = RigConfig.WeaponSpecificRigs.Find(CurrentWeapon))
{
return *Found;
}
return RigConfig.CombatBaseRig;
}
// ==================== 锁敌系统 ====================
void AMonsterHunterCameraManager::ActivateLockOn(AActor* Target, FName BoneName)
{
if (!Target || bLockOnActive) return;
LockOnTarget = Target;
LockOnBoneName = BoneName;
// 激活锁敌 Rig
FActivateCameraRigParams Params;
Params.CameraRig = RigConfig.LockOnRig;
Params.Layer = ECameraRigLayer::Global;
Params.TransitionOverride = TransitionConfig.ToLockOn;
LockOnRigInstanceID = UActivateCameraRigFunctions::ActivateCameraRig(Params);
bLockOnActive = true;
UE_LOG(LogTemp, Log, TEXT("[Camera] LockOn activated on %s (bone: %s)"),
*Target->GetName(), *BoneName.ToString());
}
void AMonsterHunterCameraManager::DeactivateLockOn()
{
if (!bLockOnActive) return;
FDeactivateCameraRigParams Params;
Params.CameraRig = RigConfig.LockOnRig;
Params.Layer = ECameraRigLayer::Global;
Params.TransitionOverride = TransitionConfig.FromLockOn;
UActivateCameraRigFunctions::DeactivateCameraRig(Params);
LockOnTarget.Reset();
bLockOnActive = false;
UE_LOG(LogTemp, Log, TEXT("[Camera] LockOn deactivated"));
}
void AMonsterHunterCameraManager::SwitchLockOnTarget(AActor* NewTarget, FName BoneName)
{
if (!bLockOnActive || !NewTarget) return;
LockOnTarget = NewTarget;
LockOnBoneName = BoneName;
UE_LOG(LogTemp, Log, TEXT("[Camera] LockOn switched to %s"), *NewTarget->GetName());
}
// ==================== 骑乘系统 ====================
void AMonsterHunterCameraManager::EnterMountMode(AActor* Monster)
{
if (!Monster || bInMountMode) return;
MountedMonster = Monster;
bInMountMode = true;
// 切换到骑乘模式
SetCameraMode(EMHCameraMode::Mount);
UE_LOG(LogTemp, Log, TEXT("[Camera] Entered mount mode on %s"), *Monster->GetName());
}
void AMonsterHunterCameraManager::ExitMountMode()
{
if (!bInMountMode) return;
bInMountMode = false;
bInMountAttack = false;
bInFinisher = false;
MountedMonster.Reset();
// 切换回战斗模式
SetCameraMode(EMHCameraMode::Combat);
UE_LOG(LogTemp, Log, TEXT("[Camera] Exited mount mode"));
}
void AMonsterHunterCameraManager::EnterMountAttack(FName TargetBone)
{
if (!bInMountMode || bInMountAttack) return;
bInMountAttack = true;
// 激活攻击相机
FActivateCameraRigParams Params;
Params.CameraRig = RigConfig.MountAttackRig;
Params.Layer = ECameraRigLayer::Main;
MountRigInstanceID = UActivateCameraRigFunctions::ActivateCameraRig(Params);
UE_LOG(LogTemp, Log, TEXT("[Camera] Mount attack on bone: %s"), *TargetBone.ToString());
}
void AMonsterHunterCameraManager::ExitMountAttack()
{
if (!bInMountAttack) return;
bInMountAttack = false;
// 切换回基础骑乘相机
FActivateCameraRigParams Params;
Params.CameraRig = RigConfig.MountBaseRig;
Params.Layer = ECameraRigLayer::Main;
UActivateCameraRigFunctions::ActivateCameraRig(Params);
UE_LOG(LogTemp, Log, TEXT("[Camera] Mount attack ended"));
}
void AMonsterHunterCameraManager::ExecuteFinisher()
{
if (!bInMountMode || bInFinisher) return;
bInFinisher = true;
// 激活终结技相机
FActivateCameraRigParams Params;
Params.CameraRig = RigConfig.MountFinisherRig;
Params.Layer = ECameraRigLayer::Main;
MountRigInstanceID = UActivateCameraRigFunctions::ActivateCameraRig(Params);
// 添加 FOV 效果
AddFOVEffect(-10, 1.5f);
UE_LOG(LogTemp, Log, TEXT("[Camera] Finisher executed"));
}
// ==================== 震动和效果 ====================
void AMonsterHunterCameraManager::AddHitShake(float Intensity, float Duration)
{
FCameraShakeParams ShakeParams;
ShakeParams.Scale = Intensity;
ShakeParams.PositionAmplitude = FVector3f(5, 5, 3) * Intensity;
ShakeParams.RotationAmplitude = FRotator3f(2, 2, 0.5f) * Intensity;
ShakeParams.AttackTime = 0.05f;
ShakeParams.SustainTime = Duration * 0.3f;
ShakeParams.DecayTime = Duration * 0.7f;
// 通过震动服务添加
// GetGameplayCameraComponent()->AddCameraShake(ShakeParams);
UE_LOG(LogTemp, Log, TEXT("[Camera] Hit shake: intensity=%.2f, duration=%.2f"),
Intensity, Duration);
}
void AMonsterHunterCameraManager::AddAttackShake(float Intensity)
{
FCameraShakeParams ShakeParams;
ShakeParams.Scale = Intensity;
ShakeParams.PositionAmplitude = FVector3f(1, 1, 0.5f) * Intensity;
ShakeParams.RotationAmplitude = FRotator3f(0.3f, 0.3f, 0.1f) * Intensity;
ShakeParams.AttackTime = 0.02f;
ShakeParams.SustainTime = 0.05f;
ShakeParams.DecayTime = 0.1f;
// GetGameplayCameraComponent()->AddCameraShake(ShakeParams);
}
void AMonsterHunterCameraManager::AddFOVEffect(float FOVOffset, float Duration)
{
// 激活 FOV 效果 Rig
FActivateCameraRigParams Params;
Params.CameraRig = RigConfig.FOVEffectRig;
Params.Layer = ECameraRigLayer::Visual;
FOVRigInstanceID = UActivateCameraRigFunctions::ActivateCameraRig(Params);
// 设置 FOV 偏移(通过 VariableTable)
// GetVariableTable()->SetValue("FOVOffset", FOVOffset);
// 设置定时器移除效果
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, [this]()
{
FDeactivateCameraRigParams DeactivateParams;
DeactivateParams.CameraRig = RigConfig.FOVEffectRig;
DeactivateParams.Layer = ECameraRigLayer::Visual;
UActivateCameraRigFunctions::DeactivateCameraRig(DeactivateParams);
}, Duration, false);
UE_LOG(LogTemp, Log, TEXT("[Camera] FOV effect: offset=%.1f, duration=%.2f"),
FOVOffset, Duration);
}
// ==================== 状态查询 ====================
bool AMonsterHunterCameraManager::IsBlending() const
{
// 查询是否在混合中
return false; // TODO: 实现查询
}
float AMonsterHunterCameraManager::GetBlendProgress() const
{
return 0.0f; // TODO: 实现查询
}
9. 性能优化与最佳实践
9.1 性能考虑
┌─────────────────────────────────────────────────────────────────────────────┐
│ GameplayCamera 性能优化 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 节点评估器缓存 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 节点评估器在 Rig 首次激活时创建并缓存 │ │
│ │ • 避免每帧重新创建评估器 │ │
│ │ • 使用对象池管理评估器实例 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 2. 条件评估 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 只有激活的 Rig 才会被评估 │ │
│ │ • 混合中的 Rig 都会被评估(需要计算混合) │ │
│ │ • 停用的 Rig 完全不消耗性能 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 3. 变量表优化 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 使用句柄而非字符串访问变量 │ │
│ │ • 避免每帧查找变量名 │ │
│ │ • 预编译变量访问路径 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 4. 层级裁剪 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 空层不会被评估 │ │
│ │ • Global/Visual 层可以完全禁用 │ │
│ │ • 根据游戏状态动态启用/禁用层 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
9.2 最佳实践
// 1. 使用 Layer 分离关注点
// ✅ 好的做法
Base Layer: 基础跟随(始终运行)
Main Layer: 主相机切换(游戏玩法)
Global Layer: 全局效果(锁敌、震动)
Visual Layer: 视觉反馈(FOV、后处理)
// ❌ 不好的做法
把所有效果都放在一个 Rig 里
// 2. 避免深层嵌套的连续混合
// ✅ 好的做法
设计时就考虑相机切换的流程,减少不必要的切换
// ❌ 不好的做法
在 0.5 秒内连续切换 5 次相机
// 3. 合理使用过渡时间
// ✅ 好的做法
探索→战斗: 1.0s(平滑过渡)
战斗→探索: 0.5s(快速响应)
受击震动: 0.1s(即时反馈)
// ❌ 不好的做法
所有过渡都用 2.0s
// 4. 使用 VariableTable 传递参数
// ✅ 好的做法
// 在蓝图中定义 Camera Variable
// 在代码中设置值
VariableTable->SetValue("LockOnTarget", TargetActor);
// ❌ 不好的做法
// 直接访问评估器内部状态
Evaluator->LockOnTarget = TargetActor;
9.3 调试工具
// 相机调试命令
UCLASS()
class UCameraDebugComponent : public UActorComponent
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category="Debug")
bool bShowDebugInfo = false;
UPROPERTY(EditAnywhere, Category="Debug")
bool bShowLayerInfo = false;
UPROPERTY(EditAnywhere, Category="Debug")
bool bShowBlendStack = false;
UPROPERTY(EditAnywhere, Category="Debug")
bool bDrawDebugLines = false;
protected:
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override
{
if (!bShowDebugInfo) return;
// 显示当前相机状态
if (bShowLayerInfo)
{
// 显示各层状态
DrawLayerDebug();
}
if (bShowBlendStack)
{
// 显示混合栈状态
DrawBlendStackDebug();
}
if (bDrawDebugLines)
{
// 绘制调试线
DrawDebugLines();
}
}
void DrawLayerDebug()
{
// 在屏幕上显示:
// Base Layer: AttachRig (Active)
// Main Layer: CombatRig (Blending: 50%)
// Global Layer: LockOnRig (Active)
// Visual Layer: (Empty)
}
void DrawBlendStackDebug()
{
// 显示混合栈内容:
// [0] GreatSwordRig (Weight: 0.5)
// [1] DualBladesRig (Weight: 0.5)
// Blend Time Remaining: 0.5s
}
void DrawDebugLines()
{
// 绘制:
// - 相机位置
// - 相机朝向
// - 锁敌目标连线
// - 跟随目标位置
}
};
10. 与 Cinemachine 的深度对比
10.1 核心差异总结
┌─────────────────────────────────────────────────────────────────────────────┐
│ GameplayCamera vs Cinemachine │
├─────────────────────────────┬───────────────────────────────────────────────┤
│ Cinemachine │ GameplayCamera │
├─────────────────────────────┼───────────────────────────────────────────────┤
│ │ │
│ 【架构】 │ 【架构】 │
│ 组件式 │ 节点-评估器分离 │
│ Extension 叠加效果 │ Layer 分层效果 │
│ │ │
│ 【混合】 │ 【混合】 │
│ NestedBlendSource 嵌套 │ Snapshot 快照 │
│ 混合继续更新 │ 混合被打断 │
│ │ │
│ 【效果叠加】 │ 【效果叠加】 │
│ Override Stack │ Layer System │
│ Extension 回调 │ Evaluation Service │
│ │ │
│ 【配置】 │ 【配置】 │
│ Inspector 直接配置 │ 资产 + 节点图 │
│ 运行时可修改 │ 数据驱动 │
│ │ │
│ 【扩展】 │ 【扩展】 │
│ 继承 CinemachineExtension │ 实现自定义节点 + 评估器 │
│ MonoBehaviour │ UObject + C++ 类 │
│ │ │
│ 【性能】 │ 【性能】 │
│ C# 开销 │ C++ 原生性能 │
│ GC 压力 │ 无 GC │
│ │ │
│ 【易用性】 │ 【易用性】 │
│ 配置简单直观 │ 学习曲线陡峭 │
│ 可视化调试好 │ 需要自定义调试工具 │
│ │ │
└─────────────────────────────┴───────────────────────────────────────────────┘
10.2 选择建议
| 场景 | 推荐 | 原因 |
|---|---|---|
| 快速原型开发 | Cinemachine | 配置简单,上手快 |
| 复杂相机系统 | GameplayCamera | 分层架构更适合复杂需求 |
| 高性能要求 | GameplayCamera | C++ 原生性能 |
| 团队协作 | GameplayCamera | 数据驱动,版本控制友好 |
| Unity 项目 | Cinemachine | 原生集成 |
| UE5 项目 | GameplayCamera | 原生集成 |
10.3 怪物猎人风格游戏的选择
对于怪物猎人这类复杂相机系统,GameplayCamera 更适合:
- 分层架构:天然支持效果叠加
- 数据驱动:武器类型、怪物类型可以对应不同的相机配置
- C++ 性能:复杂的锁敌计算、震动计算
- 可扩展性:自定义节点实现特殊行为
附录
A. 关键类参考
| 类 | 路径 | 说明 |
|---|---|---|
| UCameraRigAsset | Camera/Core/CameraRigAsset.h | 相机装备资产 |
| UCameraNode | Camera/Core/CameraNode.h | 相机节点基类 |
| FCameraNodeEvaluator | Camera/Core/CameraNodeEvaluator.h | 节点评估器基类 |
| FCameraPose | Camera/Core/CameraPose.h | 相机姿态 |
| FCameraSystemEvaluator | Camera/Core/CameraSystemEvaluator.h | 系统评估器 |
| FBlendStackCameraNodeEvaluator | Camera/Nodes/BlendStackCameraNodeEvaluator.h | 混合栈评估器 |
B. 示例项目结构
/MonsterHunterCamera
├── CameraRigs/
│ ├── Explore/
│ │ └── ExploreRig.uasset
│ ├── Combat/
│ │ ├── GreatSwordRig.uasset
│ │ ├── DualBladesRig.uasset
│ │ └── ...
│ ├── Mount/
│ │ ├── MountBaseRig.uasset
│ │ ├── MountAttackRig.uasset
│ │ └── MountFinisherRig.uasset
│ ├── Effects/
│ │ ├── LockOnRig.uasset
│ │ ├── ShakeRig.uasset
│ │ └── FOVEffectRig.uasset
│ └── Base/
│ └── AttachRig.uasset
├── CameraNodes/
│ ├── LockOnAimCameraNode.uasset
│ ├── AttachToMonsterCameraNode.uasset
│ └── ...
├── CameraTransitions/
│ ├── ExploreToCombat.uasset
│ ├── ToMount.uasset
│ └── ...
└── Blueprint/
├── MonsterHunterCameraManager.uasset
└── CameraDebugComponent.uasset
本文档详细解析了 GameplayCamera 相机系统的使用方法和内部机制
以《怪物猎人》为例,涵盖了相机切换、效果叠加、连续混合等复杂场景
最后更新: 2026-02-25