第一次提交

This commit is contained in:
不明不惑
2026-03-03 01:23:02 +08:00
commit 3e434877e8
1053 changed files with 102411 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
using UnrealBuildTool;
public class GenericCameraSystem : ModuleRules
{
public GenericCameraSystem(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] {
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"GameplayTags"
// ... add other public dependencies that you statically link with here ...
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
// ... add private dependencies that you statically link with here ...
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}

View File

@@ -0,0 +1,258 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GCMS_CameraMode.h"
#include "GCMS_CameraSystemComponent.h"
#include "Components/CapsuleComponent.h"
#include "Engine/Canvas.h"
#include "GameFramework/Character.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraMode)
//////////////////////////////////////////////////////////////////////////
// FGMS_CameraModeView
//////////////////////////////////////////////////////////////////////////
FGCMS_CameraModeView::FGCMS_CameraModeView()
: Location(ForceInit)
, Rotation(ForceInit)
, ControlRotation(ForceInit)
, FieldOfView(80.0f)
{
}
void FGCMS_CameraModeView::Blend(const FGCMS_CameraModeView& Other, float OtherWeight)
{
if (OtherWeight <= 0.0f)
{
return;
}
else if (OtherWeight >= 1.0f)
{
*this = Other;
return;
}
Location = FMath::Lerp(Location, Other.Location, OtherWeight);
const FRotator DeltaRotation = (Other.Rotation - Rotation).GetNormalized();
Rotation = Rotation + (OtherWeight * DeltaRotation);
const FRotator DeltaControlRotation = (Other.ControlRotation - ControlRotation).GetNormalized();
ControlRotation = ControlRotation + (OtherWeight * DeltaControlRotation);
SprintArmSocketOffset = FMath::Lerp(SprintArmSocketOffset, Other.SprintArmSocketOffset, OtherWeight);
SprintArmTargetOffset = FMath::Lerp(SprintArmTargetOffset, Other.SprintArmTargetOffset, OtherWeight);
SprintArmLength = FMath::Lerp(SprintArmLength, Other.SprintArmLength, OtherWeight);
FieldOfView = FMath::Lerp(FieldOfView, Other.FieldOfView, OtherWeight);
}
//////////////////////////////////////////////////////////////////////////
// UGCMS_CameraMode
//////////////////////////////////////////////////////////////////////////
UGCMS_CameraMode::UGCMS_CameraMode()
{
FieldOfView = 80.0f;
ViewPitchMin = -89.0f;
ViewPitchMax = 89.0f;
BlendTime = 0.5f;
BlendFunction = EGCMS_CameraModeBlendFunction::EaseOut;
BlendExponent = 4.0f;
BlendAlpha = 1.0f;
BlendWeight = 1.0f;
ActiveTime = 0.0f;
MaxActiveTime = 0.0f;
}
UWorld* UGCMS_CameraMode::GetWorld() const
{
return HasAnyFlags(RF_ClassDefaultObject) ? nullptr : GetOuter()->GetWorld();
}
AActor* UGCMS_CameraMode::GetTargetActor() const
{
if (UGCMS_CameraSystemComponent* Component = Cast<UGCMS_CameraSystemComponent>(GetOuter()))
{
return Component->GetOwner();
}
return nullptr;
}
FVector UGCMS_CameraMode::GetPivotLocation_Implementation() 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();
}
FRotator UGCMS_CameraMode::GetPivotRotation_Implementation() const
{
const AActor* TargetActor = GetTargetActor();
check(TargetActor);
if (const APawn* TargetPawn = Cast<APawn>(TargetActor))
{
return TargetPawn->GetViewRotation();
}
return TargetActor->GetActorRotation();
}
void UGCMS_CameraMode::UpdateCameraMode(float DeltaTime)
{
ActiveTime += DeltaTime;
if (MaxActiveTime > 0 && ActiveTime >= MaxActiveTime)
{
if (UGCMS_CameraSystemComponent* Component = Cast<UGCMS_CameraSystemComponent>(GetOuter()))
{
Component->PushDefaultCameraMode();
}
}
UpdateView(DeltaTime);
UpdateBlending(DeltaTime);
}
void UGCMS_CameraMode::UpdateView(float DeltaTime)
{
FVector PivotLocation = GetPivotLocation();
FRotator PivotRotation = GetPivotRotation();
PivotRotation.Pitch = FMath::ClampAngle(PivotRotation.Pitch, ViewPitchMin, ViewPitchMax);
OnUpdateView(DeltaTime, PivotLocation, PivotRotation);
}
void UGCMS_CameraMode::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 EGCMS_CameraModeBlendFunction::Linear:
BlendAlpha = BlendWeight;
break;
case EGCMS_CameraModeBlendFunction::EaseIn:
BlendAlpha = FMath::InterpEaseIn(0.0f, 1.0f, BlendWeight, InvExponent);
break;
case EGCMS_CameraModeBlendFunction::EaseOut:
BlendAlpha = FMath::InterpEaseOut(0.0f, 1.0f, BlendWeight, InvExponent);
break;
case EGCMS_CameraModeBlendFunction::EaseInOut:
BlendAlpha = FMath::InterpEaseInOut(0.0f, 1.0f, BlendWeight, InvExponent);
break;
default:
checkf(false, TEXT("SetBlendWeight: Invalid BlendFunction [%d]\n"), (uint8)BlendFunction);
break;
}
}
UCameraComponent* UGCMS_CameraMode::GetAssociatedCamera() const
{
if (UGCMS_CameraSystemComponent* Component = Cast<UGCMS_CameraSystemComponent>(GetOuter()))
{
return Component->GetAssociatedCamera();
}
return nullptr;
}
USpringArmComponent* UGCMS_CameraMode::GetAssociatedSprintArm() const
{
if (UGCMS_CameraSystemComponent* Component = Cast<UGCMS_CameraSystemComponent>(GetOuter()))
{
return Component->GetAssociatedSprintArm();
}
return nullptr;
}
void UGCMS_CameraMode::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 EGCMS_CameraModeBlendFunction::Linear:
BlendWeight = BlendAlpha;
break;
case EGCMS_CameraModeBlendFunction::EaseIn:
BlendWeight = FMath::InterpEaseIn(0.0f, 1.0f, BlendAlpha, Exponent);
break;
case EGCMS_CameraModeBlendFunction::EaseOut:
BlendWeight = FMath::InterpEaseOut(0.0f, 1.0f, BlendAlpha, Exponent);
break;
case EGCMS_CameraModeBlendFunction::EaseInOut:
BlendWeight = FMath::InterpEaseInOut(0.0f, 1.0f, BlendAlpha, Exponent);
break;
default:
checkf(false, TEXT("UpdateBlending: Invalid BlendFunction [%d]\n"), (uint8)BlendFunction);
break;
}
}
void UGCMS_CameraMode::OnUpdateView_Implementation(float DeltaTime, FVector PivotLocation, FRotator PivotRotation)
{
View.Location = PivotLocation;
View.Rotation = PivotRotation;
View.ControlRotation = View.Rotation;
View.FieldOfView = FieldOfView;
}
void UGCMS_CameraMode::DrawDebug(UCanvas* Canvas) const
{
check(Canvas);
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;
DisplayDebugManager.SetDrawColor(FColor::White);
DisplayDebugManager.DrawString(FString::Printf(TEXT(" GMS_CameraMode: %s (%f)"), *GetName(), BlendWeight));
}

View File

