Add wildlife performance limits
This commit is contained in:
@@ -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
|
||||
projection for MVP wildlife population control.
|
||||
- [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
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
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())
|
||||
@@ -45,7 +45,12 @@ void AAgrarianWildlifeBase::Tick(float DeltaSeconds)
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -354,6 +375,7 @@ void AAgrarianWildlifeBase::EnterDeadState()
|
||||
ClearNavigationMove();
|
||||
GetCharacterMovement()->StopMovementImmediately();
|
||||
GetCharacterMovement()->DisableMovement();
|
||||
SetActorTickEnabled(false);
|
||||
SetLifeSpan(0.0f);
|
||||
}
|
||||
|
||||
@@ -389,6 +411,32 @@ AAgrarianGameCharacter* AAgrarianWildlifeBase::FindNearestPlayer(float Radius) c
|
||||
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)
|
||||
{
|
||||
if (!Interactor || WildlifeState != EAgrarianWildlifeState::Dead || bHarvested)
|
||||
|
||||
@@ -78,6 +78,15 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement")
|
||||
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")
|
||||
TArray<FAgrarianItemStack> HarvestYields;
|
||||
|
||||
@@ -106,6 +115,7 @@ protected:
|
||||
UFUNCTION()
|
||||
void OnRep_WildlifeState();
|
||||
|
||||
bool ShouldRunServerThink(float DeltaSeconds);
|
||||
void ServerThink(float DeltaSeconds);
|
||||
void ChooseWanderTarget();
|
||||
void MoveTowardTarget();
|
||||
@@ -116,6 +126,7 @@ protected:
|
||||
void DirectMoveTowardLocation(const FVector& TargetLocation, float MovementSpeed);
|
||||
void EnterDeadState();
|
||||
AAgrarianGameCharacter* FindNearestPlayer(float Radius) const;
|
||||
float GetNearestPlayerDistanceSquared() const;
|
||||
bool Harvest(AAgrarianGameCharacter* Interactor);
|
||||
void BroadcastHealthChanged();
|
||||
void BroadcastStateChanged();
|
||||
@@ -134,4 +145,5 @@ protected:
|
||||
|
||||
bool bHasNavigationMoveTarget = false;
|
||||
float DecisionTimer = 0.0f;
|
||||
float ServerThinkAccumulator = 0.0f;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user