Estende o componente (que ja existia como debug overlay) com: - TSet<int64> VisibleEntityIds_ espelhando ENT_SPAWN/ENT_DESPAWN do V1 - Delegates BlueprintAssignable: OnEntityEnteredAOI(EntityId) + OnEntityExitedAOI(EntityId, Reason) - BlueprintPure: GetVisibleEntityCount, GetVisibleEntityIds, IsEntityVisible, GetSelfEntityId - Bind/unbind dos delegates DYNAMIC do UZeusNetworkingClientSubsystem (AddDynamic/RemoveDynamic) no BeginPlay/EndPlay - Self-entity filtrada via OnSelfEntityAssigned -- proprio char nao trackeia Sem histerese client-side (server ja filtra com margem ~2048cm; se aparecer flapping visual, adicionar depois). Memoria: project_aoi_client_component. NAO testado in-game. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
420 lines
15 KiB
C++
420 lines
15 KiB
C++
// Copyright Zeus Server Engine. All rights reserved.
|
|
|
|
#include "ZeusAOIComponent.h"
|
|
|
|
#include "ZeusNetworkingClientSubsystem.h" // V1 canonico: OnDebugAoiInfo (6161) + EmitDebugAoiRequest (6160)
|
|
#include "ZeusFrontierOverlayActor.h" // ator que desenha as paredes de fronteira
|
|
#include "ZMMONetLog.h" // Batch 2.5: LogZeusAOI (categoria do modulo ZMMO)
|
|
|
|
#include "DrawDebugHelpers.h"
|
|
#include "Engine/GameInstance.h"
|
|
#include "Engine/World.h"
|
|
#include "GameFramework/Actor.h"
|
|
|
|
// Console vars — espelham as flags do painel (overlay liga se flag OU CVar).
|
|
static TAutoConsoleVariable<int32> CVarShowAOI(
|
|
TEXT("zeus.debug.aoi"), 0, TEXT("Desenha a Zona de Interesse (esfera do raio AOI)."), ECVF_Default);
|
|
static TAutoConsoleVariable<int32> CVarShowDespawn(
|
|
TEXT("zeus.debug.despawn"), 0, TEXT("Desenha a Zona de Despawn (esfera externa do AOI)."), ECVF_Default);
|
|
static TAutoConsoleVariable<int32> CVarShowCell(
|
|
TEXT("zeus.debug.cell"), 0, TEXT("Desenha os bounds da celula (placeholder)."), ECVF_Default);
|
|
static TAutoConsoleVariable<int32> CVarShowProxy(
|
|
TEXT("zeus.debug.proxy"), 0, TEXT("Desenha labels de proxy (TODO)."), ECVF_Default);
|
|
static TAutoConsoleVariable<int32> CVarShowHandoff(
|
|
TEXT("zeus.debug.handoff"), 0, TEXT("Desenha a linha de handoff (placeholder)."), ECVF_Default);
|
|
static TAutoConsoleVariable<int32> CVarShowDrift(
|
|
TEXT("zeus.debug.drift"), 0, TEXT("Desenha o drift autoritativo (TODO)."), ECVF_Default);
|
|
static TAutoConsoleVariable<int32> CVarShowFrontier(
|
|
TEXT("zeus.debug.frontier"), 0, TEXT("Desenha as paredes das fronteiras do mesh (own<->neighbor)."), ECVF_Default);
|
|
|
|
namespace
|
|
{
|
|
const FColor kColorInterest(93, 213, 255); // cyan — Zona de Interesse
|
|
const FColor kColorDespawn(255, 140, 90); // laranja — Zona de Despawn (externa)
|
|
const FColor kColorCell(232, 192, 96);
|
|
const FColor kColorHandoff(255, 155, 190);
|
|
|
|
// Resolucao das esferas de overlay. 32 segments deixavam a "bolha" facetada
|
|
// (poucas linhas) num raio de 60-80m; 48 deixa o circulo bem mais denso/legivel.
|
|
constexpr int32 kSphereSegments = 48;
|
|
// Clamp visual defensivo: se o raio vier absurdo (config corrompida), nao desenha
|
|
// uma esfera gigante que engole o mapa. 500m cobre qualquer AOI real (max=800m,
|
|
// mas overlay > 500m ja' nao ajuda a leitura). So' afeta o desenho, nao o gameplay.
|
|
constexpr float kMaxOverlayRadiusCm = 50000.0f;
|
|
}
|
|
|
|
UZeusAOIComponent::UZeusAOIComponent()
|
|
{
|
|
PrimaryComponentTick.bCanEverTick = true;
|
|
PrimaryComponentTick.bStartWithTickEnabled = true;
|
|
SetIsReplicatedByDefault(false); // debug/cliente; gameplay AOI real entra depois
|
|
}
|
|
|
|
void UZeusAOIComponent::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
// Bind do feed de config de AOI (servidor -> cliente) pelo transporte V1
|
|
// canonico. O legacy (UZeusNetworkSubsystem) esta desativado no V1 -> usava
|
|
// SendDebugAoiRequest que logava "ignorado: nao conectado" e o overlay nunca
|
|
// recebia a config. Agora via UZeusNetworkingClientSubsystem (OnDebugAoiInfo 6161).
|
|
if (UWorld* World = GetWorld())
|
|
{
|
|
if (UGameInstance* GI = World->GetGameInstance())
|
|
{
|
|
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
|
|
{
|
|
AoiConfigHandle_ = NetC->OnDebugAoiInfo.AddUObject(this, &UZeusAOIComponent::HandleAoiConfig);
|
|
FrontierConfigHandle_ = NetC->OnDebugFrontierInfo.AddUObject(this, &UZeusAOIComponent::HandleFrontierConfig);
|
|
|
|
// 1.4 -- visibility tracking. Delegates DYNAMIC do subsystem V1
|
|
// precisam AddDynamic + UFUNCTION nas handlers (assim como no
|
|
// ZeusWorldSubsystem que ja faz spawn/despawn de proxies).
|
|
NetC->OnEntitySpawned.AddDynamic(this, &UZeusAOIComponent::HandleEntitySpawnedV1);
|
|
NetC->OnEntityDespawned.AddDynamic(this, &UZeusAOIComponent::HandleEntityDespawnedV1);
|
|
NetC->OnSelfEntityAssigned.AddDynamic(this, &UZeusAOIComponent::HandleSelfEntityAssignedV1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Best-effort: ja' pede a config (no-op se ainda nao conectado — o request
|
|
// e' re-disparado ao ligar um overlay em SetOverlayEnabled).
|
|
RequestAoiConfig();
|
|
}
|
|
|
|
void UZeusAOIComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|
{
|
|
if (AoiConfigHandle_.IsValid() || FrontierConfigHandle_.IsValid())
|
|
{
|
|
if (UWorld* World = GetWorld())
|
|
{
|
|
if (UGameInstance* GI = World->GetGameInstance())
|
|
{
|
|
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
|
|
{
|
|
if (AoiConfigHandle_.IsValid()) { NetC->OnDebugAoiInfo.Remove(AoiConfigHandle_); }
|
|
if (FrontierConfigHandle_.IsValid()) { NetC->OnDebugFrontierInfo.Remove(FrontierConfigHandle_); }
|
|
// 1.4 -- unbind dos delegates DYNAMIC de tracking.
|
|
NetC->OnEntitySpawned.RemoveDynamic(this, &UZeusAOIComponent::HandleEntitySpawnedV1);
|
|
NetC->OnEntityDespawned.RemoveDynamic(this, &UZeusAOIComponent::HandleEntityDespawnedV1);
|
|
NetC->OnSelfEntityAssigned.RemoveDynamic(this, &UZeusAOIComponent::HandleSelfEntityAssignedV1);
|
|
}
|
|
}
|
|
}
|
|
AoiConfigHandle_.Reset();
|
|
FrontierConfigHandle_.Reset();
|
|
}
|
|
VisibleEntityIds_.Empty();
|
|
if (FrontierActor_)
|
|
{
|
|
FrontierActor_->Destroy();
|
|
FrontierActor_ = nullptr;
|
|
}
|
|
Super::EndPlay(EndPlayReason);
|
|
}
|
|
|
|
void UZeusAOIComponent::TickComponent(float DeltaTime, ELevelTick TickType,
|
|
FActorComponentTickFunction* ThisTickFunction)
|
|
{
|
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
|
|
|
// Frontier: sincroniza o overlay com o estado atual. Pega o liga/desliga via
|
|
// CVar zeus.debug.frontier (que nao passa por SetOverlayEnabled) e garante que
|
|
// ao desligar as paredes sejam REMOVIDAS (UpdateFrontierOverlay -> ClearOverlay).
|
|
if (ResolveOverlay(EZeusAOIOverlay::FrontierCells) != bFrontierApplied_)
|
|
{
|
|
UpdateFrontierOverlay();
|
|
}
|
|
|
|
// Re-pede o frontier periodicamente enquanto ligado -> o overlay reflete
|
|
// split/merge da topologia dinamica (debug client-side, baixa freq, so' ON).
|
|
if (ResolveOverlay(EZeusAOIOverlay::FrontierCells))
|
|
{
|
|
FrontierRepollAccumSec_ += DeltaTime;
|
|
if (FrontierRepollAccumSec_ >= FrontierRepollIntervalSec)
|
|
{
|
|
FrontierRepollAccumSec_ = 0.0f;
|
|
RequestFrontierConfig();
|
|
}
|
|
}
|
|
|
|
if (AnyOverlayActive())
|
|
{
|
|
DrawOverlays();
|
|
}
|
|
}
|
|
|
|
void UZeusAOIComponent::RequestAoiConfig()
|
|
{
|
|
if (UWorld* World = GetWorld())
|
|
{
|
|
if (UGameInstance* GI = World->GetGameInstance())
|
|
{
|
|
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
|
|
{
|
|
NetC->EmitDebugAoiRequest(); // V1: no-op se ainda nao Accepted
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UZeusAOIComponent::HandleAoiConfig(float InterestCm, float DespawnCm)
|
|
{
|
|
InterestRadiusCm = InterestCm;
|
|
DespawnRadiusCm = DespawnCm;
|
|
// K4: log AOI cfg recebido do server (paridade com Net.AOI server-side
|
|
// "config_applied ZI=... ZD=..."). Cold path (1 por sessao).
|
|
UE_LOG(LogZeusAOI, Display,
|
|
TEXT("AOI cfg recv interestCm=%.0f (%.0fm) despawnCm=%.0f (%.0fm)"),
|
|
InterestCm, InterestCm / 100.0f,
|
|
DespawnCm, DespawnCm / 100.0f);
|
|
}
|
|
|
|
void UZeusAOIComponent::RequestFrontierConfig()
|
|
{
|
|
if (UWorld* World = GetWorld())
|
|
{
|
|
if (UGameInstance* GI = World->GetGameInstance())
|
|
{
|
|
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
|
|
{
|
|
NetC->EmitDebugFrontierRequest(); // V1: no-op se ainda nao Accepted
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UZeusAOIComponent::HandleFrontierConfig(const ZeusV1::FDebugFrontierInfo& Info)
|
|
{
|
|
FrontierInfo_ = Info;
|
|
bFrontierReceived_ = true;
|
|
UE_LOG(LogZeusAOI, Display,
|
|
TEXT("Frontier cfg recv own=%d neighbors=%d"),
|
|
Info.OwnedCells.Num(), Info.NeighborCells.Num());
|
|
UpdateFrontierOverlay();
|
|
}
|
|
|
|
AZeusFrontierOverlayActor* UZeusAOIComponent::EnsureFrontierActor()
|
|
{
|
|
if (FrontierActor_) { return FrontierActor_; }
|
|
UWorld* World = GetWorld();
|
|
if (!World) { return nullptr; }
|
|
FActorSpawnParameters Params;
|
|
Params.Owner = GetOwner();
|
|
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
|
FrontierActor_ = World->SpawnActor<AZeusFrontierOverlayActor>(
|
|
AZeusFrontierOverlayActor::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, Params);
|
|
return FrontierActor_;
|
|
}
|
|
|
|
void UZeusAOIComponent::UpdateFrontierOverlay()
|
|
{
|
|
const bool bOn = ResolveOverlay(EZeusAOIOverlay::FrontierCells);
|
|
bFrontierApplied_ = bOn;
|
|
if (!bOn)
|
|
{
|
|
if (FrontierActor_) { FrontierActor_->ClearOverlay(); }
|
|
return;
|
|
}
|
|
if (!bFrontierReceived_)
|
|
{
|
|
// Ainda nao temos as cells; pede e espera o 6163 (HandleFrontierConfig re-chama).
|
|
RequestFrontierConfig();
|
|
return;
|
|
}
|
|
AZeusFrontierOverlayActor* Actor = EnsureFrontierActor();
|
|
if (!Actor) { return; }
|
|
const float AnchorZ = GetOwner() ? GetOwner()->GetActorLocation().Z : 0.0f;
|
|
Actor->RebuildFromFrontier(FrontierInfo_, AnchorZ);
|
|
}
|
|
|
|
bool UZeusAOIComponent::ResolveOverlay(EZeusAOIOverlay Overlay) const
|
|
{
|
|
switch (Overlay)
|
|
{
|
|
case EZeusAOIOverlay::AOIRadius: return bShowAOIRadius || CVarShowAOI.GetValueOnGameThread() > 0;
|
|
case EZeusAOIOverlay::DespawnZone: return bShowDespawnZone || CVarShowDespawn.GetValueOnGameThread() > 0;
|
|
case EZeusAOIOverlay::CellBounds: return bShowCellBounds || CVarShowCell.GetValueOnGameThread() > 0;
|
|
case EZeusAOIOverlay::ProxyLabels: return bShowProxyLabels || CVarShowProxy.GetValueOnGameThread() > 0;
|
|
case EZeusAOIOverlay::HandoffLine: return bShowHandoffLine || CVarShowHandoff.GetValueOnGameThread() > 0;
|
|
case EZeusAOIOverlay::AuthDrift: return bShowAuthDrift || CVarShowDrift.GetValueOnGameThread() > 0;
|
|
case EZeusAOIOverlay::FrontierCells: return bShowFrontier || CVarShowFrontier.GetValueOnGameThread() > 0;
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
bool UZeusAOIComponent::AnyOverlayActive() const
|
|
{
|
|
return ResolveOverlay(EZeusAOIOverlay::AOIRadius)
|
|
|| ResolveOverlay(EZeusAOIOverlay::DespawnZone)
|
|
|| ResolveOverlay(EZeusAOIOverlay::CellBounds)
|
|
|| ResolveOverlay(EZeusAOIOverlay::ProxyLabels)
|
|
|| ResolveOverlay(EZeusAOIOverlay::HandoffLine)
|
|
|| ResolveOverlay(EZeusAOIOverlay::AuthDrift)
|
|
|| ResolveOverlay(EZeusAOIOverlay::FrontierCells);
|
|
}
|
|
|
|
void UZeusAOIComponent::DrawOverlays()
|
|
{
|
|
const AActor* Owner = GetOwner();
|
|
UWorld* World = GetWorld();
|
|
if (!Owner || !World) { return; }
|
|
|
|
const FVector Loc = Owner->GetActorLocation();
|
|
|
|
// Zona de Interesse (interna) — raio real do servidor. Sem o raio (==0),
|
|
// ainda nao chegou a config: nao desenha (loading).
|
|
if (ResolveOverlay(EZeusAOIOverlay::AOIRadius) && InterestRadiusCm > 0.0f)
|
|
{
|
|
const float R = FMath::Min(InterestRadiusCm, kMaxOverlayRadiusCm);
|
|
DrawDebugSphere(World, Loc, R, kSphereSegments, kColorInterest, false, -1.0f, 0, LineThickness);
|
|
}
|
|
// Zona de Despawn (externa, > interesse = histerese) — raio real do servidor.
|
|
if (ResolveOverlay(EZeusAOIOverlay::DespawnZone) && DespawnRadiusCm > 0.0f)
|
|
{
|
|
const float R = FMath::Min(DespawnRadiusCm, kMaxOverlayRadiusCm);
|
|
DrawDebugSphere(World, Loc, R, kSphereSegments, kColorDespawn, false, -1.0f, 0, LineThickness);
|
|
}
|
|
if (ResolveOverlay(EZeusAOIOverlay::CellBounds))
|
|
{
|
|
// PLACEHOLDER: box grande centrada no player. TODO bounds reais da cell.
|
|
DrawDebugBox(World, Loc, FVector(CellBoundsExtentCm, CellBoundsExtentCm, 2000.0f),
|
|
kColorCell, false, -1.0f, 0, LineThickness);
|
|
}
|
|
if (ResolveOverlay(EZeusAOIOverlay::HandoffLine))
|
|
{
|
|
// PLACEHOLDER: vetor pra frente. TODO apontar pro centro da cell vizinha.
|
|
const float Len = InterestRadiusCm > 0.0f ? InterestRadiusCm : 3000.0f;
|
|
const FVector End = Loc + Owner->GetActorForwardVector() * Len;
|
|
DrawDebugDirectionalArrow(World, Loc, End, 200.0f, kColorHandoff, false, -1.0f, 0, LineThickness);
|
|
}
|
|
// ProxyLabels / AuthDrift: TODO (precisam do registry de proxies/snapshots).
|
|
}
|
|
|
|
void UZeusAOIComponent::SetOverlayEnabled_Implementation(EZeusAOIOverlay Overlay, bool bEnabled)
|
|
{
|
|
switch (Overlay)
|
|
{
|
|
case EZeusAOIOverlay::AOIRadius: bShowAOIRadius = bEnabled; break;
|
|
case EZeusAOIOverlay::DespawnZone: bShowDespawnZone = bEnabled; break;
|
|
case EZeusAOIOverlay::CellBounds: bShowCellBounds = bEnabled; break;
|
|
case EZeusAOIOverlay::ProxyLabels: bShowProxyLabels = bEnabled; break;
|
|
case EZeusAOIOverlay::HandoffLine: bShowHandoffLine = bEnabled; break;
|
|
case EZeusAOIOverlay::AuthDrift: bShowAuthDrift = bEnabled; break;
|
|
case EZeusAOIOverlay::FrontierCells: bShowFrontier = bEnabled; break;
|
|
default: break;
|
|
}
|
|
|
|
// Ao ligar uma zona de AOI sem ter os raios ainda, pede a config (loading).
|
|
const bool bAoiZone = (Overlay == EZeusAOIOverlay::AOIRadius || Overlay == EZeusAOIOverlay::DespawnZone);
|
|
if (bEnabled && bAoiZone && InterestRadiusCm <= 0.0f)
|
|
{
|
|
RequestAoiConfig();
|
|
}
|
|
|
|
// Frontier: ao ligar pede as cells (6162) e reconstroi; ao desligar limpa as paredes.
|
|
if (Overlay == EZeusAOIOverlay::FrontierCells)
|
|
{
|
|
if (bEnabled) { RequestFrontierConfig(); }
|
|
UpdateFrontierOverlay();
|
|
}
|
|
}
|
|
|
|
void UZeusAOIComponent::SetAllOverlays_Implementation(bool bEnabled)
|
|
{
|
|
bShowAOIRadius = bShowDespawnZone = bShowCellBounds = bEnabled;
|
|
bShowProxyLabels = bShowHandoffLine = bShowAuthDrift = bEnabled;
|
|
bShowFrontier = bEnabled;
|
|
|
|
if (bEnabled && InterestRadiusCm <= 0.0f)
|
|
{
|
|
RequestAoiConfig();
|
|
}
|
|
if (bEnabled) { RequestFrontierConfig(); }
|
|
UpdateFrontierOverlay();
|
|
}
|
|
|
|
bool UZeusAOIComponent::IsOverlayEnabled_Implementation(EZeusAOIOverlay Overlay) const
|
|
{
|
|
// Reflete o estado real (flag OU CVar) p/ a UI mostrar o que esta' desenhando.
|
|
return ResolveOverlay(Overlay);
|
|
}
|
|
|
|
float UZeusAOIComponent::GetInterestRadiusCm_Implementation() const
|
|
{
|
|
return InterestRadiusCm; // 0 = config do servidor ainda nao chegou (loading)
|
|
}
|
|
|
|
float UZeusAOIComponent::GetDespawnRadiusCm_Implementation() const
|
|
{
|
|
return DespawnRadiusCm;
|
|
}
|
|
|
|
int32 UZeusAOIComponent::GetEnabledOverlayCount() const
|
|
{
|
|
return (bShowAOIRadius ? 1 : 0) + (bShowDespawnZone ? 1 : 0) + (bShowCellBounds ? 1 : 0)
|
|
+ (bShowProxyLabels ? 1 : 0) + (bShowHandoffLine ? 1 : 0) + (bShowAuthDrift ? 1 : 0)
|
|
+ (bShowFrontier ? 1 : 0);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Gameplay AOI: visibility tracking (1.4 do roteiro PvP)
|
|
// ===========================================================================
|
|
//
|
|
// Espelha ENT_SPAWN/ENT_DESPAWN do subsystem V1 num TSet local. Fires os
|
|
// delegates BlueprintAssignable pra game systems escutarem. Self-entity
|
|
// filtrada (o proprio char nao entra no tracking nem fires).
|
|
|
|
void UZeusAOIComponent::HandleSelfEntityAssignedV1(int64 EntityId)
|
|
{
|
|
SelfEntityId_ = EntityId;
|
|
// Se ja' havia "spawnado" antes do SELF chegar (race possivel), remove agora.
|
|
if (EntityId != 0 && VisibleEntityIds_.Contains(EntityId))
|
|
{
|
|
VisibleEntityIds_.Remove(EntityId);
|
|
OnEntityExitedAOI.Broadcast(EntityId, /*Reason=*/0);
|
|
}
|
|
}
|
|
|
|
void UZeusAOIComponent::HandleEntitySpawnedV1(int64 EntityId, int32 /*Kind*/,
|
|
FVector /*PosCm*/, float /*YawDeg*/,
|
|
FVector /*VelCmS*/, bool /*bGrounded*/,
|
|
int64 /*ServerTimeMs*/)
|
|
{
|
|
if (EntityId == 0) { return; }
|
|
if (EntityId == SelfEntityId_) { return; } // nunca trackear self
|
|
|
|
bool bAdded = false;
|
|
VisibleEntityIds_.Add(EntityId, &bAdded);
|
|
if (bAdded)
|
|
{
|
|
OnEntityEnteredAOI.Broadcast(EntityId);
|
|
}
|
|
// Re-spawn (entity ja' visivel) = no-op silencioso. Pode acontecer em
|
|
// keyframe pos-handoff cross-server.
|
|
}
|
|
|
|
void UZeusAOIComponent::HandleEntityDespawnedV1(int64 EntityId, int32 Reason)
|
|
{
|
|
if (EntityId == 0) { return; }
|
|
if (VisibleEntityIds_.Remove(EntityId) > 0)
|
|
{
|
|
OnEntityExitedAOI.Broadcast(EntityId, Reason);
|
|
}
|
|
}
|
|
|
|
int32 UZeusAOIComponent::GetVisibleEntityCount() const
|
|
{
|
|
return VisibleEntityIds_.Num();
|
|
}
|
|
|
|
TArray<int64> UZeusAOIComponent::GetVisibleEntityIds() const
|
|
{
|
|
return VisibleEntityIds_.Array();
|
|
}
|
|
|
|
bool UZeusAOIComponent::IsEntityVisible(int64 EntityId) const
|
|
{
|
|
return VisibleEntityIds_.Contains(EntityId);
|
|
}
|