// Copyright Pacificao. All Rights Reserved. #include "AgrarianWildlifeBase.h" #include "AgrarianGameCharacter.h" #include "AgrarianInventoryComponent.h" #include "AIController.h" #include "Components/AudioComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "GameFramework/CharacterMovementComponent.h" #include "Kismet/GameplayStatics.h" #include "Materials/MaterialInterface.h" #include "Navigation/PathFollowingComponent.h" #include "NavigationSystem.h" #include "Net/UnrealNetwork.h" #include "UObject/ConstructorHelpers.h" namespace { void ConfigureWildlifeProxyComponent(UStaticMeshComponent* Component, UStaticMesh* MeshAsset, UMaterialInterface* MaterialAsset) { if (!Component) { return; } Component->SetCollisionEnabled(ECollisionEnabled::NoCollision); Component->SetGenerateOverlapEvents(false); if (MeshAsset) { Component->SetStaticMesh(MeshAsset); } if (MaterialAsset) { Component->SetMaterial(0, MaterialAsset); } } } AAgrarianWildlifeBase::AAgrarianWildlifeBase() { PrimaryActorTick.bCanEverTick = true; bReplicates = true; SetNetCullDistanceSquared(FMath::Square(6000.0f)); AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned; AIControllerClass = AAIController::StaticClass(); GetCharacterMovement()->MaxWalkSpeed = WanderSpeed; GetCharacterMovement()->bOrientRotationToMovement = true; bUseControllerRotationYaw = false; static ConstructorHelpers::FObjectFinder ChamferCubeMesh(TEXT("/Game/Agrarian/Environment/PlaceholderMeshes/SM_AGR_Placeholder_ChamferCube.SM_AGR_Placeholder_ChamferCube")); static ConstructorHelpers::FObjectFinder CubeMesh(TEXT("/Game/Agrarian/Environment/PlaceholderMeshes/SM_AGR_Placeholder_Cube.SM_AGR_Placeholder_Cube")); static ConstructorHelpers::FObjectFinder CylinderMesh(TEXT("/Game/Agrarian/Environment/PlaceholderMeshes/SM_AGR_Placeholder_Cylinder.SM_AGR_Placeholder_Cylinder")); static ConstructorHelpers::FObjectFinder FurMaterial(TEXT("/Game/Agrarian/Materials/M_AGR_GZ_Fiber_Resource.M_AGR_GZ_Fiber_Resource")); WildlifeBodyProxy = CreateDefaultSubobject(TEXT("WildlifeBodyProxy")); WildlifeBodyProxy->SetupAttachment(RootComponent); ConfigureWildlifeProxyComponent(WildlifeBodyProxy, ChamferCubeMesh.Succeeded() ? ChamferCubeMesh.Object : nullptr, FurMaterial.Succeeded() ? FurMaterial.Object : nullptr); WildlifeBodyProxy->SetRelativeLocation(FVector(10.0f, 0.0f, -42.0f)); WildlifeBodyProxy->SetRelativeScale3D(FVector(0.62f, 0.32f, 0.24f)); WildlifeHeadProxy = CreateDefaultSubobject(TEXT("WildlifeHeadProxy")); WildlifeHeadProxy->SetupAttachment(WildlifeBodyProxy); ConfigureWildlifeProxyComponent(WildlifeHeadProxy, ChamferCubeMesh.Succeeded() ? ChamferCubeMesh.Object : nullptr, FurMaterial.Succeeded() ? FurMaterial.Object : nullptr); WildlifeHeadProxy->SetRelativeLocation(FVector(42.0f, 0.0f, 20.0f)); WildlifeHeadProxy->SetRelativeScale3D(FVector(0.46f, 0.34f, 0.36f)); WildlifeEarProxyA = CreateDefaultSubobject(TEXT("WildlifeEarProxyA")); WildlifeEarProxyA->SetupAttachment(WildlifeHeadProxy); ConfigureWildlifeProxyComponent(WildlifeEarProxyA, CubeMesh.Succeeded() ? CubeMesh.Object : nullptr, FurMaterial.Succeeded() ? FurMaterial.Object : nullptr); WildlifeEarProxyA->SetRelativeLocation(FVector(4.0f, -12.0f, 28.0f)); WildlifeEarProxyA->SetRelativeRotation(FRotator(0.0f, 0.0f, -10.0f)); WildlifeEarProxyA->SetRelativeScale3D(FVector(0.08f, 0.08f, 0.38f)); WildlifeEarProxyB = CreateDefaultSubobject(TEXT("WildlifeEarProxyB")); WildlifeEarProxyB->SetupAttachment(WildlifeHeadProxy); ConfigureWildlifeProxyComponent(WildlifeEarProxyB, CubeMesh.Succeeded() ? CubeMesh.Object : nullptr, FurMaterial.Succeeded() ? FurMaterial.Object : nullptr); WildlifeEarProxyB->SetRelativeLocation(FVector(4.0f, 12.0f, 28.0f)); WildlifeEarProxyB->SetRelativeRotation(FRotator(0.0f, 0.0f, 10.0f)); WildlifeEarProxyB->SetRelativeScale3D(FVector(0.08f, 0.08f, 0.38f)); WildlifeTailProxy = CreateDefaultSubobject(TEXT("WildlifeTailProxy")); WildlifeTailProxy->SetupAttachment(WildlifeBodyProxy); ConfigureWildlifeProxyComponent(WildlifeTailProxy, CylinderMesh.Succeeded() ? CylinderMesh.Object : nullptr, FurMaterial.Succeeded() ? FurMaterial.Object : nullptr); WildlifeTailProxy->SetRelativeLocation(FVector(-42.0f, 0.0f, 10.0f)); WildlifeTailProxy->SetRelativeRotation(FRotator(0.0f, 90.0f, 90.0f)); WildlifeTailProxy->SetRelativeScale3D(FVector(0.12f, 0.12f, 0.24f)); WildlifeAudioComponent = CreateDefaultSubobject(TEXT("WildlifeAudioComponent")); WildlifeAudioComponent->SetupAttachment(RootComponent); WildlifeAudioComponent->bAutoActivate = false; WildlifeAudioComponent->bAllowSpatialization = true; DisplayName = FText::FromString(TEXT("Wildlife")); } void AAgrarianWildlifeBase::BeginPlay() { Super::BeginPlay(); SpawnLocation = GetActorLocation(); Health = FMath::Clamp(Health, 0.0f, MaxHealth); if (HasAuthority() && !GetController()) { SpawnDefaultController(); } ChooseWanderTarget(); BroadcastHealthChanged(); BroadcastStateChanged(); } void AAgrarianWildlifeBase::Tick(float DeltaSeconds) { Super::Tick(DeltaSeconds); if (HasAuthority()) { if (ShouldRunServerThink(DeltaSeconds)) { const float EffectiveDeltaSeconds = bEnablePerformanceLimits ? ServerThinkAccumulator : DeltaSeconds; ServerThink(EffectiveDeltaSeconds); ServerThinkAccumulator = 0.0f; } } } void AAgrarianWildlifeBase::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AAgrarianWildlifeBase, WildlifeState); DOREPLIFETIME(AAgrarianWildlifeBase, Health); DOREPLIFETIME(AAgrarianWildlifeBase, bHarvested); } bool AAgrarianWildlifeBase::IsAlive() const { return WildlifeState != EAgrarianWildlifeState::Dead && Health > 0.0f; } void AAgrarianWildlifeBase::ApplyWildlifeDamage(float Amount, AActor* InstigatorActor) { if (!HasAuthority() || !IsAlive()) { return; } Health = FMath::Clamp(Health - FMath::Max(0.0f, Amount), 0.0f, MaxHealth); FocusActor = InstigatorActor; BroadcastHealthChanged(); if (Health <= 0.0f) { EnterDeadState(); return; } if (Disposition == EAgrarianWildlifeDisposition::Passive || Disposition == EAgrarianWildlifeDisposition::Fleeing) { SetWildlifeState(EAgrarianWildlifeState::Fleeing); } else if (Disposition == EAgrarianWildlifeDisposition::Aggressive) { SetWildlifeState(EAgrarianWildlifeState::Chasing); } } void AAgrarianWildlifeBase::SetWildlifeState(EAgrarianWildlifeState NewState) { if (WildlifeState == NewState) { return; } WildlifeState = NewState; if (HasAuthority()) { MulticastPlayWildlifeStateSound(WildlifeState); } BroadcastStateChanged(); } FText AAgrarianWildlifeBase::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const { if (WildlifeState == EAgrarianWildlifeState::Dead) { return bHarvested ? FText::FromString(TEXT("Harvested")) : FText::FromString(TEXT("Harvest")); } return DisplayName; } bool AAgrarianWildlifeBase::CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const { return Interactor && WildlifeState == EAgrarianWildlifeState::Dead && !bHarvested; } void AAgrarianWildlifeBase::Interact_Implementation(AAgrarianGameCharacter* Interactor) { if (HasAuthority()) { Harvest(Interactor); } } void AAgrarianWildlifeBase::OnRep_Health() { BroadcastHealthChanged(); } void AAgrarianWildlifeBase::OnRep_WildlifeState() { BroadcastStateChanged(); } void AAgrarianWildlifeBase::MulticastPlayWildlifeStateSound_Implementation(EAgrarianWildlifeState NewState) { if (GetNetMode() == NM_DedicatedServer || !WildlifeAudioComponent) { return; } USoundBase* StateSound = GetSoundForWildlifeState(NewState); if (!StateSound) { return; } WildlifeAudioComponent->SetSound(StateSound); WildlifeAudioComponent->Play(); } void AAgrarianWildlifeBase::MulticastPlayWildlifeHarvestSound_Implementation() { if (GetNetMode() == NM_DedicatedServer || !WildlifeAudioComponent || !HarvestWildlifeSound) { return; } WildlifeAudioComponent->SetSound(HarvestWildlifeSound); WildlifeAudioComponent->Play(); } bool AAgrarianWildlifeBase::ShouldRunServerThink(float DeltaSeconds) { if (!bEnablePerformanceLimits || WildlifeState == EAgrarianWildlifeState::Dead) { return true; } ServerThinkAccumulator += DeltaSeconds; if (GetNearestPlayerDistanceSquared() <= FMath::Square(FullUpdateRadius)) { return true; } return ServerThinkAccumulator >= FarUpdateIntervalSeconds; } void AAgrarianWildlifeBase::ServerThink(float DeltaSeconds) { if (!IsAlive()) { return; } DecisionTimer -= DeltaSeconds; if (DecisionTimer <= 0.0f) { DecisionTimer = DecisionIntervalSeconds; if (Disposition == EAgrarianWildlifeDisposition::Aggressive) { FocusActor = FindNearestPlayer(AggroRadius); SetWildlifeState(FocusActor ? EAgrarianWildlifeState::Chasing : EAgrarianWildlifeState::Wandering); } else { FocusActor = FindNearestPlayer(FleeRadius); SetWildlifeState(FocusActor ? EAgrarianWildlifeState::Fleeing : EAgrarianWildlifeState::Wandering); } if (WildlifeState == EAgrarianWildlifeState::Wandering && FVector::DistSquared(GetActorLocation(), CurrentMoveTarget) < FMath::Square(125.0f)) { ChooseWanderTarget(); } } MoveTowardTarget(); } void AAgrarianWildlifeBase::ChooseWanderTarget() { FVector ReachableTarget = FVector::ZeroVector; if (ChooseReachableWanderTarget(ReachableTarget)) { CurrentMoveTarget = ReachableTarget; ClearNavigationMove(); return; } const FVector RandomOffset = FVector( FMath::FRandRange(-WanderRadius, WanderRadius), FMath::FRandRange(-WanderRadius, WanderRadius), 0.0f); CurrentMoveTarget = SpawnLocation + RandomOffset; ClearNavigationMove(); } void AAgrarianWildlifeBase::MoveTowardTarget() { FVector DesiredTarget = GetActorLocation(); float DesiredSpeed = WanderSpeed; if (WildlifeState == EAgrarianWildlifeState::Fleeing && FocusActor) { FVector DesiredDirection = GetActorLocation() - FocusActor->GetActorLocation(); DesiredDirection.Z = 0.0f; if (DesiredDirection.IsNearlyZero()) { DesiredDirection = GetActorForwardVector(); DesiredDirection.Z = 0.0f; } DesiredTarget = GetActorLocation() + DesiredDirection.GetSafeNormal() * FleeRadius; DesiredSpeed = FleeSpeed; } else if (WildlifeState == EAgrarianWildlifeState::Chasing && FocusActor) { DesiredTarget = FocusActor->GetActorLocation(); DesiredSpeed = FleeSpeed; } else if (WildlifeState == EAgrarianWildlifeState::Wandering) { DesiredTarget = CurrentMoveTarget; DesiredSpeed = WanderSpeed; } if (RequestNavigationMove(DesiredTarget)) { GetCharacterMovement()->MaxWalkSpeed = DesiredSpeed; return; } DirectMoveTowardLocation(DesiredTarget, DesiredSpeed); } bool AAgrarianWildlifeBase::ChooseReachableWanderTarget(FVector& OutTarget) const { if (!bUseNavigationMovement) { return false; } const UWorld* World = GetWorld(); const UNavigationSystemV1* NavigationSystem = World ? FNavigationSystem::GetCurrent(World) : nullptr; if (!NavigationSystem) { return false; } FNavLocation ReachableLocation; if (!NavigationSystem->GetRandomReachablePointInRadius(SpawnLocation, WanderRadius, ReachableLocation)) { return false; } OutTarget = ReachableLocation.Location; return true; } bool AAgrarianWildlifeBase::ProjectPointToNavigation(const FVector& CandidateLocation, FVector& OutProjectedLocation) const { if (!bUseNavigationMovement) { return false; } const UWorld* World = GetWorld(); const UNavigationSystemV1* NavigationSystem = World ? FNavigationSystem::GetCurrent(World) : nullptr; if (!NavigationSystem) { return false; } FNavLocation ProjectedLocation; if (!NavigationSystem->ProjectPointToNavigation(CandidateLocation, ProjectedLocation, NavigationProjectionExtent)) { return false; } OutProjectedLocation = ProjectedLocation.Location; return true; } bool AAgrarianWildlifeBase::RequestNavigationMove(const FVector& TargetLocation) { if (!bUseNavigationMovement) { return false; } AAIController* AIController = Cast(GetController()); if (!AIController) { return false; } FVector ProjectedTarget = TargetLocation; if (!ProjectPointToNavigation(TargetLocation, ProjectedTarget)) { return false; } if (FVector::DistSquared(GetActorLocation(), ProjectedTarget) <= FMath::Square(NavigationAcceptanceRadius)) { AIController->StopMovement(); ClearNavigationMove(); return true; } if (bHasNavigationMoveTarget && AIController->GetMoveStatus() != EPathFollowingStatus::Idle && FVector::DistSquared(LastNavigationMoveTarget, ProjectedTarget) <= FMath::Square(NavigationRepathDistance)) { return true; } const EPathFollowingRequestResult::Type MoveResult = AIController->MoveToLocation( ProjectedTarget, NavigationAcceptanceRadius, true, true, true, true, nullptr, true); if (MoveResult == EPathFollowingRequestResult::Failed) { ClearNavigationMove(); return false; } LastNavigationMoveTarget = ProjectedTarget; bHasNavigationMoveTarget = true; return true; } void AAgrarianWildlifeBase::ClearNavigationMove() { bHasNavigationMoveTarget = false; LastNavigationMoveTarget = FVector::ZeroVector; } void AAgrarianWildlifeBase::DirectMoveTowardLocation(const FVector& TargetLocation, float MovementSpeed) { ClearNavigationMove(); GetCharacterMovement()->MaxWalkSpeed = MovementSpeed; FVector DesiredDirection = TargetLocation - GetActorLocation(); DesiredDirection.Z = 0.0f; if (!DesiredDirection.IsNearlyZero()) { AddMovementInput(DesiredDirection.GetSafeNormal()); } } void AAgrarianWildlifeBase::EnterDeadState() { Health = 0.0f; SetWildlifeState(EAgrarianWildlifeState::Dead); if (AAIController* AIController = Cast(GetController())) { AIController->StopMovement(); } ClearNavigationMove(); GetCharacterMovement()->StopMovementImmediately(); GetCharacterMovement()->DisableMovement(); SetActorTickEnabled(false); SetLifeSpan(0.0f); } AAgrarianGameCharacter* AAgrarianWildlifeBase::FindNearestPlayer(float Radius) const { UWorld* World = GetWorld(); if (!World) { return nullptr; } AAgrarianGameCharacter* NearestCharacter = nullptr; float BestDistanceSq = FMath::Square(Radius); TArray PlayerPawns; UGameplayStatics::GetAllActorsOfClass(World, AAgrarianGameCharacter::StaticClass(), PlayerPawns); for (AActor* Actor : PlayerPawns) { AAgrarianGameCharacter* Candidate = Cast(Actor); if (!Candidate) { continue; } const float DistanceSq = FVector::DistSquared(GetActorLocation(), Candidate->GetActorLocation()); if (DistanceSq < BestDistanceSq) { BestDistanceSq = DistanceSq; NearestCharacter = Candidate; } } return NearestCharacter; } float AAgrarianWildlifeBase::GetNearestPlayerDistanceSquared() const { UWorld* World = GetWorld(); if (!World) { return TNumericLimits::Max(); } float BestDistanceSq = TNumericLimits::Max(); TArray PlayerPawns; UGameplayStatics::GetAllActorsOfClass(World, AAgrarianGameCharacter::StaticClass(), PlayerPawns); for (AActor* Actor : PlayerPawns) { const AAgrarianGameCharacter* Candidate = Cast(Actor); if (!Candidate) { continue; } BestDistanceSq = FMath::Min(BestDistanceSq, FVector::DistSquared(GetActorLocation(), Candidate->GetActorLocation())); } return BestDistanceSq; } bool AAgrarianWildlifeBase::Harvest(AAgrarianGameCharacter* Interactor) { if (!Interactor || WildlifeState != EAgrarianWildlifeState::Dead || bHarvested) { return false; } UAgrarianInventoryComponent* Inventory = Interactor->GetInventoryComponent(); if (!Inventory) { return false; } for (const FAgrarianItemStack& Yield : HarvestYields) { if (Yield.IsValidStack()) { Inventory->AddItem(Yield); } } bHarvested = true; MulticastPlayWildlifeHarvestSound(); return true; } void AAgrarianWildlifeBase::BroadcastHealthChanged() { OnWildlifeHealthChanged.Broadcast(Health, MaxHealth); } void AAgrarianWildlifeBase::BroadcastStateChanged() { OnWildlifeStateChanged.Broadcast(WildlifeState); } USoundBase* AAgrarianWildlifeBase::GetSoundForWildlifeState(EAgrarianWildlifeState State) const { switch (State) { case EAgrarianWildlifeState::Fleeing: case EAgrarianWildlifeState::Chasing: return FleeWildlifeSound; case EAgrarianWildlifeState::Dead: return DeathWildlifeSound; case EAgrarianWildlifeState::Idle: case EAgrarianWildlifeState::Wandering: default: return IdleWildlifeSound; } }