Add replicated wildlife base actor
This commit is contained in:
@@ -28,6 +28,7 @@
|
|||||||
- [x] Persistence subsystem can capture and restore saveable world actors.
|
- [x] Persistence subsystem can capture and restore saveable world actors.
|
||||||
- [x] Primitive shelter actor is marked as a persistent world actor.
|
- [x] Primitive shelter actor is marked as a persistent world actor.
|
||||||
- [x] Admin/dev console commands added to the Agrarian player controller.
|
- [x] Admin/dev console commands added to the Agrarian player controller.
|
||||||
|
- [x] Wildlife base actor added with replicated health, simple movement states, and harvesting hooks.
|
||||||
|
|
||||||
## Next Unreal Editor Tasks
|
## Next Unreal Editor Tasks
|
||||||
|
|
||||||
@@ -51,4 +52,4 @@
|
|||||||
- [x] Add item definition data asset class.
|
- [x] Add item definition data asset class.
|
||||||
- [x] Add save/load capture for placed actors.
|
- [x] Add save/load capture for placed actors.
|
||||||
- [x] Add admin/dev console commands.
|
- [x] Add admin/dev console commands.
|
||||||
- [ ] Add wildlife base actor.
|
- [x] Add wildlife base actor.
|
||||||
|
|||||||
@@ -48,6 +48,24 @@ enum class EAgrarianItemType : uint8
|
|||||||
Currency UMETA(DisplayName = "Currency")
|
Currency UMETA(DisplayName = "Currency")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EAgrarianWildlifeDisposition : uint8
|
||||||
|
{
|
||||||
|
Passive UMETA(DisplayName = "Passive"),
|
||||||
|
Fleeing UMETA(DisplayName = "Fleeing"),
|
||||||
|
Aggressive UMETA(DisplayName = "Aggressive")
|
||||||
|
};
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EAgrarianWildlifeState : uint8
|
||||||
|
{
|
||||||
|
Idle UMETA(DisplayName = "Idle"),
|
||||||
|
Wandering UMETA(DisplayName = "Wandering"),
|
||||||
|
Fleeing UMETA(DisplayName = "Fleeing"),
|
||||||
|
Chasing UMETA(DisplayName = "Chasing"),
|
||||||
|
Dead UMETA(DisplayName = "Dead")
|
||||||
|
};
|
||||||
|
|
||||||
USTRUCT(BlueprintType)
|
USTRUCT(BlueprintType)
|
||||||
struct FAgrarianItemDefinition
|
struct FAgrarianItemDefinition
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
// Copyright Pacificao. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "AgrarianWildlifeBase.h"
|
||||||
|
#include "AgrarianGameCharacter.h"
|
||||||
|
#include "AgrarianInventoryComponent.h"
|
||||||
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
#include "Net/UnrealNetwork.h"
|
||||||
|
|
||||||
|
AAgrarianWildlifeBase::AAgrarianWildlifeBase()
|
||||||
|
{
|
||||||
|
PrimaryActorTick.bCanEverTick = true;
|
||||||
|
bReplicates = true;
|
||||||
|
|
||||||
|
GetCharacterMovement()->MaxWalkSpeed = WanderSpeed;
|
||||||
|
GetCharacterMovement()->bOrientRotationToMovement = true;
|
||||||
|
bUseControllerRotationYaw = false;
|
||||||
|
|
||||||
|
DisplayName = FText::FromString(TEXT("Wildlife"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AAgrarianWildlifeBase::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
SpawnLocation = GetActorLocation();
|
||||||
|
Health = FMath::Clamp(Health, 0.0f, MaxHealth);
|
||||||
|
ChooseWanderTarget();
|
||||||
|
BroadcastHealthChanged();
|
||||||
|
BroadcastStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AAgrarianWildlifeBase::Tick(float DeltaSeconds)
|
||||||
|
{
|
||||||
|
Super::Tick(DeltaSeconds);
|
||||||
|
|
||||||
|
if (HasAuthority())
|
||||||
|
{
|
||||||
|
ServerThink(DeltaSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AAgrarianWildlifeBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& 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;
|
||||||
|
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::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()
|
||||||
|
{
|
||||||
|
const FVector RandomOffset = FVector(
|
||||||
|
FMath::FRandRange(-WanderRadius, WanderRadius),
|
||||||
|
FMath::FRandRange(-WanderRadius, WanderRadius),
|
||||||
|
0.0f);
|
||||||
|
|
||||||
|
CurrentMoveTarget = SpawnLocation + RandomOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AAgrarianWildlifeBase::MoveTowardTarget()
|
||||||
|
{
|
||||||
|
FVector DesiredDirection = FVector::ZeroVector;
|
||||||
|
|
||||||
|
if (WildlifeState == EAgrarianWildlifeState::Fleeing && FocusActor)
|
||||||
|
{
|
||||||
|
DesiredDirection = GetActorLocation() - FocusActor->GetActorLocation();
|
||||||
|
GetCharacterMovement()->MaxWalkSpeed = FleeSpeed;
|
||||||
|
}
|
||||||
|
else if (WildlifeState == EAgrarianWildlifeState::Chasing && FocusActor)
|
||||||
|
{
|
||||||
|
DesiredDirection = FocusActor->GetActorLocation() - GetActorLocation();
|
||||||
|
GetCharacterMovement()->MaxWalkSpeed = FleeSpeed;
|
||||||
|
}
|
||||||
|
else if (WildlifeState == EAgrarianWildlifeState::Wandering)
|
||||||
|
{
|
||||||
|
DesiredDirection = CurrentMoveTarget - GetActorLocation();
|
||||||
|
GetCharacterMovement()->MaxWalkSpeed = WanderSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
DesiredDirection.Z = 0.0f;
|
||||||
|
if (!DesiredDirection.IsNearlyZero())
|
||||||
|
{
|
||||||
|
AddMovementInput(DesiredDirection.GetSafeNormal());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AAgrarianWildlifeBase::EnterDeadState()
|
||||||
|
{
|
||||||
|
Health = 0.0f;
|
||||||
|
SetWildlifeState(EAgrarianWildlifeState::Dead);
|
||||||
|
GetCharacterMovement()->StopMovementImmediately();
|
||||||
|
GetCharacterMovement()->DisableMovement();
|
||||||
|
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<AActor*> PlayerPawns;
|
||||||
|
UGameplayStatics::GetAllActorsOfClass(World, AAgrarianGameCharacter::StaticClass(), PlayerPawns);
|
||||||
|
for (AActor* Actor : PlayerPawns)
|
||||||
|
{
|
||||||
|
AAgrarianGameCharacter* Candidate = Cast<AAgrarianGameCharacter>(Actor);
|
||||||
|
if (!Candidate)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float DistanceSq = FVector::DistSquared(GetActorLocation(), Candidate->GetActorLocation());
|
||||||
|
if (DistanceSq < BestDistanceSq)
|
||||||
|
{
|
||||||
|
BestDistanceSq = DistanceSq;
|
||||||
|
NearestCharacter = Candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NearestCharacter;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AAgrarianWildlifeBase::BroadcastHealthChanged()
|
||||||
|
{
|
||||||
|
OnWildlifeHealthChanged.Broadcast(Health, MaxHealth);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AAgrarianWildlifeBase::BroadcastStateChanged()
|
||||||
|
{
|
||||||
|
OnWildlifeStateChanged.Broadcast(WildlifeState);
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright Pacificao. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Character.h"
|
||||||
|
#include "AgrarianInteractable.h"
|
||||||
|
#include "AgrarianTypes.h"
|
||||||
|
#include "AgrarianWildlifeBase.generated.h"
|
||||||
|
|
||||||
|
class AAgrarianGameCharacter;
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianWildlifeStateChangedSignature, EAgrarianWildlifeState, NewState);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAgrarianWildlifeHealthChangedSignature, float, Health, float, MaxHealth);
|
||||||
|
|
||||||
|
UCLASS(Blueprintable)
|
||||||
|
class AAgrarianWildlifeBase : public ACharacter, public IAgrarianInteractable
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
AAgrarianWildlifeBase();
|
||||||
|
|
||||||
|
virtual void Tick(float DeltaSeconds) override;
|
||||||
|
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Agrarian|Wildlife")
|
||||||
|
FAgrarianWildlifeStateChangedSignature OnWildlifeStateChanged;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "Agrarian|Wildlife")
|
||||||
|
FAgrarianWildlifeHealthChangedSignature OnWildlifeHealthChanged;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife")
|
||||||
|
FName WildlifeId = TEXT("wildlife");
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife")
|
||||||
|
FText DisplayName;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife")
|
||||||
|
EAgrarianWildlifeDisposition Disposition = EAgrarianWildlifeDisposition::Passive;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_WildlifeState, Category = "Agrarian|Wildlife")
|
||||||
|
EAgrarianWildlifeState WildlifeState = EAgrarianWildlifeState::Idle;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife", meta = (ClampMin = "1"))
|
||||||
|
float MaxHealth = 25.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_Health, Category = "Agrarian|Wildlife", meta = (ClampMin = "0"))
|
||||||
|
float Health = 25.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0"))
|
||||||
|
float WanderRadius = 1200.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0"))
|
||||||
|
float WanderSpeed = 180.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0"))
|
||||||
|
float FleeSpeed = 420.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0"))
|
||||||
|
float AggroRadius = 650.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0"))
|
||||||
|
float FleeRadius = 700.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0.1"))
|
||||||
|
float DecisionIntervalSeconds = 2.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Harvest")
|
||||||
|
TArray<FAgrarianItemStack> HarvestYields;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Wildlife|Harvest")
|
||||||
|
bool bHarvested = false;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Wildlife")
|
||||||
|
bool IsAlive() const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Wildlife")
|
||||||
|
void ApplyWildlifeDamage(float Amount, AActor* InstigatorActor);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Wildlife")
|
||||||
|
void SetWildlifeState(EAgrarianWildlifeState NewState);
|
||||||
|
|
||||||
|
virtual FText GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const override;
|
||||||
|
virtual bool CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const override;
|
||||||
|
virtual void Interact_Implementation(AAgrarianGameCharacter* Interactor) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_Health();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_WildlifeState();
|
||||||
|
|
||||||
|
void ServerThink(float DeltaSeconds);
|
||||||
|
void ChooseWanderTarget();
|
||||||
|
void MoveTowardTarget();
|
||||||
|
void EnterDeadState();
|
||||||
|
AAgrarianGameCharacter* FindNearestPlayer(float Radius) const;
|
||||||
|
bool Harvest(AAgrarianGameCharacter* Interactor);
|
||||||
|
void BroadcastHealthChanged();
|
||||||
|
void BroadcastStateChanged();
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FVector SpawnLocation = FVector::ZeroVector;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FVector CurrentMoveTarget = FVector::ZeroVector;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<AActor> FocusActor;
|
||||||
|
|
||||||
|
float DecisionTimer = 0.0f;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user