Setup NPC director
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
// © 2025 Naked People Team. All Rights Reserved.
|
||||
|
||||
|
||||
#include "NPCDirectorSubsystem.h"
|
||||
|
||||
#include "NPC.h"
|
||||
#include "NPCDirectorConfig.h"
|
||||
#include "NavigationSystem.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "NakedDesire/Global/NakedDesireGameInstance.h"
|
||||
|
||||
void UNPCDirectorSubsystem::OnWorldBeginPlay(UWorld& InWorld)
|
||||
{
|
||||
Super::OnWorldBeginPlay(InWorld);
|
||||
|
||||
if (!InWorld.IsGameWorld())
|
||||
return;
|
||||
|
||||
const UNPCDirectorConfig* Config = GetConfig();
|
||||
if (!Config || Config->SpawnTable.Num() == 0)
|
||||
return;
|
||||
|
||||
if (UTimeOfDaySubsystem* Time = InWorld.GetSubsystem<UTimeOfDaySubsystem>())
|
||||
{
|
||||
Time->OnPhaseChanged.AddUniqueDynamic(this, &UNPCDirectorSubsystem::HandlePhaseChanged);
|
||||
CachedPhase = Time->GetPhase();
|
||||
}
|
||||
|
||||
PrewarmPool();
|
||||
|
||||
const float Interval = FMath::Max(Config->UpdateInterval, 0.05f);
|
||||
InWorld.GetTimerManager().SetTimer(
|
||||
UpdateTimerHandle, this, &UNPCDirectorSubsystem::UpdatePopulation, Interval, true);
|
||||
|
||||
UpdatePopulation();
|
||||
}
|
||||
|
||||
void UNPCDirectorSubsystem::Deinitialize()
|
||||
{
|
||||
if (const UWorld* World = GetWorld())
|
||||
{
|
||||
World->GetTimerManager().ClearTimer(UpdateTimerHandle);
|
||||
if (UTimeOfDaySubsystem* Time = World->GetSubsystem<UTimeOfDaySubsystem>())
|
||||
Time->OnPhaseChanged.RemoveDynamic(this, &UNPCDirectorSubsystem::HandlePhaseChanged);
|
||||
}
|
||||
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
void UNPCDirectorSubsystem::HandlePhaseChanged(EDayPhase NewPhase)
|
||||
{
|
||||
// The timer applies the new target on its next reconcile; just cache it.
|
||||
CachedPhase = NewPhase;
|
||||
}
|
||||
|
||||
void UNPCDirectorSubsystem::PrewarmPool()
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
const UNPCDirectorConfig* Config = GetConfig();
|
||||
if (!World || !Config)
|
||||
return;
|
||||
|
||||
FActorSpawnParameters Params;
|
||||
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
|
||||
for (int32 i = 0; i < Config->MaxNPCs; ++i)
|
||||
{
|
||||
const TSubclassOf<ANPC> NPCClass = PickWeightedClass();
|
||||
if (!NPCClass)
|
||||
continue;
|
||||
|
||||
ANPC* NPC = World->SpawnActor<ANPC>(NPCClass, FVector::ZeroVector, FRotator::ZeroRotator, Params);
|
||||
if (!NPC)
|
||||
continue;
|
||||
|
||||
NPC->DeactivateToPool();
|
||||
Pool.Add(NPC);
|
||||
}
|
||||
}
|
||||
|
||||
void UNPCDirectorSubsystem::UpdatePopulation()
|
||||
{
|
||||
const APawn* Player = GetPlayerPawn();
|
||||
const UNPCDirectorConfig* Config = GetConfig();
|
||||
if (!Player || !Config)
|
||||
return;
|
||||
|
||||
const FVector PlayerLocation = Player->GetActorLocation();
|
||||
const float DespawnRadiusSq = FMath::Square(Config->DespawnRadius);
|
||||
|
||||
// 1. Recycle anything that wandered (or was left) beyond the despawn radius.
|
||||
for (int32 i = Active.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
ANPC* NPC = Active[i];
|
||||
if (!NPC || FVector::DistSquared(NPC->GetActorLocation(), PlayerLocation) > DespawnRadiusSq)
|
||||
ReturnToPool(NPC);
|
||||
}
|
||||
|
||||
const int32 Target = CurrentTargetCount();
|
||||
|
||||
// 2. Over target (e.g. day -> night drop): recycle the farthest live NPCs down to target.
|
||||
while (Active.Num() > Target)
|
||||
{
|
||||
int32 FarthestIdx = INDEX_NONE;
|
||||
float FarthestDistSq = -1.0f;
|
||||
for (int32 i = 0; i < Active.Num(); ++i)
|
||||
{
|
||||
const float DistSq = FVector::DistSquared(Active[i]->GetActorLocation(), PlayerLocation);
|
||||
if (DistSq > FarthestDistSq)
|
||||
{
|
||||
FarthestDistSq = DistSq;
|
||||
FarthestIdx = i;
|
||||
}
|
||||
}
|
||||
if (FarthestIdx == INDEX_NONE)
|
||||
break;
|
||||
ReturnToPool(Active[FarthestIdx]);
|
||||
}
|
||||
|
||||
// 3. Under target: activate pooled NPCs at NavMesh points in the spawn ring.
|
||||
while (Active.Num() < Target && Pool.Num() > 0)
|
||||
{
|
||||
FVector SpawnLocation;
|
||||
if (!FindSpawnPoint(PlayerLocation, SpawnLocation))
|
||||
break; // no reachable point this tick; try again next reconcile
|
||||
|
||||
ANPC* NPC = TakeFromPool();
|
||||
if (!NPC)
|
||||
break;
|
||||
|
||||
// Face away from the player so a newly activated NPC reads as walking into the scene.
|
||||
const FRotator Facing = (SpawnLocation - PlayerLocation).GetSafeNormal2D().Rotation();
|
||||
NPC->ActivateFromPool(SpawnLocation, Facing);
|
||||
Active.Add(NPC);
|
||||
}
|
||||
}
|
||||
|
||||
int32 UNPCDirectorSubsystem::CurrentTargetCount() const
|
||||
{
|
||||
const UNPCDirectorConfig* Config = GetConfig();
|
||||
if (!Config)
|
||||
return 0;
|
||||
|
||||
const int32 PhaseTarget = (CachedPhase == EDayPhase::Day) ? Config->TargetCountDay : Config->TargetCountNight;
|
||||
// Can never exceed what the pool can hold.
|
||||
return FMath::Clamp(PhaseTarget, 0, Config->MaxNPCs);
|
||||
}
|
||||
|
||||
ANPC* UNPCDirectorSubsystem::TakeFromPool()
|
||||
{
|
||||
while (Pool.Num() > 0)
|
||||
{
|
||||
ANPC* NPC = Pool.Pop(EAllowShrinking::No);
|
||||
if (NPC)
|
||||
return NPC;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UNPCDirectorSubsystem::ReturnToPool(ANPC* NPC)
|
||||
{
|
||||
Active.RemoveSwap(NPC);
|
||||
if (NPC)
|
||||
{
|
||||
NPC->DeactivateToPool();
|
||||
Pool.Add(NPC);
|
||||
}
|
||||
}
|
||||
|
||||
TSubclassOf<ANPC> UNPCDirectorSubsystem::PickWeightedClass() const
|
||||
{
|
||||
const UNPCDirectorConfig* Config = GetConfig();
|
||||
if (!Config)
|
||||
return nullptr;
|
||||
|
||||
float TotalWeight = 0.0f;
|
||||
for (const FNPCSpawnEntry& Entry : Config->SpawnTable)
|
||||
{
|
||||
if (Entry.NPCClass && Entry.Weight > 0.0f)
|
||||
TotalWeight += Entry.Weight;
|
||||
}
|
||||
if (TotalWeight <= 0.0f)
|
||||
return nullptr;
|
||||
|
||||
float Roll = FMath::FRandRange(0.0f, TotalWeight);
|
||||
for (const FNPCSpawnEntry& Entry : Config->SpawnTable)
|
||||
{
|
||||
if (!Entry.NPCClass || Entry.Weight <= 0.0f)
|
||||
continue;
|
||||
Roll -= Entry.Weight;
|
||||
if (Roll <= 0.0f)
|
||||
return Entry.NPCClass;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool UNPCDirectorSubsystem::FindSpawnPoint(const FVector& Around, FVector& OutLocation) const
|
||||
{
|
||||
UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
|
||||
const UNPCDirectorConfig* Config = GetConfig();
|
||||
if (!NavSys || !Config)
|
||||
return false;
|
||||
|
||||
const float MinRadiusSq = FMath::Square(Config->SpawnRadiusMin);
|
||||
|
||||
// Rejection-sample a reachable point in the outer radius until one lands outside the inner radius.
|
||||
for (int32 Attempt = 0; Attempt < 8; ++Attempt)
|
||||
{
|
||||
FNavLocation NavLocation;
|
||||
if (NavSys->GetRandomReachablePointInRadius(Around, Config->SpawnRadiusMax, NavLocation) &&
|
||||
FVector::DistSquared(NavLocation.Location, Around) >= MinRadiusSq)
|
||||
{
|
||||
OutLocation = NavLocation.Location;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
APawn* UNPCDirectorSubsystem::GetPlayerPawn() const
|
||||
{
|
||||
return UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
|
||||
}
|
||||
|
||||
UNPCDirectorConfig* UNPCDirectorSubsystem::GetConfig() const
|
||||
{
|
||||
const UNakedDesireGameInstance* GameInstance =
|
||||
Cast<UNakedDesireGameInstance>(GetWorld() ? GetWorld()->GetGameInstance() : nullptr);
|
||||
return GameInstance ? GameInstance->NPCDirector : nullptr;
|
||||
}
|
||||
Reference in New Issue
Block a user