feat: Character camera and simple cover system

This commit is contained in:
NeonDmn 2025-05-22 21:32:39 +02:00
parent 848f500933
commit 8c9bc0f44e
6 changed files with 404 additions and 9 deletions

View File

@ -32,5 +32,10 @@
"Name": "MDL", "Name": "MDL",
"Enabled": false "Enabled": false
} }
],
"TargetPlatforms": [
"Linux",
"LinuxArm64",
"Windows"
] ]
} }

View File

@ -8,6 +8,7 @@
#include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/CharacterMovementComponent.h"
#include "Items/AmmoBoxBase.h" #include "Items/AmmoBoxBase.h"
#include "Items/HealthBoxBase.h" #include "Items/HealthBoxBase.h"
#include "Kismet/GameplayStatics.h"
#include "Player/InteractionComponent.h" #include "Player/InteractionComponent.h"
#include "Widget/WBP_PlayerUI.h" #include "Widget/WBP_PlayerUI.h"
@ -26,9 +27,15 @@ AExoPlayerCharacter::AExoPlayerCharacter()
Weapon->CastShadow = false; Weapon->CastShadow = false;
Weapon->SetRelativeLocation(FVector(-50.f, 0.f, -90.f)); Weapon->SetRelativeLocation(FVector(-50.f, 0.f, -90.f));
bUseControllerRotationPitch = false; //bUseControllerRotationPitch = false;
bUseControllerRotationYaw = true; //bUseControllerRotationYaw = true;
bUseControllerRotationRoll = false; //bUseControllerRotationRoll = false;
// Setup camera component
CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
CameraComponent->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
CameraComponent->bUsePawnControlRotation = true;
CameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, BaseEyeHeight));
} }
void AExoPlayerCharacter::BeginPlay() void AExoPlayerCharacter::BeginPlay()
@ -36,7 +43,8 @@ void AExoPlayerCharacter::BeginPlay()
Super::BeginPlay(); Super::BeginPlay();
GetCapsuleComponent()->OnComponentBeginOverlap.AddDynamic(this, &AExoPlayerCharacter::OnActorBeginOverlap); GetCapsuleComponent()->OnComponentBeginOverlap.AddDynamic(this, &AExoPlayerCharacter::OnActorBeginOverlap);
//UGameplayStatics::GetPlayerCameraManager(GetWorld(), 0)->SetViewTarget(this);
StandingEyeHeight = CameraComponent->GetRelativeLocation().Z;
PlayerHud = CreateWidget<UWBP_PlayerUI>(GetWorld(),PlayerHudClass); PlayerHud = CreateWidget<UWBP_PlayerUI>(GetWorld(),PlayerHudClass);

View File

