Add wildlife spawn manager

This commit is contained in:
2026-05-18 14:16:50 -07:00
parent f0d8e3ef05
commit 494fe6f2ef
5 changed files with 304 additions and 1 deletions
+4 -1
View File
@@ -686,7 +686,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
- [x] Add damage.
- [x] Add harvesting interaction.
- [x] Add meat/hide resources.
- [ ] Add spawn manager.
- [x] Add spawn manager. Added a replicated, server-authoritative wildlife
spawn manager actor with configurable wildlife class, initial spawn count,
max active population, spawn radius, respawn interval, and optional navigation
projection for MVP wildlife population control.
- [x] Add replication.
- [ ] Add performance limits.
+8
View File
@@ -542,6 +542,14 @@ tile has no nav data, wildlife falls back to direct movement input so damage,
flee, chase, and harvest loops remain functional while map navigation is being
authored.
### Wildlife Spawning
`AAgrarianWildlifeSpawnManager` owns MVP wildlife population seeding on the
server. Designers can assign a wildlife class, initial count, max active count,
spawn radius, and respawn interval. Spawn points optionally project to navigation
before spawning so the first wildlife prototype favors reachable positions while
still falling back cleanly on maps without complete nav data.
### JSON Metadata
Use JSON files for external terrain/tile pipeline metadata while the pipeline is
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Validate MVP wildlife spawn manager wiring."""
from pathlib import Path
import sys
ROOT = Path(__file__).resolve().parents[1]
def read(relative_path: str) -> str:
path = ROOT / relative_path
if not path.exists():
raise AssertionError(f"Missing required file: {relative_path}")
return path.read_text(encoding="utf-8")
def require(haystack: str, needle: str, context: str) -> None:
if needle not in haystack:
raise AssertionError(f"Missing {needle!r} in {context}")
def main() -> int:
errors: list[str] = []
checks = {
"Source/AgrarianGame/AgrarianWildlifeSpawnManager.h": [
"class AAgrarianWildlifeSpawnManager",
"TSubclassOf<AAgrarianWildlifeBase> WildlifeClass",
"InitialSpawnCount",
"MaxActiveWildlife",
"SpawnRadius",
"SpawnIntervalSeconds",
"bProjectSpawnsToNavigation",
"SpawnWildlife()",
"GetActiveWildlifeCount()",
"TArray<TObjectPtr<AAgrarianWildlifeBase>> SpawnedWildlife",
],
"Source/AgrarianGame/AgrarianWildlifeSpawnManager.cpp": [
"bReplicates = true",
"DOREPLIFETIME(AAgrarianWildlifeSpawnManager, SpawnedWildlife)",
"HasAuthority()",
"SpawnInitialWildlife()",
"GetActiveWildlifeCount() >= MaxActiveWildlife",
"ProjectPointToNavigation",
"SpawnActor<AAgrarianWildlifeBase>",
"AdjustIfPossibleButAlwaysSpawn",
"RemoveInvalidSpawnedWildlife()",
],
"AGRARIAN_DEVELOPMENT_ROADMAP.md": [
"[x] Add spawn manager.",
"server-authoritative wildlife",
"max active population",
],
"Docs/TechnicalDesignDocument.md": [
"Wildlife Spawning",
"AAgrarianWildlifeSpawnManager",
"spawn radius",
"respawn interval",
],
}
for relative_path, needles in checks.items():
try:
content = read(relative_path)
for needle in needles:
require(content, needle, relative_path)
except AssertionError as exc:
errors.append(str(exc))
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1
print("PASS: wildlife spawn manager is wired and documented.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,151 @@
// Copyright Pacificao. All Rights Reserved.
#include "AgrarianWildlifeSpawnManager.h"
#include "AgrarianWildlifeBase.h"
#include "NavigationSystem.h"
#include "Net/UnrealNetwork.h"
AAgrarianWildlifeSpawnManager::AAgrarianWildlifeSpawnManager()
{
PrimaryActorTick.bCanEverTick = true;
bReplicates = true;
SetReplicatingMovement(false);
}
void AAgrarianWildlifeSpawnManager::BeginPlay()
{
Super::BeginPlay();
SpawnTimer = SpawnIntervalSeconds;
if (HasAuthority() && bSpawnOnBeginPlay)
{
SpawnInitialWildlife();
}
}
void AAgrarianWildlifeSpawnManager::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (!HasAuthority() || !WildlifeClass)
{
return;
}
RemoveInvalidSpawnedWildlife();
if (GetActiveWildlifeCount() >= MaxActiveWildlife)
{
return;
}
SpawnTimer -= DeltaSeconds;
if (SpawnTimer <= 0.0f)
{
SpawnTimer = SpawnIntervalSeconds;
SpawnWildlife();
}
}
void AAgrarianWildlifeSpawnManager::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AAgrarianWildlifeSpawnManager, SpawnedWildlife);
}
AAgrarianWildlifeBase* AAgrarianWildlifeSpawnManager::SpawnWildlife()
{
if (!HasAuthority() || !WildlifeClass || GetActiveWildlifeCount() >= MaxActiveWildlife)
{
return nullptr;
}
FVector SpawnLocation = GetActorLocation();
if (!ChooseSpawnLocation(SpawnLocation))
{
return nullptr;
}
FActorSpawnParameters SpawnParameters;
SpawnParameters.Owner = this;
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
AAgrarianWildlifeBase* SpawnedActor = GetWorld()->SpawnActor<AAgrarianWildlifeBase>(
WildlifeClass,
SpawnLocation,
FRotator::ZeroRotator,
SpawnParameters);
if (SpawnedActor)
{
SpawnedWildlife.Add(SpawnedActor);
}
return SpawnedActor;
}
int32 AAgrarianWildlifeSpawnManager::GetActiveWildlifeCount() const
{
int32 ActiveCount = 0;
for (AAgrarianWildlifeBase* Wildlife : SpawnedWildlife)
{
if (IsValid(Wildlife))
{
++ActiveCount;
}
}
return ActiveCount;
}
void AAgrarianWildlifeSpawnManager::SpawnInitialWildlife()
{
const int32 TargetCount = FMath::Min(InitialSpawnCount, MaxActiveWildlife);
for (int32 SpawnIndex = GetActiveWildlifeCount(); SpawnIndex < TargetCount; ++SpawnIndex)
{
if (!SpawnWildlife())
{
break;
}
}
}
bool AAgrarianWildlifeSpawnManager::ChooseSpawnLocation(FVector& OutLocation) const
{
const FVector RandomOffset(
FMath::FRandRange(-SpawnRadius, SpawnRadius),
FMath::FRandRange(-SpawnRadius, SpawnRadius),
0.0f);
const FVector CandidateLocation = GetActorLocation() + RandomOffset;
if (!bProjectSpawnsToNavigation)
{
OutLocation = CandidateLocation;
return true;
}
const UWorld* World = GetWorld();
const UNavigationSystemV1* NavigationSystem = World ? FNavigationSystem::GetCurrent<UNavigationSystemV1>(World) : nullptr;
if (!NavigationSystem)
{
OutLocation = CandidateLocation;
return true;
}
FNavLocation ProjectedLocation;
if (!NavigationSystem->ProjectPointToNavigation(CandidateLocation, ProjectedLocation, NavigationProjectionExtent))
{
return false;
}
OutLocation = ProjectedLocation.Location;
return true;
}
void AAgrarianWildlifeSpawnManager::RemoveInvalidSpawnedWildlife()
{
SpawnedWildlife.RemoveAll([](const TObjectPtr<AAgrarianWildlifeBase>& Wildlife)
{
return !IsValid(Wildlife);
});
}
@@ -0,0 +1,62 @@
// Copyright Pacificao. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "AgrarianWildlifeSpawnManager.generated.h"
class AAgrarianWildlifeBase;
UCLASS(Blueprintable)
class AAgrarianWildlifeSpawnManager : public AActor
{
GENERATED_BODY()
public:
AAgrarianWildlifeSpawnManager();
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Spawning")
TSubclassOf<AAgrarianWildlifeBase> WildlifeClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Spawning", meta = (ClampMin = "0"))
int32 InitialSpawnCount = 3;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Spawning", meta = (ClampMin = "0"))
int32 MaxActiveWildlife = 6;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Spawning", meta = (ClampMin = "0"))
float SpawnRadius = 3500.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Spawning", meta = (ClampMin = "0.1"))
float SpawnIntervalSeconds = 20.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Spawning")
bool bSpawnOnBeginPlay = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Spawning")
bool bProjectSpawnsToNavigation = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Spawning")
FVector NavigationProjectionExtent = FVector(650.0f, 650.0f, 900.0f);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Wildlife|Spawning")
AAgrarianWildlifeBase* SpawnWildlife();
UFUNCTION(BlueprintCallable, Category = "Agrarian|Wildlife|Spawning")
int32 GetActiveWildlifeCount() const;
protected:
void SpawnInitialWildlife();
bool ChooseSpawnLocation(FVector& OutLocation) const;
void RemoveInvalidSpawnedWildlife();
UPROPERTY(Replicated)
TArray<TObjectPtr<AAgrarianWildlifeBase>> SpawnedWildlife;
float SpawnTimer = 0.0f;
};