Add wildlife performance limits

This commit is contained in:
2026-05-18 14:21:34 -07:00
parent 494fe6f2ef
commit a834eb74c4
5 changed files with 151 additions and 2 deletions
+4 -1
View File
@@ -691,7 +691,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
max active population, spawn radius, respawn interval, and optional navigation max active population, spawn radius, respawn interval, and optional navigation
projection for MVP wildlife population control. projection for MVP wildlife population control.
- [x] Add replication. - [x] Add replication.
- [ ] Add performance limits. - [x] Add performance limits. Wildlife now has MVP server-side performance
throttling: nearby wildlife updates normally, far wildlife batches server
thinking on a slower interval, dead wildlife stops ticking, and spawn-manager
max active population remains the first population cap.
## 0.1.L Basic Multiplayer ## 0.1.L Basic Multiplayer
+8
View File
@@ -550,6 +550,14 @@ spawn radius, and respawn interval. Spawn points optionally project to navigatio
before spawning so the first wildlife prototype favors reachable positions while before spawning so the first wildlife prototype favors reachable positions while
still falling back cleanly on maps without complete nav data. still falling back cleanly on maps without complete nav data.
### Wildlife Performance Limits
MVP wildlife uses simple server-side performance guardrails before the later
AI-scaling pass. Nearby wildlife updates every tick for responsive flee/chase
behavior. Wildlife outside `FullUpdateRadius` batches server thinking on
`FarUpdateIntervalSeconds`, dead wildlife disables ticking, and spawn managers
enforce `MaxActiveWildlife` so prototype populations cannot grow without a cap.
### JSON Metadata ### JSON Metadata
Use JSON files for external terrain/tile pipeline metadata while the pipeline is Use JSON files for external terrain/tile pipeline metadata while the pipeline is
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Validate MVP wildlife performance-limit 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(content: str, needle: str, context: str) -> None:
if needle not in content:
raise AssertionError(f"Missing {needle!r} in {context}")
def main() -> int:
errors: list[str] = []
checks = {
"Source/AgrarianGame/AgrarianWildlifeBase.h": [
"bEnablePerformanceLimits",
"FullUpdateRadius",
"FarUpdateIntervalSeconds",
"ShouldRunServerThink",
"GetNearestPlayerDistanceSquared",
"ServerThinkAccumulator",
],
"Source/AgrarianGame/AgrarianWildlifeBase.cpp": [
"ShouldRunServerThink(DeltaSeconds)",
"ServerThinkAccumulator += DeltaSeconds",
"FMath::Square(FullUpdateRadius)",
"FarUpdateIntervalSeconds",
"SetActorTickEnabled(false)",
"TNumericLimits<float>::Max()",
],
"Source/AgrarianGame/AgrarianWildlifeSpawnManager.h": [
"MaxActiveWildlife",
],
"Source/AgrarianGame/AgrarianWildlifeSpawnManager.cpp": [
"GetActiveWildlifeCount() >= MaxActiveWildlife",
],
"AGRARIAN_DEVELOPMENT_ROADMAP.md": [
"[x] Add performance limits.",
"nearby wildlife updates normally",
"far wildlife batches server",
],
"Docs/TechnicalDesignDocument.md": [
"Wildlife Performance Limits",
"FullUpdateRadius",
"FarUpdateIntervalSeconds",
"MaxActiveWildlife",
],
}
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 performance limits are wired and documented.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+49 -1
View File
@@ -45,7 +45,12 @@ void AAgrarianWildlifeBase::Tick(float DeltaSeconds)
if (HasAuthority()) if (HasAuthority())
{ {
ServerThink(DeltaSeconds); if (ShouldRunServerThink(DeltaSeconds))
{
const float EffectiveDeltaSeconds = bEnablePerformanceLimits ? ServerThinkAccumulator : DeltaSeconds;
ServerThink(EffectiveDeltaSeconds);
ServerThinkAccumulator = 0.0f;
}
} }
} }
@@ -134,6 +139,22 @@ void AAgrarianWildlifeBase::OnRep_WildlifeState()
BroadcastStateChanged(); BroadcastStateChanged();
} }
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) void AAgrarianWildlifeBase::ServerThink(float DeltaSeconds)
{ {
if (!IsAlive()) if (!IsAlive())
@@ -354,6 +375,7 @@ void AAgrarianWildlifeBase::EnterDeadState()
ClearNavigationMove(); ClearNavigationMove();
GetCharacterMovement()->StopMovementImmediately(); GetCharacterMovement()->StopMovementImmediately();
GetCharacterMovement()->DisableMovement(); GetCharacterMovement()->DisableMovement();
SetActorTickEnabled(false);
SetLifeSpan(0.0f); SetLifeSpan(0.0f);
} }
@@ -389,6 +411,32 @@ AAgrarianGameCharacter* AAgrarianWildlifeBase::FindNearestPlayer(float Radius) c
return NearestCharacter; return NearestCharacter;
} }
float AAgrarianWildlifeBase::GetNearestPlayerDistanceSquared() const
{
UWorld* World = GetWorld();
if (!World)
{
return TNumericLimits<float>::Max();
}
float BestDistanceSq = TNumericLimits<float>::Max();
TArray<AActor*> PlayerPawns;
UGameplayStatics::GetAllActorsOfClass(World, AAgrarianGameCharacter::StaticClass(), PlayerPawns);
for (AActor* Actor : PlayerPawns)
{
const AAgrarianGameCharacter* Candidate = Cast<AAgrarianGameCharacter>(Actor);
if (!Candidate)
{
continue;
}
BestDistanceSq = FMath::Min(BestDistanceSq, FVector::DistSquared(GetActorLocation(), Candidate->GetActorLocation()));
}
return BestDistanceSq;
}
bool AAgrarianWildlifeBase::Harvest(AAgrarianGameCharacter* Interactor) bool AAgrarianWildlifeBase::Harvest(AAgrarianGameCharacter* Interactor)
{ {
if (!Interactor || WildlifeState != EAgrarianWildlifeState::Dead || bHarvested) if (!Interactor || WildlifeState != EAgrarianWildlifeState::Dead || bHarvested)
@@ -78,6 +78,15 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement")
FVector NavigationProjectionExtent = FVector(500.0f, 500.0f, 900.0f); FVector NavigationProjectionExtent = FVector(500.0f, 500.0f, 900.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Performance")
bool bEnablePerformanceLimits = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Performance", meta = (ClampMin = "0"))
float FullUpdateRadius = 6500.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Performance", meta = (ClampMin = "0.1"))
float FarUpdateIntervalSeconds = 2.5f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Harvest") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Harvest")
TArray<FAgrarianItemStack> HarvestYields; TArray<FAgrarianItemStack> HarvestYields;
@@ -106,6 +115,7 @@ protected:
UFUNCTION() UFUNCTION()
void OnRep_WildlifeState(); void OnRep_WildlifeState();
bool ShouldRunServerThink(float DeltaSeconds);
void ServerThink(float DeltaSeconds); void ServerThink(float DeltaSeconds);
void ChooseWanderTarget(); void ChooseWanderTarget();
void MoveTowardTarget(); void MoveTowardTarget();
@@ -116,6 +126,7 @@ protected:
void DirectMoveTowardLocation(const FVector& TargetLocation, float MovementSpeed); void DirectMoveTowardLocation(const FVector& TargetLocation, float MovementSpeed);
void EnterDeadState(); void EnterDeadState();
AAgrarianGameCharacter* FindNearestPlayer(float Radius) const; AAgrarianGameCharacter* FindNearestPlayer(float Radius) const;
float GetNearestPlayerDistanceSquared() const;
bool Harvest(AAgrarianGameCharacter* Interactor); bool Harvest(AAgrarianGameCharacter* Interactor);
void BroadcastHealthChanged(); void BroadcastHealthChanged();
void BroadcastStateChanged(); void BroadcastStateChanged();
@@ -134,4 +145,5 @@ protected:
bool bHasNavigationMoveTarget = false; bool bHasNavigationMoveTarget = false;
float DecisionTimer = 0.0f; float DecisionTimer = 0.0f;
float ServerThinkAccumulator = 0.0f;
}; };