@@ -0,0 +1,257 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GCMS_CameraModeStack.h"
#include "Engine/Canvas.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraModeStack)
//////////////////////////////////////////////////////////////////////////
// UGCMS_CameraModeStack
//////////////////////////////////////////////////////////////////////////
UGCMS_CameraModeStack::UGCMS_CameraModeStack()
{
bIsActive = true;
}
void UGCMS_CameraModeStack::ActivateStack()
{
if (!bIsActive)
{
bIsActive = true;
// Notify camera modes that they are being activated.
for (UGCMS_CameraMode* CameraMode : CameraModeStack)
{
check(CameraMode);
CameraMode->OnActivation();
}
}
}
void UGCMS_CameraModeStack::DeactivateStack()
{
if (bIsActive)
{
bIsActive = false;
// Notify camera modes that they are being deactivated.
for (UGCMS_CameraMode* CameraMode : CameraModeStack)
{
check(CameraMode);
CameraMode->OnDeactivation();
}
}
}
void UGCMS_CameraModeStack::PushCameraMode(TSubclassOf<UGCMS_CameraMode> CameraModeClass)
{
if (!CameraModeClass)
{
return;
}
// get camera from pool.
UGCMS_CameraMode* NewCameraMode = GetCameraModeInstance(CameraModeClass);
check(NewCameraMode);
int32 StackSize = CameraModeStack.Num();
if ((StackSize > 0) && (CameraModeStack[0] == NewCameraMode))
{
// Already top of stack.
return;
}
// See if it's already in the stack and remove it.
// Figure out how much it was contributing to the stack.
int32 ExistingStackIndex = INDEX_NONE;
float ExistingStackContribution = 1.0f;
for (int32 StackIndex = 0; StackIndex < StackSize; ++StackIndex)
{
if (CameraModeStack[StackIndex] == NewCameraMode)
{
ExistingStackIndex = StackIndex;
ExistingStackContribution *= NewCameraMode->GetBlendWeight();
break;
}
else
{
ExistingStackContribution *= (1.0f - CameraModeStack[StackIndex]->GetBlendWeight());
}
}
// existing in stack, remove it before add.
if (ExistingStackIndex != INDEX_NONE)
{
CameraModeStack.RemoveAt(ExistingStackIndex);
StackSize--;
}
else
{
ExistingStackContribution = 0.0f;
}
// Decide what initial weight to start with.
const bool bShouldBlend = ((NewCameraMode->GetBlendTime() > 0.0f) && (StackSize > 0));
const float BlendWeight = (bShouldBlend ? ExistingStackContribution : 1.0f);
NewCameraMode->SetBlendWeight(BlendWeight);
// Add new entry to top of stack.
CameraModeStack.Insert(NewCameraMode, 0);
// Make sure stack bottom is always weighted 100%.
CameraModeStack.Last()->SetBlendWeight(1.0f);
// Let the camera mode know if it's being added to the stack.
if (ExistingStackIndex == INDEX_NONE)
{
NewCameraMode->OnActivation();
}
}
void UGCMS_CameraModeStack::PopCameraMode(TSubclassOf<UGCMS_CameraMode> CameraModeClass)
{
}
bool UGCMS_CameraModeStack::EvaluateStack(float DeltaTime, FGCMS_CameraModeView& OutCameraModeView)
{
if (!bIsActive)
{
return false;
}
//Update camera modes.
UpdateStack(DeltaTime);
//Blend values from camera modes.
BlendStack(OutCameraModeView);
return true;
}
UGCMS_CameraMode* UGCMS_CameraModeStack::GetCameraModeInstance(TSubclassOf<UGCMS_CameraMode> CameraModeClass)
{
check(CameraModeClass);
// First see if we already created one.
for (UGCMS_CameraMode* CameraMode : CameraModeInstances)
{
if ((CameraMode != nullptr) && (CameraMode->GetClass() == CameraModeClass))
{
return CameraMode;
}
}
// Not found, so we need to create it.
UGCMS_CameraMode* NewCameraMode = NewObject<UGCMS_CameraMode>(GetOuter(), CameraModeClass, NAME_None, RF_NoFlags);
check(NewCameraMode);
CameraModeInstances.Add(NewCameraMode);
return NewCameraMode;
}
void UGCMS_CameraModeStack::UpdateStack(float DeltaTime)
{
const int32 StackSize = CameraModeStack.Num();
if (StackSize <= 0)
{
return;
}
int32 RemoveCount = 0;
int32 RemoveIndex = INDEX_NONE;
for (int32 StackIndex = 0; StackIndex < StackSize; ++StackIndex)
{
UGCMS_CameraMode* CameraMode = CameraModeStack[StackIndex];
check(CameraMode);
CameraMode->UpdateCameraMode(DeltaTime);
if (CameraMode->GetBlendWeight() >= 1.0f)
{
// Everything below this mode is now irrelevant and can be removed.
RemoveIndex = (StackIndex + 1);
RemoveCount = (StackSize - RemoveIndex);
break;
}
}
if (RemoveCount > 0)
{
// Let the camera modes know they being removed from the stack.
for (int32 StackIndex = RemoveIndex; StackIndex < StackSize; ++StackIndex)
{
UGCMS_CameraMode* CameraMode = CameraModeStack[StackIndex];
check(CameraMode);
CameraMode->OnDeactivation();
}
CameraModeStack.RemoveAt(RemoveIndex, RemoveCount);
}
}
void UGCMS_CameraModeStack::BlendStack(FGCMS_CameraModeView& OutCameraModeView) const
{
const int32 StackSize = CameraModeStack.Num();
if (StackSize <= 0)
{
return;
}
// Start at the bottom and blend up the stack
const UGCMS_CameraMode* CameraMode = CameraModeStack[StackSize - 1];
check(CameraMode);
OutCameraModeView = CameraMode->GetCameraModeView();
for (int32 StackIndex = (StackSize - 2); StackIndex >= 0; --StackIndex)
{
CameraMode = CameraModeStack[StackIndex];
check(CameraMode);
OutCameraModeView.Blend(CameraMode->GetCameraModeView(), CameraMode->GetBlendWeight());
}
}
void UGCMS_CameraModeStack::DrawDebug(UCanvas* Canvas) const
{
check(Canvas);
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;
DisplayDebugManager.SetDrawColor(FColor::Green);
DisplayDebugManager.DrawString(FString(TEXT(" --- Camera Modes (Begin) ---")));
for (const UGCMS_CameraMode* CameraMode : CameraModeStack)
{
check(CameraMode);
CameraMode->DrawDebug(Canvas);
}
DisplayDebugManager.SetDrawColor(FColor::Green);
DisplayDebugManager.DrawString(FString::Printf(TEXT(" --- Camera Modes (End) ---")));
}
void UGCMS_CameraModeStack::GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const
{
if (CameraModeStack.Num() == 0)
{
OutWeightOfTopLayer = 1.0f;
OutTagOfTopLayer = FGameplayTag();
return;
}
else
{
UGCMS_CameraMode* TopEntry = CameraModeStack.Last();
check(TopEntry);
OutWeightOfTopLayer = TopEntry->GetBlendWeight();
OutTagOfTopLayer = TopEntry->GetCameraTypeTag();
}
}

View File