@ -7,6 +7,7 @@
#include "Characters/ExoPlayerCharacter.h" #include "Characters/ExoPlayerCharacter.h"
#include "GameFramework/Character.h" #include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/CharacterMovementComponent.h"
#include "Kismet/GameplayStatics.h"
AExoPlayerController::AExoPlayerController() AExoPlayerController::AExoPlayerController()
{ {
@ -28,17 +29,34 @@ void AExoPlayerController::BeginPlay()
Subsystem->AddMappingContext(InputContext, 0); Subsystem->AddMappingContext(InputContext, 0);
} }
InteractionComponent = PlayerCharacter->FindComponentByClass<UInteractionComponent>(); //InteractionComponent = PlayerCharacter->FindComponentByClass<UInteractionComponent>();
ShootingComponent = PlayerCharacter->FindComponentByClass<UShootingComponent>(); //ShootingComponent = PlayerCharacter->FindComponentByClass<UShootingComponent>();
InteractionComponent = PlayerCharacter->InteractionComponent;
ShootingComponent = PlayerCharacter->ShootingComponent;
// Ustawianie w komponencie poruszania pr<70>dko<6B>ci zapisanej w characterze // Ustawianie w komponencie poruszania pr<70>dko<6B>ci zapisanej w characterze
PlayerCharacter->GetCharacterMovement()->MaxWalkSpeed = PlayerCharacter->WalkSpeed; PlayerCharacter->GetCharacterMovement()->MaxWalkSpeed = PlayerCharacter->WalkSpeed;
// Oblicz prawidłową maksymalną wysokość na którą można wychylić się z nad osłony
UE_LOG(LogTemp, Display, TEXT("MaxCoverAimHeight: %f"), MaxCoverAimHeight);
} }
void AExoPlayerController::PlayerTick(float DeltaTime) void AExoPlayerController::PlayerTick(float DeltaTime)
{ {
Super::PlayerTick(DeltaTime); Super::PlayerTick(DeltaTime);
// TESTING
if (bIsInCover)
{
AdjustCameraWhileInCover();
}
UpdateCoverStandHeight(DeltaTime);
if (bShowCoverSystemDebug)
{
GEngine->AddOnScreenDebugMessage(2, -1, FColor::Red, FString::Printf(TEXT("In cover: %s"), bIsInCover ? "true" : "false"));
}
} }
void AExoPlayerController::SetupInputComponent() void AExoPlayerController::SetupInputComponent()
@ -139,13 +157,38 @@ void AExoPlayerController::PlayerStartCrouch()
PlayerCharacter->GetCharacterMovement()->MaxWalkSpeedCrouched = PlayerCharacter->SlideSpeed; PlayerCharacter->GetCharacterMovement()->MaxWalkSpeedCrouched = PlayerCharacter->SlideSpeed;
GetWorldTimerManager().SetTimer(SlideCooldownTimer, this, &AExoPlayerController::ResetSlide, PlayerCharacter->SlideCooldown, false); GetWorldTimerManager().SetTimer(SlideCooldownTimer, this, &AExoPlayerController::ResetSlide, PlayerCharacter->SlideCooldown, false);
} }
PlayerCharacter->Crouch(); PlayerCharacter->Crouch();
PlayerCharacter->CameraComponent->SetRelativeLocation(FVector(
0.0f,
0.0f,
PlayerCharacter->CrouchedEyeHeight)
);
// Start checking for cover
bIsInCover = CheckForCover();
GetWorld()->GetTimerManager().SetTimer(
CoverCheckTimer,
this,
&AExoPlayerController::OnCoverTimer,
CoverCheckRate,
true
);
} }
void AExoPlayerController::PlayerStopCrouch() void AExoPlayerController::PlayerStopCrouch()
{ {
PlayerCharacter->UnCrouch(); PlayerCharacter->UnCrouch();
PlayerCharacter->CameraComponent->SetRelativeLocation(FVector(
0.0f,
0.0f,
PlayerCharacter->BaseEyeHeight)
);
// Stop checking for cover
GetWorld()->GetTimerManager().ClearTimer(CoverCheckTimer);
bIsInCover = false;
TargetCoverStandAlpha = 0.0f;
} }
void AExoPlayerController::ResetSlide() void AExoPlayerController::ResetSlide()
@ -224,3 +267,258 @@ void AExoPlayerController::PlayerReload()
{ {
ShootingComponent->Reload(); ShootingComponent->Reload();
} }
bool AExoPlayerController::CheckForCover()
{
// Cover is recalculated every time the player crouches or stops moving while crouched
// Cover targets are cleared completely when player stands up again or dies
// Do a simple 8-directional line trace
FVector TraceDirection = PlayerCharacter->GetActorForwardVector();
FVector TraceStart = PlayerCharacter->GetActorLocation();
TraceStart.Z -= PlayerCharacter->GetCapsuleComponent()->GetScaledCapsuleHalfHeight() - CoverObstacleMinHeight;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(PlayerCharacter);
TArray<FHitResult> ValidHits;
for (int i = 0; i <= 8; i++)
{
// Trace for possible cover at set distance
FHitResult Hit;
FVector TraceEnd = TraceStart + (TraceDirection * MaxDistanceFromCover);
GetWorld()->LineTraceSingleByChannel(
Hit,
TraceStart,
TraceEnd,
CoverTraceChannel,
QueryParams
);
if (bShowCoverSystemDebug)
{
// DEBUG
DrawDebugLine(GetWorld(), TraceStart,TraceEnd,
Hit.bBlockingHit ? FColor::Blue : FColor::Red,
false, 5.0f, 0, 1.0f);
}
if (Hit.bBlockingHit == true) {
// Check if this particular hit isn't against a wall (>CoverMaxHeight)
FHitResult SphereHit;
FVector SpherePosition = Hit.ImpactPoint +
FVector(
0.0f, 0.0f,
CoverObstacleMaxHeight + CoverAimWindowRadius - CoverObstacleMinHeight);
GetWorld()->SweepSingleByChannel(
SphereHit,
SpherePosition,
SpherePosition,
FQuat::Identity,
CoverTraceChannel,
FCollisionShape::MakeSphere(CoverAimWindowRadius),
QueryParams);
if (bShowCoverSystemDebug)
{
DrawDebugSphere(GetWorld(), SphereHit.TraceStart, CoverAimWindowRadius, 8,
Hit.bBlockingHit ? FColor::Blue : FColor::Red,
false, 5.0f, 0, 1);
}
if (SphereHit.bBlockingHit == false)
{
ValidHits.Add(Hit);
}
}
// Rotate trace to search in another direction next time
TraceDirection = TraceDirection.RotateAngleAxis(45, FVector(0, 0, 1));
//UE_LOG(LogTemp, Display, TEXT("Check Cover %d"), i);
}
return ValidHits.Num() > 0;
}
void AExoPlayerController::AdjustCameraWhileInCover()
{
if (!bIsInCover)
{
return;
}
MaxCoverAimHeight = PlayerCharacter->StandingEyeHeight * CoverAimStandFactor;
//FVector StartOffset = GetCharacter()->GetActorForwardVector() * 20.0f;
FVector ObstacleTraceStart = GetCharacter()->GetActorLocation();
ObstacleTraceStart.Z += GetCharacter()->CrouchedEyeHeight;
FVector ObstacleTraceEnd =
ObstacleTraceStart + PlayerCameraManager->GetCameraRotation().Vector() * CoverAimFreeDistance;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(PlayerCharacter);
TEnumAsByte<ECollisionChannel> ObstacleTraceChannel = ECC_WorldStatic;
FHitResult Hit;
bool bFreeSpace = false;
// Trace until no hit is found (free space to aim)
for (int i = 0; i < MaxCoverAimHeight; i += 2*CoverAimWindowRadius)
{
ObstacleTraceStart.Z += i;
ObstacleTraceEnd.Z += i;
GetWorld()->SweepSingleByChannel(
Hit,
ObstacleTraceStart,
ObstacleTraceEnd,
FQuat::Identity,
ObstacleTraceChannel,
FCollisionShape::MakeSphere(CoverAimWindowRadius),
QueryParams
);
if (bShowCoverSystemDebug)
{
DrawDebugSphere(GetWorld(), Hit.Location, CoverAimWindowRadius, 8,
Hit.bBlockingHit ? FColor::Blue : FColor::Red,
false, 0.0f, 0, 0);
}
if (Hit.bBlockingHit == false)
{
UE_LOG(LogTemp, Display, TEXT("Free space"));
bFreeSpace = true;
break;
}
}
if (bFreeSpace)
{
TargetCoverStandAlpha = FMath::GetMappedRangeValueClamped(
UE::Math::TVector2<float>(GetCharacter()->CrouchedEyeHeight, GetCharacter()->CrouchedEyeHeight + MaxCoverAimHeight),
UE::Math::TVector2(0.0f, 1.0f),
Hit.TraceStart.Z
);
if (bShowCoverSystemDebug)
{
DrawDebugLine(GetWorld(), Hit.TraceStart, Hit.TraceEnd, FColor::Red);
}
}
// IDEA FOR A MORE COMPLEX SYSTEM IN THE FUTURE:
//vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
// To not introduce accidental flickering and optimize it a bit, all of this should run
// ONLY on cover update
// FVector CrouchedEyeWorldPosition = PlayerCharacter->GetActorLocation();
// CrouchedEyeWorldPosition.Z += PlayerCharacter->CrouchedEyeHeight;
// FVector CoverCeiling = CrouchedEyeWorldPosition + MaxCoverAimHeight;
// float PlayerCapsuleRadius = PlayerCharacter->GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
// TEnumAsByte<ECollisionChannel> ObstacleTraceChannel = ECC_WorldStatic;
// FCollisionQueryParams QueryParams;
// QueryParams.AddIgnoredActor(PlayerCharacter);
//
// // Shoot ray up at maximum cover length to determine how high the player can "stand up"
// FHitResult WallTopHit;
// GetWorld()->SweepSingleByChannel(
// WallTopHit,
// CrouchedEyeWorldPosition,
// CoverCeiling,
// FQuat::Identity,
// ObstacleTraceChannel,
// FCollisionShape::MakeSphere(PlayerCapsuleRadius),
// QueryParams
// );
//
// FVector CeilingWorldLocation;
// if (WallTopHit.bBlockingHit == true)
// {
// CeilingWorldLocation = WallTopHit.Location;
// }
// else
// {
// CeilingWorldLocation = WallTopHit.TraceEnd;
// }
//
// // From hit location (or if no hit - from end position) shoot ray towards camera's forward
// // vector at minimum cover length
//
// constexpr float WallFinderDistance = 75.0f;
// FVector WallFinderDirection = PlayerCharacter->GetActorForwardVector();
// WallFinderDirection.Z = 0.0f; // Not needed if the player always stays upright
//
// GetWorld()->LineTraceSingleByChannel(
// WallTopHit,
// CeilingWorldLocation,
// CeilingWorldLocation + (WallFinderDirection * WallFinderDistance),
// ObstacleTraceChannel,
// QueryParams
// );
//
// // regardless of the hit result shoot multi-ray downwards from end location
// // with maximum cover length
//
// FVector WallTopTraceStart = WallTopHit.bBlockingHit ? WallTopHit.Location : WallTopHit.TraceEnd;
// TArray<FHitResult> ValidHitsCandidateArray;
//
// GetWorld()->LineTraceMultiByChannel(
// ValidHitsCandidateArray,
// WallTopTraceStart,
// WallTopTraceStart + (FVector(0.0f, 0.0f, 0.0f) * MaxCoverAimHeight),
// ObstacleTraceChannel,
// QueryParams
// );
// On every ray hit trace an in-place sphere to determine if the aiming spot is valid
// All valid aiming spots are collected and the lowest (smallest Z) is chosen
// Sphere trace from camera forward a certain distance to regulate camera position while
// aiming up and down // TODO: elaborate
}
void AExoPlayerController::UpdateCoverStandHeight(float DeltaTime)
{
// if (FMath::IsNearlyEqual(CoverStandAlpha, TargetCoverStandAlpha, 0.01f))
// {
// return;
// }
CoverStandAlpha = FMath::Lerp(CoverStandAlpha, TargetCoverStandAlpha, DeltaTime * 5.0f);
//CoverStandAlpha = TargetCoverStandAlpha;
// BANDAID SOLUTION
const float NewZ = FMath::Lerp(
PlayerCharacter->CrouchedEyeHeight,
PlayerCharacter->StandingEyeHeight,
CoverStandAlpha
);
PlayerCharacter->CameraComponent->SetRelativeLocation(
FVector(0.0f, 0.0f, NewZ)
);
if (bShowCoverSystemDebug && GEngine)
{
GEngine->AddOnScreenDebugMessage(1, -1, FColor::Red, FString::Printf(TEXT("Current cover stand alpha: %f"), CoverStandAlpha));
}
}
void AExoPlayerController::OnCoverTimer()
{
bIsInCover = CheckForCover();
if (bIsInCover == false)
{
TargetCoverStandAlpha = 0.0f;
UpdateCoverStandHeight(GetWorld()->GetDeltaSeconds());
}
}
void AExoPlayerController::DebugCoverSystem(bool show)
{
bShowCoverSystemDebug = show;
}

