2 Commits

Author SHA1 Message Date
9a430dabc3 feat(aoi): UZeusAOIComponent ganha visibility tracking + delegates BP (1.4 do roteiro PvP)
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>
2026-06-18 00:23:51 -03:00
a3f2fa9c06 feat(client): overlay de fronteira -- ajustes + materiais e external actor
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 22:44:11 -03:00
14 changed files with 134 additions and 10 deletions

View File

@@ -66,6 +66,13 @@ void UZeusAOIComponent::BeginPlay()
{
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);
}
}
}
@@ -87,12 +94,17 @@ void UZeusAOIComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
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();
@@ -344,3 +356,64 @@ int32 UZeusAOIComponent::GetEnabledOverlayCount() const
+ (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);
}

View File

@@ -10,6 +10,15 @@
class AZeusFrontierOverlayActor;
// Delegates BlueprintAssignable pra game systems reagirem a entrada/saida de
// outras entidades na AOI do player local. Disparados quando ENT_SPAWN /
// ENT_DESPAWN chega do servidor (V1 canonico). Self-entity filtrada -- o
// proprio char NAO fires aqui.
// - Entered: 1 param (EntityId)
// - Exited: 2 params (EntityId, Reason). Reason vem do payload ENT_DESPAWN.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZeusAOIEntityEnter, int64, EntityId);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZeusAOIEntityExit, int64, EntityId, int32, Reason);
/**
* UZeusAOIComponent (cliente / jogo)
*
@@ -70,6 +79,22 @@ public:
UFUNCTION(BlueprintPure, Category = "Zeus|AOI Debug")
int32 GetEnabledOverlayCount() const;
// === Gameplay AOI: visibility tracking (1.4 do roteiro PvP) ============
//
// Espelha quem o servidor mandou ENT_SPAWN/ENT_DESPAWN pro player local.
// Cliente NAO faz histérese própria -- server já filtra com
// margem de ~2048cm ([[project_aoi_crossserver_hysteresis]]). Game systems
// (HUD, audio, ability target filtering) escutam estes delegates pra
// reagir a entrada/saída. Self-entity filtrada (o próprio char
// não fires nem entra no tracking).
UPROPERTY(BlueprintAssignable, Category = "Zeus|AOI") FZeusAOIEntityEnter OnEntityEnteredAOI;
UPROPERTY(BlueprintAssignable, Category = "Zeus|AOI") FZeusAOIEntityExit OnEntityExitedAOI;
UFUNCTION(BlueprintPure, Category = "Zeus|AOI") int32 GetVisibleEntityCount() const;
UFUNCTION(BlueprintPure, Category = "Zeus|AOI") TArray<int64> GetVisibleEntityIds() const;
UFUNCTION(BlueprintPure, Category = "Zeus|AOI") bool IsEntityVisible(int64 EntityId) const;
UFUNCTION(BlueprintPure, Category = "Zeus|AOI") int64 GetSelfEntityId() const { return SelfEntityId_; }
private:
void DrawOverlays();
bool ResolveOverlay(EZeusAOIOverlay Overlay) const;
@@ -108,4 +133,17 @@ private:
float FrontierRepollAccumSec_ = 0.0f;
/** Ator que desenha as paredes + labels (spawn lazy ao ligar o overlay). */
UPROPERTY() TObjectPtr<AZeusFrontierOverlayActor> FrontierActor_ = nullptr;
// === Visibility tracking (1.4) ==========================================
UFUNCTION() void HandleEntitySpawnedV1(int64 EntityId, int32 Kind, FVector PosCm,
float YawDeg, FVector VelCmS, bool bGrounded,
int64 ServerTimeMs);
UFUNCTION() void HandleEntityDespawnedV1(int64 EntityId, int32 Reason);
UFUNCTION() void HandleSelfEntityAssignedV1(int64 EntityId);
/** EntityIds atualmente visíveis (espelho do server). TSet pra O(1) check.
* Não inclui self. Limpo no EndPlay. */
TSet<int64> VisibleEntityIds_;
/** EntityId do próprio char (recebido via ENT_SELF). 0 = ainda não atribuído. */
int64 SelfEntityId_ = 0;
};

View File

@@ -18,20 +18,33 @@ namespace
const FColor kLabelAlive(93, 213, 255); // ciano -- server online
const FColor kLabelDead(255, 90, 90); // vermelho -- server offline / cell vazia
// Offset do cellId de filha de split (espelha CHILD_CELLID_OFFSET do server).
constexpr uint32 kChildCellIdOffset = 1000000u;
// Decodifica o cellId int na notacao de quadtree: raiz -> "0".."3"; filha ->
// "<pai>,<quadrante>" (ex "0,2"); neto -> "0,2,1" (recursivo). Espelha
// childCellId = offset + pai*4 + quadrante do SplitCoordinator.
// Decodifica o cellId na notacao "raiz fora, splits no array" da k-d tree
// binaria: raiz pura -> "0"; derivada -> "0,[0,1]". Espelha o layout
// bit-packed de Server/ZeusServices/Shared/types/src/cell-id.ts:
// bit 30 = flag (1 = cell derivada de split)
// bits 29..26 = depth (profundidade de split)
// bits 25..15 = rootId (cell raiz do baseGrid)
// bits 14..0 = path (1 bit por nivel, MSB-first)
FString DecodeCellPath(uint32 CellId)
{
if (CellId < kChildCellIdOffset)
constexpr uint32 kFlagBit = 1u << 30;
if ((CellId & kFlagBit) == 0u)
{
return FString::Printf(TEXT("%u"), CellId);
return FString::Printf(TEXT("%u"), CellId); // raiz pura
}
const uint32 Rel = CellId - kChildCellIdOffset;
return DecodeCellPath(Rel / 4u) + FString::Printf(TEXT(",%u"), Rel % 4u);
const uint32 Depth = (CellId >> 26) & 0xFu;
const uint32 Root = (CellId >> 15) & 0x7FFu;
const uint32 Packed = CellId & 0x7FFFu;
FString Path;
for (uint32 i = 0; i < Depth; ++i)
{
if (i > 0)
{
Path += TEXT(",");
}
Path += ((Packed >> (14u - i)) & 1u) ? TEXT("1") : TEXT("0");
}
return FString::Printf(TEXT("%u,[%s]"), Root, *Path);
}
// Aresta compartilhada entre dois retangulos axis-aligned (no plano XY).