@@ -0,0 +1,100 @@
// // Copyright 2025 https://yuewu.dev/en All Rights Reserved.
//
// #include "GCMS_CameraMode_ThirdPerson.h"
//
// #include "GCMS_CameraAssistInterface.h"
// #include "GCMS_CameraMode.h"
// #include "Components/PrimitiveComponent.h"
// #include "GCMS_CameraPenetrationAvoidanceFeeler.h"
// #include "Curves/CurveVector.h"
// #include "Engine/Canvas.h"
// #include "GameFramework/Controller.h"
// #include "GameFramework/Character.h"
// #include "Math/RotationMatrix.h"
//
// #include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraMode_ThirdPerson)
//
//
// UGCMS_CameraMode_ThirdPerson::UGCMS_CameraMode_ThirdPerson()
// {
// TargetOffsetCurve = nullptr;
// }
//
// void UGCMS_CameraMode_ThirdPerson::UpdateView_Implementation(float DeltaTime)
// {
// UpdateForTarget(DeltaTime);
// UpdateCrouchOffset(DeltaTime);
//
// FVector PivotLocation = GetPivotLocation() + CurrentCrouchOffset;
// FRotator PivotRotation = GetPivotRotation();
//
// PivotRotation.Pitch = FMath::ClampAngle(PivotRotation.Pitch, ViewPitchMin, ViewPitchMax);
//
// View.Location = PivotLocation;
// View.Rotation = PivotRotation;
// View.ControlRotation = View.Rotation;
// View.FieldOfView = FieldOfView;
//
// // Apply third person offset using pitch.
// if (!bUseRuntimeFloatCurves)
// {
// if (TargetOffsetCurve)
// {
// const FVector TargetOffset = TargetOffsetCurve->GetVectorValue(PivotRotation.Pitch);
// View.Location = PivotLocation + PivotRotation.RotateVector(TargetOffset);
// }
// }
// else
// {
// FVector TargetOffset(0.0f);
//
// TargetOffset.X = TargetOffsetX.GetRichCurveConst()->Eval(PivotRotation.Pitch);
// TargetOffset.Y = TargetOffsetY.GetRichCurveConst()->Eval(PivotRotation.Pitch);
// TargetOffset.Z = TargetOffsetZ.GetRichCurveConst()->Eval(PivotRotation.Pitch);
//
// View.Location = PivotLocation + PivotRotation.RotateVector(TargetOffset);
// }
//
// // Adjust final desired camera location to prevent any penetration
// UpdatePreventPenetration(DeltaTime);
// }
//
// void UGCMS_CameraMode_ThirdPerson::UpdateForTarget(float DeltaTime)
// {
// if (const ACharacter* TargetCharacter = Cast<ACharacter>(GetTargetActor()))
// {
// if (TargetCharacter->bIsCrouched)
// {
// const ACharacter* TargetCharacterCDO = TargetCharacter->GetClass()->GetDefaultObject<ACharacter>();
// const float CrouchedHeightAdjustment = TargetCharacterCDO->CrouchedEyeHeight - TargetCharacterCDO->BaseEyeHeight;
//
// SetTargetCrouchOffset(FVector(0.f, 0.f, CrouchedHeightAdjustment));
//
// return;
// }
// }
//
// SetTargetCrouchOffset(FVector::ZeroVector);
// }
//
// void UGCMS_CameraMode_ThirdPerson::SetTargetCrouchOffset(FVector NewTargetOffset)
// {
// CrouchOffsetBlendPct = 0.0f;
// InitialCrouchOffset = CurrentCrouchOffset;
// TargetCrouchOffset = NewTargetOffset;
// }
//
//
// void UGCMS_CameraMode_ThirdPerson::UpdateCrouchOffset(float DeltaTime)
// {
// if (CrouchOffsetBlendPct < 1.0f)
// {
// CrouchOffsetBlendPct = FMath::Min(CrouchOffsetBlendPct + DeltaTime * CrouchOffsetBlendMultiplier, 1.0f);
// CurrentCrouchOffset = FMath::InterpEaseInOut(InitialCrouchOffset, TargetCrouchOffset, CrouchOffsetBlendPct, 1.0f);
// }
// else
// {
// CurrentCrouchOffset = TargetCrouchOffset;
// CrouchOffsetBlendPct = 1.0f;
// }
// }

View File