View File

@ -104,6 +104,11 @@ private:
void ReloadCompleted(); void ReloadCompleted();
AActor* ExecuteLineTrace(float LineRange); AActor* ExecuteLineTrace(float LineRange);
// Cover logic - ToQly
//
FTimerHandle ShootCooldownTimer; FTimerHandle ShootCooldownTimer;
FTimerHandle ReloadTimer; FTimerHandle ReloadTimer;

View File

@ -3,6 +3,7 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "Camera/CameraComponent.h"
#include "Characters/ExoCharacterBase.h" #include "Characters/ExoCharacterBase.h"
#include "Components/CapsuleComponent.h" #include "Components/CapsuleComponent.h"
#include "ExoPlayerCharacter.generated.h" #include "ExoPlayerCharacter.generated.h"
@ -28,6 +29,12 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly) UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr<UShootingComponent> ShootingComponent; TObjectPtr<UShootingComponent> ShootingComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera")
TObjectPtr<UCameraComponent> CameraComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera")
float StandingEyeHeight;
UPROPERTY(EditAnywhere, Category = "Dodge Properties") UPROPERTY(EditAnywhere, Category = "Dodge Properties")
float DodgeForce = 5000.f; float DodgeForce = 5000.f;

View File

@ -91,7 +91,25 @@ protected:
UFUNCTION(BlueprintCallable, Category = "Input") UFUNCTION(BlueprintCallable, Category = "Input")
void PlayerReload(); // R void PlayerReload(); // R
UFUNCTION(BlueprintCallable, Category = "Cover System")
bool CheckForCover();
UFUNCTION(BlueprintCallable, Category = "Cover System")
void AdjustCameraWhileInCover();
UFUNCTION(BlueprintCallable, Category = "Cover System")
void UpdateCoverStandHeight(float DeltaTime);
void OnCoverTimer();
float MapAlphaToCoverStandHeight(float Alpha);
// DEBUG
UFUNCTION(Exec, Category = "Cover System")
void DebugCoverSystem(bool show);
bool bShowCoverSystemDebug = false;
UPROPERTY(EditAnywhere, Category = "Input") UPROPERTY(EditAnywhere, Category = "Input")
TObjectPtr<UInputMappingContext> InputContext; TObjectPtr<UInputMappingContext> InputContext;
@ -159,8 +177,62 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character")
TObjectPtr<AExoPlayerCharacter> PlayerCharacter; TObjectPtr<AExoPlayerCharacter> PlayerCharacter;
// Czy postać gracza jest obecnie schowana za osłoną
UPROPERTY(EditAnywhere, Category = "Cover System")
bool bIsInCover = false;
/** Alpha wysokości kamery podczas wychulania zza osłony
* 0 - Wysokość kucania
* 1 - Maksymalna wysokość wychylenia określona przez CoverAimStandFactor
**/
UPROPERTY(EditAnywhere, Category = "Cover System")
float TargetCoverStandAlpha = 0.0f;
// Częstotliwość okresowego szukania osłon podczas kucania
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover System")
float CoverCheckRate = 0.5f;
// Dystans na którym postać jest w stanie odnaleźć osłonę
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover System")
float MaxDistanceFromCover = 50.0f;
// Minimalna wysokość przeszkody którą można określić osłoną
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover System")
float CoverObstacleMinHeight = 75.0f;
// Maksymalna wysokość przeszkody którą można określić osłoną
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover System")
float CoverObstacleMaxHeight = 125.0f;
// Promień wolnej przestrzeni przez którą można się wychylić
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover System")
float CoverAimWindowRadius = 2.0f;
// Dystans od kamery po wychyleniu który musi być wolny by dało się wychylić
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover System")
float CoverAimFreeDistance = 150.0f;
/** Określa na jaki procent "Stania" postać może się podnieść podczas celowania
* zza osłony. 0 -> Nie podniesie się wogóle (jakby wychylanie się było wyłączone)
* 1 -> Postać jest w stanie się podnieść do pełnej wysokości, tak jakby normalnie stała
**/
UPROPERTY(EditAnywhere,
meta = (ClampMin = "0.0", ClampMax = "1.0",
UIMin = "0.0",
UIMax = "1.0") ,
BlueprintReadWrite, Category = "Cover System")
float CoverAimStandFactor = 0.9f;
// Trace Channel po którym szukać geometrii potencjalnych osłon
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cover System")
TEnumAsByte<ECollisionChannel> CoverTraceChannel = ECC_WorldStatic;
private: private:
UInteractionComponent* InteractionComponent; UInteractionComponent* InteractionComponent;
UShootingComponent* ShootingComponent; UShootingComponent* ShootingComponent;
float CoverStandAlpha = 0.0f;
FTimerHandle CoverCheckTimer;
float MaxCoverAimHeight;
}; };