Lyra的相机系统采用分层架构和栈式混合的设计模式,主要包含继承自APlayerCameraManager的作为总管理器的组件ALyraPlayerCameraManager,负责协调所有相机的工作,继承自UCameraComponent的相机组件,每个角色都有一份相机组件实例,这个组件包含一个很重要的相机模式栈ULyraCameraModeStack,用于管理多个相机模式的混合,一个普通对象ULyraCameraMode,作为相机模式的对象存在,定义相机的基本行为,每个相机都有自己的视角计算逻辑,支持平滑过渡混合,使用不同的混合函数,ULyraCameraMode_ThirdPerson是第三人称的具体实现。
ALyraPlayerCameraManager
LyraPlayerCameraManager 主要做了两件事,第一件事是创建并保存了UICamera的引用,第二件事是判断UICamera是否需要更新摄像机状态。
首先LyraPlayerCameraManager分别设置了三个宏,一个是相机默认FOV(视野)的值,另外两个是最大/最小俯仰角

如果UI相机需要更新,那么会先调用父类的相机更新逻辑,再让UI相机去覆盖,UI相机的优先级高于父类
plain
void ALyraPlayerCameraManager::UpdateViewTarget(FTViewTarget& OutVT, float DeltaTime)
{
// If the UI Camera is looking at something, let it have priority.
if (UICamera->NeedsToUpdateViewTarget())
{
Super::UpdateViewTarget(OutVT, DeltaTime);
UICamera->UpdateViewTarget(OutVT, DeltaTime);
return;
}
Super::UpdateViewTarget(OutVT, DeltaTime);
}
UICamera重写没有实现,但是在父类中,它的实现是这样的:
plain
void APlayerCameraManager::UpdateViewTarget(FTViewTarget& OutVT, float DeltaTime)
{
// Don't update outgoing viewtarget during an interpolation
if ((PendingViewTarget.Target != NULL) && BlendParams.bLockOutgoing && OutVT.Equal(ViewTarget))
{
return;
}
// Store previous POV, in case we need it later
FMinimalViewInfo OrigPOV = OutVT.POV;
// Reset the view target POV fully
static const FMinimalViewInfo DefaultViewInfo;
OutVT.POV = DefaultViewInfo;
OutVT.POV.FOV = DefaultFOV;
OutVT.POV.OrthoWidth = DefaultOrthoWidth;
OutVT.POV.AspectRatio = DefaultAspectRatio;
OutVT.POV.bConstrainAspectRatio = bDefaultConstrainAspectRatio;
OutVT.POV.ProjectionMode = bIsOrthographic ? ECameraProjectionMode::Orthographic : ECameraProjectionMode::Perspective;
OutVT.POV.PostProcessBlendWeight = 1.0f;
OutVT.POV.bAutoCalculateOrthoPlanes = bAutoCalculateOrthoPlanes;
OutVT.POV.AutoPlaneShift = AutoPlaneShift;
OutVT.POV.bUpdateOrthoPlanes = bUpdateOrthoPlanes;
OutVT.POV.bUseCameraHeightAsViewTarget = bUseCameraHeightAsViewTarget;
bool bDoNotApplyModifiers = false;
if (ACameraActor* CamActor = Cast<ACameraActor>(OutVT.Target))
{
// Viewing through a camera actor.
CamActor->GetCameraComponent()->GetCameraView(DeltaTime, OutVT.POV);
}
else
{
static const FName NAME_Fixed = FName(TEXT("Fixed"));
static const FName NAME_ThirdPerson = FName(TEXT("ThirdPerson"));
static const FName NAME_FreeCam = FName(TEXT("FreeCam"));
static const FName NAME_FreeCam_Default = FName(TEXT("FreeCam_Default"));
static const FName NAME_FirstPerson = FName(TEXT("FirstPerson"));
if (CameraStyle == NAME_Fixed)
{
// do not update, keep previous camera position by restoring
// saved POV, in case CalcCamera changes it but still returns false
OutVT.POV = OrigPOV;
// don't apply modifiers when using this debug camera mode
bDoNotApplyModifiers = true;
}
else if (CameraStyle == NAME_ThirdPerson || CameraStyle == NAME_FreeCam || CameraStyle == NAME_FreeCam_Default)
{
// Simple third person view implementation
FVector Loc = OutVT.Target->GetActorLocation();
FRotator Rotator = OutVT.Target->GetActorRotation();
if (OutVT.Target == PCOwner)
{
Loc = PCOwner->GetFocalLocation();
}
// Take into account Mesh Translation so it takes into account the PostProcessing we do there.
// @fixme, can crash in certain BP cases where default mesh is null
// APawn* TPawn = Cast<APawn>(OutVT.Target);
// if ((TPawn != NULL) && (TPawn->Mesh != NULL))
// {
// Loc += FQuatRotationMatrix(OutVT.Target->GetActorQuat()).TransformVector(TPawn->Mesh->RelativeLocation - GetDefault<APawn>(TPawn->GetClass())->Mesh->RelativeLocation);
// }
//OutVT.Target.GetActorEyesViewPoint(Loc, Rot);
if( CameraStyle == NAME_FreeCam || CameraStyle == NAME_FreeCam_Default )
{
Rotator = PCOwner->GetControlRotation();
}
FVector Pos = Loc + ViewTargetOffset + FRotationMatrix(Rotator).TransformVector(FreeCamOffset) - Rotator.Vector() * FreeCamDistance;
FCollisionQueryParams BoxParams(SCENE_QUERY_STAT(FreeCam), false, this);
BoxParams.AddIgnoredActor(OutVT.Target);
FHitResult Result;
GetWorld()->SweepSingleByChannel(Result, Loc, Pos, FQuat::Identity, ECC_Camera, FCollisionShape::MakeBox(FVector(12.f)), BoxParams);
OutVT.POV.Location = !Result.bBlockingHit ? Pos : Result.Location;
OutVT.POV.Rotation = Rotator;
// don't apply modifiers when using this debug camera mode
bDoNotApplyModifiers = true;
}
else if (CameraStyle == NAME_FirstPerson)
{
// Simple first person, view through viewtarget's 'eyes'
OutVT.Target->GetActorEyesViewPoint(OutVT.POV.Location, OutVT.POV.Rotation);
// don't apply modifiers when using this debug camera mode
bDoNotApplyModifiers = true;
}
else
{
UpdateViewTargetInternal(OutVT, DeltaTime);
}
}
if (!bDoNotApplyModifiers || bAlwaysApplyModifiers)
{
// Apply camera modifiers at the end (view shakes for example)
ApplyCameraModifiers(DeltaTime, OutVT.POV);
}
// Synchronize the actor with the view target results
SetActorLocationAndRotation(OutVT.POV.Location, OutVT.POV.Rotation, false);
if (bAutoCalculateOrthoPlanes && OutVT.Target)
{
OutVT.POV.SetCameraToViewTarget(OutVT.Target->GetActorLocation());
}
UpdateCameraLensEffects(OutVT);
}
父类主要是基于目标参数Actor计算基本的相机位置和旋转,应用相机抖动,后处理等效果,确保相机在合理范围内,保持玩家控制器与相机视角一致
Lyra重写的可能情况:假设场景里出现一个3D的UI,但是由于摄像机一直处于并跟随角色后方,如果此时打开这个UI,那么角色的摄像机就会移动(UpdateViewTarget)到指定的对象中,主要是灵活地运镜那种效果。
DisplayDebug是显示当前相机的Debug信息,显示基础相机信息,如果角色有相机组件,显示器调试信息
plain
void ALyraPlayerCameraManager::DisplayDebug(UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplay, float& YL, float& YPos)
{
check(Canvas);
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;
DisplayDebugManager.SetFont(GEngine->GetSmallFont());
DisplayDebugManager.SetDrawColor(FColor::Yellow);
DisplayDebugManager.DrawString(FString::Printf(TEXT("LyraPlayerCameraManager: %s"), *GetNameSafe(this)));
Super::DisplayDebug(Canvas, DebugDisplay, YL, YPos);
const APawn* Pawn = (PCOwner ? PCOwner->GetPawn() : nullptr);
if (const ULyraCameraComponent* CameraComponent = ULyraCameraComponent::FindCameraComponent(Pawn))
{
CameraComponent->DrawDebug(Canvas);
}
}
LyraUICameraManagerComponent
然后来看一下上面提到的UICamera是怎么实现的,这个组件继承自ActorComponent,我目前看的这个Lyra项目是5.6的,在当前版本里,这个UI相机优先级系统 并没有真正生效。但是目前架构推测可能是要做对话系统那种电影级相机,如果未来不是这么弄的那当我没说。
它构建了一个单例的Getter函数,主要从LyraPlayerCameraManager里获取
plain
ULyraUICameraManagerComponent* ULyraUICameraManagerComponent::GetComponent(APlayerController* PC)
{
if (PC != nullptr)
{
if (ALyraPlayerCameraManager* PCCamera = Cast<ALyraPlayerCameraManager>(PC->PlayerCameraManager))
{
return PCCamera->GetUICameraComponent();
}
}
return nullptr;
}
它自己 SetViewTarget函数,这个函数的作用是把相机切换到目标的相机上
plain
void ULyraUICameraManagerComponent::SetViewTarget(AActor* InViewTarget, FViewTargetTransitionParams TransitionParams)
{
TGuardValue<bool> UpdatingViewTargetGuard(bUpdatingViewTarget, true);
ViewTarget = InViewTarget;
CastChecked<ALyraPlayerCameraManager>(GetOwner())->SetViewTarget(ViewTarget, TransitionParams);
}
TGuardValue 是一个RAII(资源获取即初始化)模式的工具类,它的作用是
plain
template <typename RefType, typename AssignedType = RefType>
struct TGuardValue : private FNoncopyable
{
[[nodiscard]] TGuardValue(RefType& ReferenceValue, const AssignedType& NewValue)
: RefValue(ReferenceValue), OldValue(ReferenceValue)
{
RefValue = NewValue;
}
~TGuardValue()
{
RefValue = OldValue;
}
/**
* Overloaded dereference operator.
* Provides read-only access to the original value of the data being tracked by this struct
*
* @return a const reference to the original data value
*/
FORCEINLINE const AssignedType& operator*() const
{
return OldValue;
}
private:
RefType& RefValue;
AssignedType OldValue;
};
这个对象会在析构的时候将引用对象恢复成原来的值,所以TGuardValueUpdatingViewTargetGuard(bUpdatingViewTarget, true);的意思是在进入函数的时候将bUpdatingViewTarget设置为true,函数结束的时候就会自动将其恢复成原来的值,其一目的是防止函数被递归调用。
LyraCameraComponent
LyraCameraComponent主要管理多个相机模式的平滑混合和过渡,维护一个相机的栈结构,处理多个相机模式的权重混合,实现相机模式之间的平滑过渡;每帧计算相机应该在哪里,朝向哪里,视野多大;保持玩家输入方向和相机视角一致;提供相机决策模式接口,是Lyra相机系统的核心执行对象。
首先在组件开始注册的时候,会创建并保存相机模式栈,相机模式栈在构造的时候强制置空确保引用干净。
plain
void ULyraCameraComponent::OnRegister()
{
Super::OnRegister();
if (!CameraModeStack)
{
CameraModeStack = NewObject<ULyraCameraModeStack>(this);
check(CameraModeStack);
}
}
GetCameraView是最重要的方法,负责计算最终的相机视图
plain
void ULyraCameraComponent::GetCameraView(float DeltaTime, FMinimalViewInfo& DesiredView)
{
check(CameraModeStack);
UpdateCameraModes();
FLyraCameraModeView CameraModeView;
CameraModeStack->EvaluateStack(DeltaTime, CameraModeView);
// Keep player controller in sync with the latest view.
if (APawn* TargetPawn = Cast<APawn>(GetTargetActor()))
{
if (APlayerController* PC = TargetPawn->GetController<APlayerController>())
{
PC->SetControlRotation(CameraModeView.ControlRotation);
}
}
// Apply any offset that was added to the field of view.
CameraModeView.FieldOfView += FieldOfViewOffset;
FieldOfViewOffset = 0.0f;
// Keep camera component in sync with the latest view.
SetWorldLocationAndRotation(CameraModeView.Location, CameraModeView.Rotation);
FieldOfView = CameraModeView.FieldOfView;
// Fill in desired view.
DesiredView.Location = CameraModeView.Location;
DesiredView.Rotation = CameraModeView.Rotation;
DesiredView.FOV = CameraModeView.FieldOfView;
DesiredView.OrthoWidth = OrthoWidth;
DesiredView.OrthoNearClipPlane = OrthoNearClipPlane;
DesiredView.OrthoFarClipPlane = OrthoFarClipPlane;
DesiredView.AspectRatio = AspectRatio;
DesiredView.bConstrainAspectRatio = bConstrainAspectRatio;
DesiredView.bUseFieldOfViewForLOD = bUseFieldOfViewForLOD;
DesiredView.ProjectionMode = ProjectionMode;
// See if the CameraActor wants to override the PostProcess settings used.
DesiredView.PostProcessBlendWeight = PostProcessBlendWeight;
if (PostProcessBlendWeight > 0.0f)
{
DesiredView.PostProcessSettings = PostProcessSettings;
}
if (IsXRHeadTrackedCamera())
{
// In XR much of the camera behavior above is irrellevant, but the post process settings are not.
Super::GetCameraView(DeltaTime, DesiredView);
}
}
它首先会断言检查相机模式栈 是否存在,然后,它调用了更新相机模式的函数UpdateCameraModes,决定当前应该使用哪个相机模式,在这个函数里,它主要广播获取对应的LyraCameraMode,而这个广播绑定在角色的HeroComponent里
在对应的回调函数里,从该角色所拥有的PawnData中获取默认的相机模式,因此UpdateCameraModes是尝试从角色的PawnData中获取相机模式的。
调用EvaluateStack来评估模式栈,这里和上面有关于相机模式栈的部分会写在下面,这里先知道它的一些函数会在这里调用。
然后调用SetControlRotaiton来将玩家角色(Pawn)旋转至相机所看方向
plain
// Keep player controller in sync with the latest view.
if (APawn* TargetPawn = Cast<APawn>(GetTargetActor()))
{
if (APlayerController* PC = TargetPawn->GetController<APlayerController>())
{
PC->SetControlRotation(CameraModeView.ControlRotation);
}
}
然后应用视野偏移,这个是支持一次性的视野变化效果,比如受伤时候的屏幕晃动(参考我的世界那种),特殊技能的镜头震动,环境交互的视觉反馈
plain
// Apply any offset that was added to the field of view.
CameraModeView.FieldOfView += FieldOfViewOffset;
FieldOfViewOffset = 0.0f;
然后是同步相机组件状态,确保相机组件本身的Transform和相机模式中评估计算出来的结果一致。
plain
// Keep camera component in sync with the latest view.
SetWorldLocationAndRotation(CameraModeView.Location, CameraModeView.Rotation);
FieldOfView = CameraModeView.FieldOfView;
然后是一些后处理效果,这里可以扩展
plain
// See if the CameraActor wants to override the PostProcess settings used.
DesiredView.PostProcessBlendWeight = PostProcessBlendWeight;
if (PostProcessBlendWeight > 0.0f)
{
DesiredView.PostProcessSettings = PostProcessSettings;
}
最后是判断是否是VR/AR设备,这些设备有特殊需求,但是后处理设置依然很重要
最后在实际项目流程是这样的
plain
// 按下瞄准键之前
GetCameraView被调用:
1. UpdateCameraModes() → 委托返回"第三人称模式"
2. EvaluateStack() → 计算纯第三人称视角
3. 同步控制器 → 正常视角方向
4. 返回视图数据 → 标准第三人称画面
// 按下瞄准的第1帧 (按下一瞬间)
GetCameraView被调用:
1. UpdateCameraModes() → 委托返回"瞄准模式"
2. EvaluateStack() → 推入瞄准模式,开始混合
- 栈状态:[第三人称模式权重80%] + [瞄准模式权重20%]
3. 混合计算 → 80%第三人称 + 20%瞄准的过渡视角
4. 返回视图数据 → 相机开始向瞄准位置移动
// 按下瞄准的第10帧(大概就是完全按下这样)
GetCameraView被调用:
1. UpdateCameraModes() → 委托返回"瞄准模式"
2. EvaluateStack() → 混合完成
- 栈状态:[第三人称模式权重0%] + [瞄准模式权重100%]
3. 混合计算 → 纯瞄准视角
4. 返回视图数据 → 标准的瞄准画面
LyraCameraMode
LyraCameraMode继承自UObject,是个普通的基础对象类,它标记成不能直接实例化需要子类化的抽象类(Abstract)和不可蓝图化(NotBlueprintable),只能在C++中实现
首先是FLyraCameraModeView - 相机视图数据结构,内部包含封装相机的基本视图参数,用于模式间的混合计算。
plain
struct FLyraCameraModeView
{
public:
FLyraCameraModeView();
void Blend(const FLyraCameraModeView& Other, float OtherWeight);
public:
FVector Location; // 相机位置
FRotator Rotation; // 相机旋转
FRotator ControlRotation; // 控制器旋转(用于同步)
float FieldOfView; // 视野角度
};
ELyraCameraModeBlendFunction - 混合函数枚举,定义相机模式过渡的数学曲线模型,实现不同的过渡效果
plain
enum class ELyraCameraModeBlendFunction : uint8
{
Linear, // 线性插值
EaseIn, // 加速进入,平滑退出
EaseOut, // 平滑进入,不减速退出
EaseInOut // 平滑进入和退出
};
混合相关参数,控制相机模式的 过渡动画 行为。
plain
float BlendTime; // 混合时间(秒)
ELyraCameraModeBlendFunction BlendFunction; // 混合函数类型
float BlendExponent; // 混合曲线指数
float BlendAlpha; // 线性混合Alpha值
float BlendWeight; // 计算出的混合权重
相机参数,定义该相机模式的基本视觉参数。
首先是**GetPivotLoccation()**方法,计算相机的基础位置
plain
FVector ULyraCameraMode::GetPivotLocation() const
{
const AActor* TargetActor = GetTargetActor();
check(TargetActor);
if (const APawn* TargetPawn = Cast<APawn>(TargetActor))
{
// Height adjustments for characters to account for crouching.
if (const ACharacter* TargetCharacter = Cast<ACharacter>(TargetPawn))
{
const ACharacter* TargetCharacterCDO = TargetCharacter->GetClass()->GetDefaultObject<ACharacter>();
check(TargetCharacterCDO);
const UCapsuleComponent* CapsuleComp = TargetCharacter->GetCapsuleComponent();
check(CapsuleComp);
const UCapsuleComponent* CapsuleCompCDO = TargetCharacterCDO->GetCapsuleComponent();
check(CapsuleCompCDO);
const float DefaultHalfHeight = CapsuleCompCDO->GetUnscaledCapsuleHalfHeight();
const float ActualHalfHeight = CapsuleComp->GetUnscaledCapsuleHalfHeight();
const float HeightAdjustment = (DefaultHalfHeight - ActualHalfHeight) + TargetCharacterCDO->BaseEyeHeight;
return TargetCharacter->GetActorLocation() + (FVector::UpVector * HeightAdjustment);
}
return TargetPawn->GetPawnViewLocation();
}
return TargetActor->GetActorLocation();
}
首先获取对象,这个对象是从创建这个相机模式的外部对象中获取的
plain
ULyraCameraComponent* ULyraCameraMode::GetLyraCameraComponent() const
{
return CastChecked<ULyraCameraComponent>(GetOuter());
}
然后将对象转换成我们的角色,获取角色的胶囊体,然后计算蹲伏高度调整,对这个函数主要是处理蹲伏的;DefaultHalfHeight就是角色站立时的胶囊体默认半高 ,ActualHalfHeight就是角色当前胶囊体的实际半高 ,HeightAdjustment就是最终的高度调整, ++高度调整 = (默认高度 - 实际高度) + 基础眼睛高度++。
最终位置 : 角色位置 + 向上向量 × 高度调整
具体效果表现为:当角色蹲下时,胶囊体会变矮(这个功能和相关配置在角色的移动组件里有),相机也会相应降低高度,不同的角色相机计算出的基础位置也不同
然后是GetPivotRotation() 方法 - 计算相机的基础旋转
plain
FRotator ULyraCameraMode::GetPivotRotation() const
{
const AActor* TargetActor = GetTargetActor();
check(TargetActor);
if (const APawn* TargetPawn = Cast<APawn>(TargetActor))
{
return TargetPawn->GetViewRotation();
}
return TargetActor->GetActorRotation();
}
具体计算过程:如果是Pawn角色,返回角色的视角旋转(玩家控制的方向),如果是普通Actor,返回Actor自身的旋转,具体表现为相机会跟随玩家鼠标/手柄的输入方向,Actor自身旋转的话就是让相机始终朝着角色面对的方向。
然后是UpdateView方法,计算每帧相机应该在哪里,朝向哪里,视野多大
plain
void ULyraCameraMode::UpdateView(float DeltaTime)
{
FVector PivotLocation = GetPivotLocation();
FRotator PivotRotation = GetPivotRotation();
PivotRotation.Pitch = FMath::ClampAngle(PivotRotation.Pitch, ViewPitchMin, ViewPitchMax);
View.Location = PivotLocation;
View.Rotation = PivotRotation;
View.ControlRotation = View.Rotation;
View.FieldOfView = FieldOfView;
}
首先调用上面说到的两个函数获取相机的位置和旋转,然后限制相机的旋转俯仰角