@@ -0,0 +1,290 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GCMS_CameraMode_WithPenetrationAvoidance.h"
#include "Engine/Canvas.h"
#include "GameFramework/Pawn.h"
#include "Components/PrimitiveComponent.h"
#include "GameFramework/Controller.h"
#include "Engine/HitResult.h"
#include "GCMS_CameraAssistInterface.h"
#include "GameFramework/CameraBlockingVolume.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraMode_WithPenetrationAvoidance)
namespace GMS_CameraMode_WithPenetrationAvoidance_Statics
{
static const FName NAME_IgnoreCameraCollision = TEXT("IgnoreCameraCollision");
}
UGCMS_CameraMode_WithPenetrationAvoidance::UGCMS_CameraMode_WithPenetrationAvoidance()
{
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +00.0f, 0.0f), 1.00f, 1.00f, 14.f, 0));
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +16.0f, 0.0f), 0.75f, 0.75f, 00.f, 3));
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, -16.0f, 0.0f), 0.75f, 0.75f, 00.f, 3));
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, +32.0f, 0.0f), 0.50f, 0.50f, 00.f, 5));
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+00.0f, -32.0f, 0.0f), 0.50f, 0.50f, 00.f, 5));
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(+20.0f, +00.0f, 0.0f), 1.00f, 1.00f, 00.f, 4));
PenetrationAvoidanceFeelers.Add(FGCMS_CameraPenetrationAvoidanceFeeler(FRotator(-20.0f, +00.0f, 0.0f), 0.50f, 0.50f, 00.f, 4));
}
void UGCMS_CameraMode_WithPenetrationAvoidance::UpdatePreventPenetration(float DeltaTime)
{
if (!bPreventPenetration)
{
return;
}
AActor* TargetActor = GetTargetActor();
APawn* TargetPawn = Cast<APawn>(TargetActor);
AController* TargetController = TargetPawn ? TargetPawn->GetController() : nullptr;
IGCMS_CameraAssistInterface* TargetControllerAssist = Cast<IGCMS_CameraAssistInterface>(TargetController);
IGCMS_CameraAssistInterface* TargetActorAssist = Cast<IGCMS_CameraAssistInterface>(TargetActor);
TOptional<AActor*> OptionalPPTarget = TargetActorAssist ? TargetActorAssist->GetCameraPreventPenetrationTarget() : TOptional<AActor*>();
AActor* PPActor = OptionalPPTarget.IsSet() ? OptionalPPTarget.GetValue() : TargetActor;
IGCMS_CameraAssistInterface* PPActorAssist = OptionalPPTarget.IsSet() ? Cast<IGCMS_CameraAssistInterface>(PPActor) : nullptr;
const UPrimitiveComponent* PPActorRootComponent = Cast<UPrimitiveComponent>(PPActor->GetRootComponent());
if (PPActorRootComponent)
{
// Attempt at picking SafeLocation automatically, so we reduce camera translation when aiming.
// Our camera is our reticle, so we want to preserve our aim and keep that as steady and smooth as possible.
// Pick closest point on capsule to our aim line.
FVector ClosestPointOnLineToCapsuleCenter;
FVector SafeLocation = PPActor->GetActorLocation();
FMath::PointDistToLine(SafeLocation, View.Rotation.Vector(), View.Location, ClosestPointOnLineToCapsuleCenter);
// Adjust Safe distance height to be same as aim line, but within capsule.
float const PushInDistance = PenetrationAvoidanceFeelers[0].Extent + CollisionPushOutDistance;
float const MaxHalfHeight = PPActor->GetSimpleCollisionHalfHeight() - PushInDistance;
SafeLocation.Z = FMath::Clamp(ClosestPointOnLineToCapsuleCenter.Z, SafeLocation.Z - MaxHalfHeight, SafeLocation.Z + MaxHalfHeight);
float DistanceSqr;
PPActorRootComponent->GetSquaredDistanceToCollision(ClosestPointOnLineToCapsuleCenter, DistanceSqr, SafeLocation);
// Push back inside capsule to avoid initial penetration when doing line checks.
if (PenetrationAvoidanceFeelers.Num() > 0)
{
SafeLocation += (SafeLocation - ClosestPointOnLineToCapsuleCenter).GetSafeNormal() * PushInDistance;
}
// Then aim line to desired camera position
bool const bSingleRayPenetrationCheck = !bDoPredictiveAvoidance;
PreventCameraPenetration(bSingleRayPenetrationCheck, DeltaTime, PPActor, SafeLocation, View.Location, AimLineToDesiredPosBlockedPct);
IGCMS_CameraAssistInterface* AssistArray[] = {TargetControllerAssist, TargetActorAssist, PPActorAssist};
if (AimLineToDesiredPosBlockedPct < ReportPenetrationPercent)
{
for (IGCMS_CameraAssistInterface* Assist : AssistArray)
{
if (Assist)
{
// camera is too close, tell the assists
Assist->OnCameraPenetratingTarget();
}
}
}
}
}
void UGCMS_CameraMode_WithPenetrationAvoidance::PreventCameraPenetration(bool bSingleRayOnly, const float& DeltaTime, const AActor* ViewTarget, FVector const& SafeLoc, FVector& CameraLoc,
float& DistBlockedPct)
{
#if ENABLE_DRAW_DEBUG
DebugActorsHitDuringCameraPenetration.Reset();
#endif
float HardBlockedPct = DistBlockedPct;
float SoftBlockedPct = DistBlockedPct;
FVector BaseRay = CameraLoc - SafeLoc;
FRotationMatrix BaseRayMatrix(BaseRay.Rotation());
FVector BaseRayLocalUp, BaseRayLocalFwd, BaseRayLocalRight;
BaseRayMatrix.GetScaledAxes(BaseRayLocalFwd, BaseRayLocalRight, BaseRayLocalUp);
float DistBlockedPctThisFrame = 1.f;
int32 const NumRaysToShoot = bSingleRayOnly ? FMath::Min(1, PenetrationAvoidanceFeelers.Num()) : PenetrationAvoidanceFeelers.Num();
FCollisionQueryParams SphereParams(SCENE_QUERY_STAT(CameraPen), false, nullptr/*PlayerCamera*/);
SphereParams.AddIgnoredActor(ViewTarget);
//TODO ILyraCameraTarget.GetIgnoredActorsForCameraPentration();
//if (IgnoreActorForCameraPenetration)
//{
// SphereParams.AddIgnoredActor(IgnoreActorForCameraPenetration);
//}
FCollisionShape SphereShape = FCollisionShape::MakeSphere(0.f);
UWorld* World = GetWorld();
for (int32 RayIdx = 0; RayIdx < NumRaysToShoot; ++RayIdx)
{
FGCMS_CameraPenetrationAvoidanceFeeler& Feeler = PenetrationAvoidanceFeelers[RayIdx];
if (Feeler.FramesUntilNextTrace <= 0)
{
// calc ray target
FVector RayTarget;
{
FVector RotatedRay = BaseRay.RotateAngleAxis(Feeler.AdjustmentRot.Yaw, BaseRayLocalUp);
RotatedRay = RotatedRay.RotateAngleAxis(Feeler.AdjustmentRot.Pitch, BaseRayLocalRight);
RayTarget = SafeLoc + RotatedRay;
}
// cast for world and pawn hits separately. this is so we can safely ignore the
// camera's target pawn
SphereShape.Sphere.Radius = Feeler.Extent;
ECollisionChannel TraceChannel = ECC_Camera; //(Feeler.PawnWeight > 0.f) ? ECC_Pawn : ECC_Camera;
// do multi-line check to make sure the hits we throw out aren't
// masking real hits behind (these are important rays).
// MT-> passing camera as actor so that camerablockingvolumes know when it's the camera doing traces
FHitResult Hit;
const bool bHit = World->SweepSingleByChannel(Hit, SafeLoc, RayTarget, FQuat::Identity, TraceChannel, SphereShape, SphereParams);
#if ENABLE_DRAW_DEBUG
if (World->TimeSince(LastDrawDebugTime) < 1.f)
{
DrawDebugSphere(World, SafeLoc, SphereShape.Sphere.Radius, 8, FColor::Red);
DrawDebugSphere(World, bHit ? Hit.Location : RayTarget, SphereShape.Sphere.Radius, 8, FColor::Red);
DrawDebugLine(World, SafeLoc, bHit ? Hit.Location : RayTarget, FColor::Red);
}
#endif // ENABLE_DRAW_DEBUG
Feeler.FramesUntilNextTrace = Feeler.TraceInterval;
const AActor* HitActor = Hit.GetActor();
if (bHit && HitActor)
{
bool bIgnoreHit = false;
if (HitActor->ActorHasTag(GMS_CameraMode_WithPenetrationAvoidance_Statics::NAME_IgnoreCameraCollision))
{
bIgnoreHit = true;
SphereParams.AddIgnoredActor(HitActor);
}
// Ignore CameraBlockingVolume hits that occur in front of the ViewTarget.
if (!bIgnoreHit && HitActor->IsA<ACameraBlockingVolume>())
{
const FVector ViewTargetForwardXY = ViewTarget->GetActorForwardVector().GetSafeNormal2D();
const FVector ViewTargetLocation = ViewTarget->GetActorLocation();
const FVector HitOffset = Hit.Location - ViewTargetLocation;
const FVector HitDirectionXY = HitOffset.GetSafeNormal2D();
const float DotHitDirection = FVector::DotProduct(ViewTargetForwardXY, HitDirectionXY);
if (DotHitDirection > 0.0f)
{
bIgnoreHit = true;
// Ignore this CameraBlockingVolume on the remaining sweeps.
SphereParams.AddIgnoredActor(HitActor);
}
else
{
#if ENABLE_DRAW_DEBUG
DebugActorsHitDuringCameraPenetration.AddUnique(TObjectPtr<const AActor>(HitActor));
#endif
}
}
if (!bIgnoreHit)
{
float const Weight = Cast<APawn>(Hit.GetActor()) ? Feeler.PawnWeight : Feeler.WorldWeight;
float NewBlockPct = Hit.Time;
NewBlockPct += (1.f - NewBlockPct) * (1.f - Weight);
// Recompute blocked pct taking into account pushout distance.
NewBlockPct = ((Hit.Location - SafeLoc).Size() - CollisionPushOutDistance) / (RayTarget - SafeLoc).Size();
DistBlockedPctThisFrame = FMath::Min(NewBlockPct, DistBlockedPctThisFrame);
// This feeler got a hit, so do another trace next frame
Feeler.FramesUntilNextTrace = 0;
#if ENABLE_DRAW_DEBUG
DebugActorsHitDuringCameraPenetration.AddUnique(TObjectPtr<const AActor>(HitActor));
#endif
}
}
if (RayIdx == 0)
{
// don't interpolate toward this one, snap to it
// assumes ray 0 is the center/main ray
HardBlockedPct = DistBlockedPctThisFrame;
}
else
{
SoftBlockedPct = DistBlockedPctThisFrame;
}
}
else
{
--Feeler.FramesUntilNextTrace;
}
}
if (bResetInterpolation)
{
DistBlockedPct = DistBlockedPctThisFrame;
}
else if (DistBlockedPct < DistBlockedPctThisFrame)
{
// interpolate smoothly out
if (PenetrationBlendOutTime > DeltaTime)
{
DistBlockedPct = DistBlockedPct + DeltaTime / PenetrationBlendOutTime * (DistBlockedPctThisFrame - DistBlockedPct);
}
else
{
DistBlockedPct = DistBlockedPctThisFrame;
}
}
else
{
if (DistBlockedPct > HardBlockedPct)
{
DistBlockedPct = HardBlockedPct;
}
else if (DistBlockedPct > SoftBlockedPct)
{
// interpolate smoothly in
if (PenetrationBlendInTime > DeltaTime)
{
DistBlockedPct = DistBlockedPct - DeltaTime / PenetrationBlendInTime * (DistBlockedPct - SoftBlockedPct);
}
else
{
DistBlockedPct = SoftBlockedPct;
}
}
}
DistBlockedPct = FMath::Clamp<float>(DistBlockedPct, 0.f, 1.f);
if (DistBlockedPct < (1.f - ZERO_ANIMWEIGHT_THRESH))
{
CameraLoc = SafeLoc + (CameraLoc - SafeLoc) * DistBlockedPct;
}
}
void UGCMS_CameraMode_WithPenetrationAvoidance::DrawDebug(UCanvas* Canvas) const
{
Super::DrawDebug(Canvas);
#if ENABLE_DRAW_DEBUG
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;
for (int i = 0; i < DebugActorsHitDuringCameraPenetration.Num(); i++)
{
DisplayDebugManager.DrawString(
FString::Printf(TEXT("HitActorDuringPenetration[%d]: %s")
, i
, *DebugActorsHitDuringCameraPenetration[i]->GetName()));
}
LastDrawDebugTime = GetWorld()->GetTimeSeconds();
#endif
}

