From 9a430dabc3618382b79795c1bd9b4004c0bfdc53 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Thu, 18 Jun 2026 00:23:51 -0300 Subject: [PATCH] feat(aoi): UZeusAOIComponent ganha visibility tracking + delegates BP (1.4 do roteiro PvP) Estende o componente (que ja existia como debug overlay) com: - TSet 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 --- Source/ZMMO/Game/Network/ZeusAOIComponent.cpp | 73 +++++++++++++++++++ Source/ZMMO/Game/Network/ZeusAOIComponent.h | 38 ++++++++++ 2 files changed, 111 insertions(+) diff --git a/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp b/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp index 0b4c95a..8c61b0d 100644 --- a/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp +++ b/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp @@ -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 UZeusAOIComponent::GetVisibleEntityIds() const +{ + return VisibleEntityIds_.Array(); +} + +bool UZeusAOIComponent::IsEntityVisible(int64 EntityId) const +{ + return VisibleEntityIds_.Contains(EntityId); +} diff --git a/Source/ZMMO/Game/Network/ZeusAOIComponent.h b/Source/ZMMO/Game/Network/ZeusAOIComponent.h index a24a5a9..874aa3f 100644 --- a/Source/ZMMO/Game/Network/ZeusAOIComponent.h +++ b/Source/ZMMO/Game/Network/ZeusAOIComponent.h @@ -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 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 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 VisibleEntityIds_; + /** EntityId do próprio char (recebido via ENT_SELF). 0 = ainda não atribuído. */ + int64 SelfEntityId_ = 0; };