然后设置最终的视图参数,位置、朝向、控制器旋转、视野高度
接下来我们来看UpdateBlending() 方法 - 相机模式混合计算
plain
void ULyraCameraMode::UpdateBlending(float DeltaTime)
{
if (BlendTime > 0.0f)
{
BlendAlpha += (DeltaTime / BlendTime);
BlendAlpha = FMath::Min(BlendAlpha, 1.0f);
}
else
{
BlendAlpha = 1.0f;
}
const float Exponent = (BlendExponent > 0.0f) ? BlendExponent : 1.0f;
switch (BlendFunction)
{
case ELyraCameraModeBlendFunction::Linear:
BlendWeight = BlendAlpha;
break;
case ELyraCameraModeBlendFunction::EaseIn:
BlendWeight = FMath::InterpEaseIn(0.0f, 1.0f, BlendAlpha, Exponent);
break;
case ELyraCameraModeBlendFunction::EaseOut:
BlendWeight = FMath::InterpEaseOut(0.0f, 1.0f, BlendAlpha, Exponent);
break;
case ELyraCameraModeBlendFunction::EaseInOut:
BlendWeight = FMath::InterpEaseInOut(0.0f, 1.0f, BlendAlpha, Exponent);
break;
default:
checkf(false, TEXT("UpdateBlending: Invalid BlendFunction [%d]\n"), (uint8)BlendFunction);
break;
}
}
具体计算过程:++BlendAlpha += (DeltaTime / BlendTime)++,限制进度不超过0,然后根据混合函数计算权重
这个是上面介绍的ELyraCameraModeBlendFunction枚举
1.Linear : 线性混合,直接使用进度
2.EaseIn : 缓入效果,开始慢,后面快
3.EaseOut : 缓出效果,开始快,后面慢
4.EaseInOut : 缓入缓出,开始和结束都慢
这个方法确保相机模式切换的时候不会突然跳变,同时支持不同的过渡曲线,也可以设置不同的过渡事件
SetBlendWeight(float Weight) - 设置混合权重
plain
void ULyraCameraMode::SetBlendWeight(float Weight)
{
BlendWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
// Since we're setting the blend weight directly, we need to calculate the blend alpha to account for the blend function.
const float InvExponent = (BlendExponent > 0.0f) ? (1.0f / BlendExponent) : 1.0f;
switch (BlendFunction)
{
case ELyraCameraModeBlendFunction::Linear:
BlendAlpha = BlendWeight;
break;
case ELyraCameraModeBlendFunction::EaseIn:
BlendAlpha = FMath::InterpEaseIn(0.0f, 1.0f, BlendWeight, InvExponent);
break;
case ELyraCameraModeBlendFunction::EaseOut:
BlendAlpha = FMath::InterpEaseOut(0.0f, 1.0f, BlendWeight, InvExponent);
break;
case ELyraCameraModeBlendFunction::EaseInOut:
BlendAlpha = FMath::InterpEaseInOut(0.0f, 1.0f, BlendWeight, InvExponent);
break;
default:
checkf(false, TEXT("SetBlendWeight: Invalid BlendFunction [%d]\n"), (uint8)BlendFunction);
break;
}
}
具体计算过程:
- ++权重限制 : BlendWeight = FMath::Clamp(Weight, 0.0f, 1.0f)++
- ++指数计算 : InvExponent = 1.0f / BlendExponent++
- ++Linear : BlendAlpha = BlendWeight (直接相等)++
- ++EaseIn : BlendAlpha = pow(BlendWeight, InvExponent) (指数函数)++
- ++EaseOut : BlendAlpha = 1 - pow(1 - BlendWeight, InvExponent) (反向指数)++
- ++EaseInOut : 结合EaseIn和EaseOut的S形曲线++
这个函数用于实现相机抖动的,比如角色受到攻击时: SetBlendWeight(0.3f) 产生轻微相机抖动;爆炸效果: SetBlendWeight(0.8f) 产生强烈相机震动
三种函数协同工作示例:
plain
// 1. 游戏逻辑决定需要切换到瞄准模式
ULyraCameraMode* AimMode = CameraModeStack->PushCameraMode(AimModeClass);
// 2. 查询瞄准模式的混合时间
float AimBlendTime = AimMode->GetBlendTime(); // 返回0.2秒
// 3. 每帧更新混合进度
AimMode->UpdateBlending(DeltaTime);
// 4. 查询当前权重
float CurrentWeight = AimMode->GetBlendWeight(); // 从0.0到1.0变化
// 5. 如果需要特殊效果,手动设置权重
if (PlayerHitByBullet) {
AimMode->SetBlendWeight(0.5f); // 相机抖动效果
}
LyraCameraModeStack
ULyraCameraModeStack也是一个UObject对象,它定义在LyraCameraMode类里,这个类是管理多个相机模式 (就是上面的CameraMode)的堆叠 和混合 ,实现的是"栈式混合系统 ",允许多个相机模式同时生效 并按权重混合。
深入研究一下这个类的设计思想:
ULyraCameraModeStack类似一个 相机效果的Photoshop图层系统 ,有多个图层,可以叠加效果,可以同时有"第三人称" + "瞄准" + "抖动" + "过场动画"效果,每个效果都有自己的透明度和过渡时间。
而对于LyraCameraModeStack来说,每一个"图层"就是一个相机模式。
比如正常游戏状态下:
栈状态:[第三人称模式] (100%生效)
效果:角色背后视角,正常视野
当按下瞄准的时候:
栈状态变化:
瞄准模式\] (开始混合) → \[第三人称模式\] (100%)
实际效果表现为:0.2秒内,相机从背后平滑地移动到肩膀的位置,视野从正常缩小到瞄准视野,自然地过渡,不会突然地跳变
```plain
// Lyra做法:效果叠加
if (isAiming) {
stack->PushCameraMode(aimMode); // 添加瞄准效果
}
if (isShaking) {
stack->PushCameraMode(shakeMode); // 添加抖动效果
}
// 结果:瞄准和抖动效果同时生效
```
加入的镜头效果,放大,变换会在最顶层添加**相机模式**(栈思想),然后移除的时候最先移除最后一个添加的效果。
因此,这个了类里的所有方法和定义的属性都是**围绕以上方法实现**的
先来看PushCameraMode() 方法 - 添加相机模式到栈顶
```plain
void ULyraCameraModeStack::PushCameraMode(TSubclassOf