View File

@@ -0,0 +1,6 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GCMS_CameraPenetrationAvoidanceFeeler.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraPenetrationAvoidanceFeeler)

View File

@@ -0,0 +1,188 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GCMS_CameraSystemComponent.h"
#include "GameFramework/HUD.h"
#include "DisplayDebugHelpers.h"
#include "Engine/Canvas.h"
#include "Engine/Engine.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/PlayerController.h"
#include "GCMS_CameraMode.h"
#include "GCMS_CameraModeStack.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GCMS_CameraSystemComponent)
UGCMS_CameraSystemComponent::UGCMS_CameraSystemComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
CameraModeStack = nullptr;
PrimaryComponentTick.bCanEverTick = true;
FieldOfViewOffset = 0.0f;
}
void UGCMS_CameraSystemComponent::OnShowDebugInfo(AHUD* HUD, UCanvas* Canvas, const FDebugDisplayInfo& DisplayInfo, float& YL, float& YPos)
{
if (DisplayInfo.IsDisplayOn(TEXT("CAMERA")))
{
if (const UGCMS_CameraSystemComponent* CameraComponent = GetCameraSystemComponent(HUD->GetCurrentDebugTargetActor()))
{
CameraComponent->DrawDebug(Canvas);
}
}
}
void UGCMS_CameraSystemComponent::OnRegister()
{
Super::OnRegister();
if (!CameraModeStack)
{
CameraModeStack = NewObject<UGCMS_CameraModeStack>(this);
check(CameraModeStack);
}
}
void UGCMS_CameraSystemComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (CameraModeStack && CameraModeStack->IsStackActivate() && AssociatedCameraComponent && AssociatedSprintArmComponent)
{
UpdateCameraModes();
FGCMS_CameraModeView 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);
}
}
AssociatedCameraComponent->FieldOfView = CameraModeView.FieldOfView;
AssociatedSprintArmComponent->TargetArmLength = CameraModeView.SprintArmLength;
AssociatedSprintArmComponent->SocketOffset = CameraModeView.SprintArmSocketOffset;
AssociatedSprintArmComponent->TargetOffset = CameraModeView.SprintArmTargetOffset;
}
}
UCameraComponent* UGCMS_CameraSystemComponent::GetAssociatedCamera() const
{
return AssociatedCameraComponent;
}
USpringArmComponent* UGCMS_CameraSystemComponent::GetAssociatedSprintArm() const
{
return AssociatedSprintArmComponent;
}
void UGCMS_CameraSystemComponent::Activate(bool bReset)
{
Super::Activate(bReset);
if (CameraModeStack)
{
if (IsActive())
{
CameraModeStack->ActivateStack();
}
else
{
CameraModeStack->DeactivateStack();
}
}
}
void UGCMS_CameraSystemComponent::Deactivate()
{
Super::Deactivate();
if (CameraModeStack)
{
CameraModeStack->DeactivateStack();
}
}
void UGCMS_CameraSystemComponent::UpdateCameraModes()
{
check(CameraModeStack);
if (CameraModeStack->IsStackActivate())
{
if (DetermineCameraModeDelegate.IsBound())
{
if (const TSubclassOf<UGCMS_CameraMode> CameraMode = DetermineCameraModeDelegate.Execute())
{
CameraModeStack->PushCameraMode(CameraMode);
}
}
}
}
void UGCMS_CameraSystemComponent::PushCameraMode(TSubclassOf<UGCMS_CameraMode> NewCameraMode)
{
if (CameraModeStack->IsStackActivate())
{
if (NewCameraMode)
{
CameraModeStack->PushCameraMode(NewCameraMode);
}
}
}
void UGCMS_CameraSystemComponent::PushDefaultCameraMode()
{
if (CameraModeStack->IsStackActivate())
{
if (DefaultCameraMode)
{
CameraModeStack->PushCameraMode(DefaultCameraMode);
}
}
}
void UGCMS_CameraSystemComponent::Initialize(UCameraComponent* NewCameraComponent, USpringArmComponent* NewSpringArmComponent)
{
if (!CameraModeStack)
{
CameraModeStack = NewObject<UGCMS_CameraModeStack>(this);
check(CameraModeStack);
}
AssociatedCameraComponent = NewCameraComponent;
AssociatedSprintArmComponent = NewSpringArmComponent;
}
void UGCMS_CameraSystemComponent::DrawDebug(UCanvas* Canvas) const
{
check(Canvas);
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;
DisplayDebugManager.SetFont(GEngine->GetSmallFont());
DisplayDebugManager.SetDrawColor(FColor::Yellow);
DisplayDebugManager.DrawString(FString::Printf(TEXT("GCMS_CameraSystemComponent: %s"), *GetNameSafe(GetTargetActor())));
DisplayDebugManager.SetDrawColor(FColor::White);
DisplayDebugManager.DrawString(FString::Printf(TEXT(" Location: %s"), *AssociatedCameraComponent->GetComponentLocation().ToCompactString()));
DisplayDebugManager.DrawString(FString::Printf(TEXT(" Rotation: %s"), *AssociatedCameraComponent->GetComponentRotation().ToCompactString()));
DisplayDebugManager.DrawString(FString::Printf(TEXT(" SprintArmLength: %f"), AssociatedSprintArmComponent->TargetArmLength));
DisplayDebugManager.DrawString(FString::Printf(TEXT(" SprintArmSocketOffset: %s"), *AssociatedSprintArmComponent->SocketOffset.ToCompactString()));
DisplayDebugManager.DrawString(FString::Printf(TEXT(" SprintArmTargetOffset: %s"), *AssociatedSprintArmComponent->TargetOffset.ToCompactString()));
DisplayDebugManager.DrawString(FString::Printf(TEXT(" FOV: %f"), AssociatedCameraComponent->FieldOfView));
check(CameraModeStack);
CameraModeStack->DrawDebug(Canvas);
}
void UGCMS_CameraSystemComponent::GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const
{
check(CameraModeStack);
CameraModeStack->GetBlendInfo(/*out*/ OutWeightOfTopLayer, /*out*/ OutTagOfTopLayer);
}

