diff --git a/Exo.uproject b/Exo.uproject index da85ef5..f0d12cf 100644 --- a/Exo.uproject +++ b/Exo.uproject @@ -32,5 +32,10 @@ "Name": "MDL", "Enabled": false } + ], + "TargetPlatforms": [ + "Linux", + "LinuxArm64", + "Windows" ] } \ No newline at end of file diff --git a/Source/Exo/Private/Characters/ExoPlayerCharacter.cpp b/Source/Exo/Private/Characters/ExoPlayerCharacter.cpp index 6180334..bb7019b 100644 --- a/Source/Exo/Private/Characters/ExoPlayerCharacter.cpp +++ b/Source/Exo/Private/Characters/ExoPlayerCharacter.cpp @@ -8,6 +8,7 @@ #include "GameFramework/CharacterMovementComponent.h" #include "Items/AmmoBoxBase.h" #include "Items/HealthBoxBase.h" +#include "Kismet/GameplayStatics.h" #include "Player/InteractionComponent.h" #include "Widget/WBP_PlayerUI.h" @@ -26,9 +27,15 @@ AExoPlayerCharacter::AExoPlayerCharacter() Weapon->CastShadow = false; Weapon->SetRelativeLocation(FVector(-50.f, 0.f, -90.f)); - bUseControllerRotationPitch = false; - bUseControllerRotationYaw = true; - bUseControllerRotationRoll = false; + //bUseControllerRotationPitch = false; + //bUseControllerRotationYaw = true; + //bUseControllerRotationRoll = false; + + // Setup camera component + CameraComponent = CreateDefaultSubobject(TEXT("CameraComponent")); + CameraComponent->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); + CameraComponent->bUsePawnControlRotation = true; + CameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, BaseEyeHeight)); } void AExoPlayerCharacter::BeginPlay() @@ -36,7 +43,8 @@ void AExoPlayerCharacter::BeginPlay() Super::BeginPlay(); GetCapsuleComponent()->OnComponentBeginOverlap.AddDynamic(this, &AExoPlayerCharacter::OnActorBeginOverlap); - + //UGameplayStatics::GetPlayerCameraManager(GetWorld(), 0)->SetViewTarget(this); + StandingEyeHeight = CameraComponent->GetRelativeLocation().Z; PlayerHud = CreateWidget(GetWorld(),PlayerHudClass); diff --git a/Source/Exo/Private/Player/ExoPlayerController.cpp b/Source/Exo/Private/Player/ExoPlayerController.cpp index a546933..a9b24a0 100644 --- a/Source/Exo/Private/Player/ExoPlayerController.cpp +++ b/Source/Exo/Private/Player/ExoPlayerController.cpp @@ -7,6 +7,7 @@ #include "Characters/ExoPlayerCharacter.h" #include "GameFramework/Character.h" #include "GameFramework/CharacterMovementComponent.h" +#include "Kismet/GameplayStatics.h" AExoPlayerController::AExoPlayerController() { @@ -28,17 +29,34 @@ void AExoPlayerController::BeginPlay() Subsystem->AddMappingContext(InputContext, 0); } - InteractionComponent = PlayerCharacter->FindComponentByClass(); - ShootingComponent = PlayerCharacter->FindComponentByClass(); + //InteractionComponent = PlayerCharacter->FindComponentByClass(); + //ShootingComponent = PlayerCharacter->FindComponentByClass(); + InteractionComponent = PlayerCharacter->InteractionComponent; + ShootingComponent = PlayerCharacter->ShootingComponent; // Ustawianie w komponencie poruszania pr�dko�ci zapisanej w characterze 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) { 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() @@ -139,13 +157,38 @@ void AExoPlayerController::PlayerStartCrouch() PlayerCharacter->GetCharacterMovement()->MaxWalkSpeedCrouched = PlayerCharacter->SlideSpeed; GetWorldTimerManager().SetTimer(SlideCooldownTimer, this, &AExoPlayerController::ResetSlide, PlayerCharacter->SlideCooldown, false); } - + 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() { 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() @@ -224,3 +267,258 @@ void AExoPlayerController::PlayerReload() { 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 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 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(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 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 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; +} diff --git a/Source/Exo/Public/Characters/Components/ShootingComponent.h b/Source/Exo/Public/Characters/Components/ShootingComponent.h index 7995b84..1c41581 100644 --- a/Source/Exo/Public/Characters/Components/ShootingComponent.h +++ b/Source/Exo/Public/Characters/Components/ShootingComponent.h @@ -104,6 +104,11 @@ private: void ReloadCompleted(); AActor* ExecuteLineTrace(float LineRange); + // Cover logic - ToQly + + + // + FTimerHandle ShootCooldownTimer; FTimerHandle ReloadTimer; diff --git a/Source/Exo/Public/Characters/ExoPlayerCharacter.h b/Source/Exo/Public/Characters/ExoPlayerCharacter.h index f83b1bc..d1e7ffa 100644 --- a/Source/Exo/Public/Characters/ExoPlayerCharacter.h +++ b/Source/Exo/Public/Characters/ExoPlayerCharacter.h @@ -3,6 +3,7 @@ #pragma once #include "CoreMinimal.h" +#include "Camera/CameraComponent.h" #include "Characters/ExoCharacterBase.h" #include "Components/CapsuleComponent.h" #include "ExoPlayerCharacter.generated.h" @@ -28,6 +29,12 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly) TObjectPtr ShootingComponent; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera") + TObjectPtr CameraComponent; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera") + float StandingEyeHeight; + UPROPERTY(EditAnywhere, Category = "Dodge Properties") float DodgeForce = 5000.f; diff --git a/Source/Exo/Public/Player/ExoPlayerController.h b/Source/Exo/Public/Player/ExoPlayerController.h index dcb151f..159fc63 100644 --- a/Source/Exo/Public/Player/ExoPlayerController.h +++ b/Source/Exo/Public/Player/ExoPlayerController.h @@ -91,7 +91,25 @@ protected: UFUNCTION(BlueprintCallable, Category = "Input") 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") TObjectPtr InputContext; @@ -159,8 +177,62 @@ protected: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character") TObjectPtr 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 CoverTraceChannel = ECC_WorldStatic; + private: UInteractionComponent* InteractionComponent; UShootingComponent* ShootingComponent; + + float CoverStandAlpha = 0.0f; + FTimerHandle CoverCheckTimer; + float MaxCoverAimHeight; };