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
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user