View File

@@ -0,0 +1,31 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GenericCameraSystem.h"
#include "GameFramework/HUD.h"
#include "GCMS_CameraSystemComponent.h"
#define LOCTEXT_NAMESPACE "FGenericCameraSystemModule"
void FGenericCameraSystemModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
#if WITH_EDITOR
#if ENABLE_DRAW_DEBUG
if (!IsRunningDedicatedServer())
{
AHUD::OnShowDebugInfo.AddStatic(&UGCMS_CameraSystemComponent::OnShowDebugInfo);
}
#endif // ENABLE_DRAW_DEBUG
#endif
}
void FGenericCameraSystemModule::ShutdownModule()
{
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
// we call this function before unloading the module.
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FGenericCameraSystemModule, GenericCameraSystem)

View File

@@ -0,0 +1,41 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "GCMS_CameraAssistInterface.generated.h"
/** */
UINTERFACE(BlueprintType)
class UGCMS_CameraAssistInterface : public UInterface
{
GENERATED_BODY()
};
class GENERICCAMERASYSTEM_API IGCMS_CameraAssistInterface
{
GENERATED_BODY()
public:
/**
* Get the list of actors that we're allowing the camera to penetrate. Useful in 3rd person cameras
* when you need the following camera to ignore things like the a collection of view targets, the pawn,
* a vehicle..etc.
*/
virtual void GetIgnoredActorsForCameraPentration(TArray<const AActor*>& OutActorsAllowPenetration) const { }
/**
* The target actor to prevent penetration on. Normally, this is almost always the view target, which if
* unimplemented will remain true. However, sometimes the view target, isn't the same as the root actor
* you need to keep in frame.
*/
virtual TOptional<AActor*> GetCameraPreventPenetrationTarget() const
{
return TOptional<AActor*>();
}
/** Called if the camera penetrates the focal target. Useful if you want to hide the target actor when being overlapped. */
virtual void OnCameraPenetratingTarget() { }
};

View File

@@ -0,0 +1,235 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "Engine/World.h"
#include "GameplayTagContainer.h"
#include "GCMS_CameraMode.generated.h"
class USpringArmComponent;
class UCameraComponent;
class AActor;
class UCanvas;
/**
* EGCMS_CameraModeBlendFunction
*
* Blend function used for transitioning between camera modes.
*/
UENUM(BlueprintType)
enum class EGCMS_CameraModeBlendFunction : uint8
{
// Does a simple linear interpolation.
Linear,
// Immediately accelerates, but smoothly decelerates into the target. Ease amount controlled by the exponent.
EaseIn,
// Smoothly accelerates, but does not decelerate into the target. Ease amount controlled by the exponent.
EaseOut,
// Smoothly accelerates and decelerates. Ease amount controlled by the exponent.
EaseInOut,
COUNT UMETA(Hidden)
};
/**
* GCMS_CameraModeView
*
* View data produced by the camera mode that is used to blend camera modes.
*/
USTRUCT(BlueprintType)
struct GENERICCAMERASYSTEM_API FGCMS_CameraModeView
{
public:
GENERATED_BODY()
FGCMS_CameraModeView();
void Blend(const FGCMS_CameraModeView& Other, float OtherWeight);
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
FVector Location{ForceInit};
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
FRotator Rotation{ForceInit};
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
FVector SprintArmSocketOffset{ForceInit};
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
FVector SprintArmTargetOffset{ForceInit};
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
float SprintArmLength{ForceInit};
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
FRotator ControlRotation{ForceInit};
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GCMS")
float FieldOfView{ForceInit};
};
/**
* UGCMS_CameraMode
*
* Base class for all camera modes.
*/
UCLASS(Abstract, Blueprintable)
class GENERICCAMERASYSTEM_API UGCMS_CameraMode : public UObject
{
GENERATED_BODY()
public:
UGCMS_CameraMode();
virtual UWorld* GetWorld() const override;
/**
* @return Returns the target actor that owning camera is looking at. 返回所属相机当前跟随的目标Actor。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="CameraMode")
AActor* GetTargetActor() const;
const FGCMS_CameraModeView& GetCameraModeView() const { return View; }
// Called when this camera mode is activated on the camera mode stack.
virtual void OnActivation()
{
K2_OnActivation();
};
// Called when this camera mode is deactivated on the camera mode stack.
virtual void OnDeactivation()
{
K2_OnDeactivation();
};
void UpdateCameraMode(float DeltaTime);
float GetBlendTime() const { return BlendTime; }
float GetBlendWeight() const { return BlendWeight; }
void SetBlendWeight(float Weight);
FGameplayTag GetCameraTypeTag() const
{
return CameraTypeTag;
}
UFUNCTION(BlueprintCallable, BlueprintPure, Category="CameraMode")
UCameraComponent* GetAssociatedCamera() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category="CameraMode")
USpringArmComponent* GetAssociatedSprintArm() const;
virtual void DrawDebug(UCanvas* Canvas) const;
protected:
/**
* Called when this camera mode activated.
* 在此相机模式激活时调用。
*/
UFUNCTION(BlueprintImplementableEvent, Category="CameraMode", meta=(DisplayName="OnActivation"))
void K2_OnActivation();
/**
* Called when this camera mode deactivated.
* 在此相机模式禁用时调用。
*/
UFUNCTION(BlueprintImplementableEvent, Category="CameraMode", meta=(DisplayName="OnDeactivation"))
void K2_OnDeactivation();
/**
* Return the pivot location of this camera mode.
* @details Default will return Character's capsule location(Taking crouching state in count), or fallback to Pawn's ViewLocation or fallback to PawnLocation.
* 返回此相机模式的轴心位置。
* @细节默认实现是返回考虑到Character的蹲伏状态的胶囊体中心点或者回退到Pawn的ViewLocation或者回退到Pawn的Location。
*/
UFUNCTION(BlueprintNativeEvent, Category="CameraMode")
FVector GetPivotLocation() const;
/**
* Return the pivot rotation of this camera mode.
* @details Default will return TargetActor(Pawn)'s view rotation or fallback to actor rotation.
* 返回此相机模式的轴心旋转。
* @细节 默认会返回TargetActor(Pawn)的ViewRotation,或者回退到Actor的旋转。
*/
UFUNCTION(BlueprintNativeEvent, Category="CameraMode")
FRotator GetPivotRotation() const;
virtual void UpdateView(float DeltaTime);
virtual void UpdateBlending(float DeltaTime);
/**
* This is where you update View(CameraModeView)
* @param DeltaTime
* @param PivotLocation Location returned from GetPivotLocation
* @param PivotRotation Rotation returned from GetPivotRotation
*/
UFUNCTION(BlueprintNativeEvent, Category="CameraMode")
void OnUpdateView(float DeltaTime, FVector PivotLocation, FRotator PivotRotation);
virtual void OnUpdateView_Implementation(float DeltaTime, FVector PivotLocation, FRotator PivotRotation);
protected:
// A tag that can be queried by gameplay code that cares when a kind of camera mode is active
// without having to ask about a specific mode (e.g., when aiming downsights to get more accuracy)
UPROPERTY(EditDefaultsOnly, Category = "Blending")
FGameplayTag CameraTypeTag;
// View output produced by the camera mode.
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="View")
FGCMS_CameraModeView View;
// The horizontal field of view (in degrees).
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "View", Meta = (UIMin = "5.0", UIMax = "170", ClampMin = "5.0", ClampMax = "170.0"))
float FieldOfView;
// Minimum view pitch (in degrees).
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "View", Meta = (UIMin = "-89.9", UIMax = "89.9", ClampMin = "-89.9", ClampMax = "89.9"))
float ViewPitchMin;
// Maximum view pitch (in degrees).
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "View", Meta = (UIMin = "-89.9", UIMax = "89.9", ClampMin = "-89.9", ClampMax = "89.9"))
float ViewPitchMax;
/**
* How long it takes to blend in this mode.
* 此相机模式的混入时间。
*/
UPROPERTY(EditDefaultsOnly, Category = "Blending")
float BlendTime;
/**
* Function used for blending.
* 用于混合的方式。
*/
UPROPERTY(EditDefaultsOnly, Category = "Blending")
EGCMS_CameraModeBlendFunction BlendFunction;
// Exponent used by blend functions to control the shape of the curve.
UPROPERTY(EditDefaultsOnly, Category = "Blending")
float BlendExponent;
// Linear blend alpha used to determine the blend weight.
UPROPERTY(VisibleInstanceOnly, Category="Blending")
float BlendAlpha;
// Blend weight calculated using the blend alpha and function.
UPROPERTY(VisibleInstanceOnly, Category="Blending")
float BlendWeight;
UPROPERTY(EditDefaultsOnly, Category = "Duration")
float ActiveTime;
/**
* The max active time of this camera mode, When active time reach this value, this camera mode will auto popup.
* 此相机模式的最大激活时间,当当前激活时间到达此时长,会返回到默认相机模式。
*/
UPROPERTY(EditDefaultsOnly, Category = "Duraction")
float MaxActiveTime;
protected:
/** If true, skips all interpolation and puts camera in ideal location. Automatically set to false next frame. */
UPROPERTY(transient)
uint32 bResetInterpolation : 1;
};

View File

@@ -0,0 +1,63 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GCMS_CameraMode.h"
#include "UObject/Object.h"
#include "GCMS_CameraModeStack.generated.h"
/**
* UGCMS_CameraModeStack
*
* Stack used for blending camera modes.
*/
UCLASS()
class GENERICCAMERASYSTEM_API UGCMS_CameraModeStack : public UObject
{
GENERATED_BODY()
public:
UGCMS_CameraModeStack();
void ActivateStack();
void DeactivateStack();
bool IsStackActivate() const { return bIsActive; }
void PushCameraMode(TSubclassOf<UGCMS_CameraMode> CameraModeClass);
void PopCameraMode(TSubclassOf<UGCMS_CameraMode> CameraModeClass);
bool EvaluateStack(float DeltaTime, FGCMS_CameraModeView& OutCameraModeView);
void DrawDebug(UCanvas* Canvas) const;
// Gets the tag associated with the top layer and the blend weight of it
void GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const;
protected:
/**
* Get or create new camera mode.
*/
UGCMS_CameraMode* GetCameraModeInstance(TSubclassOf<UGCMS_CameraMode> CameraModeClass);
void UpdateStack(float DeltaTime);
void BlendStack(FGCMS_CameraModeView& OutCameraModeView) const;
protected:
bool bIsActive;
/**
* Cached camera mode instance pool.
*/
UPROPERTY()
TArray<TObjectPtr<UGCMS_CameraMode>> CameraModeInstances;
/**
* The list of active camera mode.
*/
UPROPERTY()
TArray<TObjectPtr<UGCMS_CameraMode>> CameraModeStack;
};

View File

@@ -0,0 +1,66 @@
// // Copyright 2025 https://yuewu.dev/en All Rights Reserved.
//
// #pragma once
//
// #include "Curves/CurveFloat.h"
// #include "DrawDebugHelpers.h"
// #include "GCMS_CameraMode_WithPenetrationAvoidance.h"
// #include "GCMS_CameraMode_ThirdPerson.generated.h"
//
// class UCurveVector;
//
// /**
// * UGCMS_CameraMode_ThirdPerson
// *
// * A basic third person camera mode.
// */
// UCLASS(Abstract, Blueprintable)
// class UGCMS_CameraMode_ThirdPerson : public UGCMS_CameraMode_WithPenetrationAvoidance
// {
// GENERATED_BODY()
//
// public:
// UGCMS_CameraMode_ThirdPerson();
//
// protected:
//
// virtual void UpdateView_Implementation(float DeltaTime) override;
//
// UFUNCTION(BlueprintCallable, Category="Third Person")
// void UpdateForTarget(float DeltaTime);
//
// protected:
// // Curve that defines local-space offsets from the target using the view pitch to evaluate the curve.
// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "!bUseRuntimeFloatCurves"))
// TObjectPtr<const UCurveVector> TargetOffsetCurve;
//
// // UE-103986: Live editing of RuntimeFloatCurves during PIE does not work (unlike curve assets).
// // Once that is resolved this will become the default and TargetOffsetCurve will be removed.
// UPROPERTY(EditDefaultsOnly, Category = "Third Person")
// bool bUseRuntimeFloatCurves;
//
// // time will be [-ViewPitchMin,ViewPitchMax]
// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "bUseRuntimeFloatCurves"))
// FRuntimeFloatCurve TargetOffsetX;
//
// // time will be [-ViewPitchMin,ViewPitchMax]
// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "bUseRuntimeFloatCurves"))
// FRuntimeFloatCurve TargetOffsetY;
//
// // time will be [-ViewPitchMin,ViewPitchMax]
// UPROPERTY(EditDefaultsOnly, Category = "Third Person", Meta = (EditCondition = "bUseRuntimeFloatCurves"))
// FRuntimeFloatCurve TargetOffsetZ;
//
// // Alters the speed that a crouch offset is blended in or out
// UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Third Person")
// float CrouchOffsetBlendMultiplier = 5.0f;
//
// protected:
// void SetTargetCrouchOffset(FVector NewTargetOffset);
// void UpdateCrouchOffset(float DeltaTime);
//
// FVector InitialCrouchOffset = FVector::ZeroVector;
// FVector TargetCrouchOffset = FVector::ZeroVector;
// float CrouchOffsetBlendPct = 1.0f;
// FVector CurrentCrouchOffset = FVector::ZeroVector;
// };

View File

@@ -0,0 +1,72 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "DrawDebugHelpers.h"
#include "GCMS_CameraMode.h"
#include "GCMS_CameraPenetrationAvoidanceFeeler.h"
#include "GCMS_CameraMode_WithPenetrationAvoidance.generated.h"
/**
*
*/
UCLASS(Abstract, Blueprintable)
class GENERICCAMERASYSTEM_API UGCMS_CameraMode_WithPenetrationAvoidance : public UGCMS_CameraMode
{
GENERATED_BODY()
public:
UGCMS_CameraMode_WithPenetrationAvoidance();
// Penetration prevention
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance")
float PenetrationBlendInTime = 0.1f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance")
float PenetrationBlendOutTime = 0.15f;
/** If true, does collision checks to keep the camera out of the world. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance")
bool bPreventPenetration = true;
/** If true, try to detect nearby walls and move the camera in anticipation. Helps prevent popping. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="PenetrationAvoidance")
bool bDoPredictiveAvoidance = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PenetrationAvoidance")
float CollisionPushOutDistance = 2.f;
/** When the camera's distance is pushed into this percentage of its full distance due to penetration */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PenetrationAvoidance")
float ReportPenetrationPercent = 0.f;
/**
* These are the feeler rays that are used to find where to place the camera.
* Index: 0 : This is the normal feeler we use to prevent collisions.
* Index: 1+ : These feelers are used if you bDoPredictiveAvoidance=true, to scan for potential impacts if the player
* were to rotate towards that direction and primitively collide the camera so that it pulls in before
* impacting the occluder.
*/
UPROPERTY(EditDefaultsOnly, Category = "PenetrationAvoidance")
TArray<FGCMS_CameraPenetrationAvoidanceFeeler> PenetrationAvoidanceFeelers;
UPROPERTY(Transient)
float AimLineToDesiredPosBlockedPct;
UPROPERTY(Transient)
TArray<TObjectPtr<const AActor>> DebugActorsHitDuringCameraPenetration;
#if ENABLE_DRAW_DEBUG
mutable float LastDrawDebugTime = -MAX_FLT;
#endif
protected:
UFUNCTION(BlueprintCallable, Category="CameraMode|PenetrationAvoidance")
void UpdatePreventPenetration(float DeltaTime);
UFUNCTION(BlueprintCallable, Category="CameraMode|PenetrationAvoidance")
void PreventCameraPenetration(bool bSingleRayOnly, const float& DeltaTime, const AActor* ViewTarget, const FVector& SafeLoc, FVector& CameraLoc, float& DistBlockedPct);
virtual void DrawDebug(UCanvas* Canvas) const override;
};

View File

@@ -0,0 +1,67 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "GCMS_CameraPenetrationAvoidanceFeeler.generated.h"
/**
* Struct defining a feeler ray used for camera penetration avoidance.
*/
USTRUCT()
struct GENERICCAMERASYSTEM_API FGCMS_CameraPenetrationAvoidanceFeeler
{
GENERATED_BODY()
/** FRotator describing deviance from main ray */
UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler)
FRotator AdjustmentRot;
/** how much this feeler affects the final position if it hits the world */
UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler)
float WorldWeight;
/** how much this feeler affects the final position if it hits a APawn (setting to 0 will not attempt to collide with pawns at all) */
UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler)
float PawnWeight;
/** extent to use for collision when tracing this feeler */
UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler)
float Extent;
/** minimum frame interval between traces with this feeler if nothing was hit last frame */
UPROPERTY(EditAnywhere, Category=PenetrationAvoidanceFeeler)
int32 TraceInterval;
/** number of frames since this feeler was used */
UPROPERTY(transient)
int32 FramesUntilNextTrace;
FGCMS_CameraPenetrationAvoidanceFeeler()
: AdjustmentRot(ForceInit)
, WorldWeight(0)
, PawnWeight(0)
, Extent(0)
, TraceInterval(0)
, FramesUntilNextTrace(0)
{
}
FGCMS_CameraPenetrationAvoidanceFeeler(const FRotator& InAdjustmentRot,
const float& InWorldWeight,
const float& InPawnWeight,
const float& InExtent,
const int32& InTraceInterval = 0,
const int32& InFramesUntilNextTrace = 0)
: AdjustmentRot(InAdjustmentRot)
, WorldWeight(InWorldWeight)
, PawnWeight(InPawnWeight)
, Extent(InExtent)
, TraceInterval(InTraceInterval)
, FramesUntilNextTrace(InFramesUntilNextTrace)
{
}
};

View File

@@ -0,0 +1,110 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "Components/ActorComponent.h"
#include "GameFramework/Actor.h"
#include "GCMS_CameraSystemComponent.generated.h"
class UCameraComponent;
class USpringArmComponent;
class UCanvas;
class AHUD;
class UGCMS_CameraMode;
class UGCMS_CameraModeStack;
class UObject;
struct FFrame;
struct FGameplayTag;
struct FMinimalViewInfo;
template <class TClass>
class TSubclassOf;
DECLARE_DELEGATE_RetVal(TSubclassOf<UGCMS_CameraMode>, FGMSCameraModeDelegate);
/**
* UGCMS_CameraSystemComponent
*
* The base camera component class used by GMS camera system.
*/
UCLASS(ClassGroup=GCMS, meta=(BlueprintSpawnableComponent))
class GENERICCAMERASYSTEM_API UGCMS_CameraSystemComponent : public UActorComponent
{
GENERATED_BODY()
public:
UGCMS_CameraSystemComponent(const FObjectInitializer& ObjectInitializer);
static void OnShowDebugInfo(AHUD* HUD, UCanvas* Canvas, const FDebugDisplayInfo& DisplayInfo, float& YL, float& YPos);
// Returns the camera component if one exists on the specified actor.
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "GCMS|Camera", meta=(DefaultToSelf="Actor"))
static UGCMS_CameraSystemComponent* GetCameraSystemComponent(const AActor* Actor) { return (Actor ? Actor->FindComponentByClass<UGCMS_CameraSystemComponent>() : nullptr); }
/**
* Returns the target actor that the camera is looking at.
*/
virtual AActor* GetTargetActor() const { return GetOwner(); }
// Delegate used to query for the best camera mode.
FGMSCameraModeDelegate DetermineCameraModeDelegate;
// Add an offset to the field of view. The offset is only for one frame, it gets cleared once it is applied.
void AddFieldOfViewOffset(float FovOffset) { FieldOfViewOffset += FovOffset; }
// Push specified Camera Mode.
UFUNCTION(BlueprintCallable, Category="GCMS|Camera")
void PushCameraMode(TSubclassOf<UGCMS_CameraMode> NewCameraMode);
UFUNCTION(BlueprintCallable, Category="GCMS|Camera")
void PushDefaultCameraMode();
/**
* Initialize it with camera component and sprint arm component.
*/
UFUNCTION(BlueprintCallable, Category="GCMS|Camera")
void Initialize(UCameraComponent* NewCameraComponent, USpringArmComponent* NewSpringArmComponent);
virtual void DrawDebug(UCanvas* Canvas) const;
/**
* Gets the camera mode tag associated with the top layer and the blend weight of it
* 返回顶层相机模式的tag和当前权重。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCMS|Camera")
void GetBlendInfo(float& OutWeightOfTopLayer, FGameplayTag& OutTagOfTopLayer) const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCMS|Camera")
UCameraComponent* GetAssociatedCamera() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GCMS|Camera")
USpringArmComponent* GetAssociatedSprintArm() const;
virtual void Activate(bool bReset) override;
virtual void Deactivate() override;
protected:
virtual void OnRegister() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
virtual void UpdateCameraModes();
protected:
UPROPERTY()
TObjectPtr<UCameraComponent> AssociatedCameraComponent;
UPROPERTY()
TObjectPtr<USpringArmComponent> AssociatedSprintArmComponent;
// Stack used to blend the camera modes.
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="GCMS|Camera", meta=(ShowInnerProperties))
TObjectPtr<UGCMS_CameraModeStack> CameraModeStack;
// Default camera mode will used.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="GCMS|Camera")
TSubclassOf<UGCMS_CameraMode> DefaultCameraMode;
// Offset applied to the field of view. The offset is only for one frame, it gets cleared once it is applied.
float FieldOfViewOffset;
};

View File

@@ -0,0 +1,14 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "Modules/ModuleManager.h"
class FGenericCameraSystemModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};