Espelho simétrico do front-end pattern (UUIFrontEndFlowSubsystem +
DA_FrontEndScreenSet + UUIPrimaryGameLayout_Base) para a UI in-game.
Pavimenta caminho pra Inventory, Skills, StatusWindow, EscapeMenu, etc.
## UI in-game system (Source/ZMMO/Game/UI/InGame/)
- EZMMOInGameUIState enum (None, Playing, StatusWindow, Inventory,
SkillTree, EscapeMenu, Loading) em Data/UI/InGameTypes.h
- UUIInGameScreenSet DataAsset: TMap<EZMMOInGameUIState, TSoftClassPtr<...>>
- UUIInGameFlowSubsystem (GameInstanceSubsystem) com SetState/ToggleScreen
- DA_InGameScreenSet em Content/ZMMO/UI/InGame/ mapeia Playing->WBP_HUD
- Layer routing: Playing -> UI.Layer.Game | menus -> GameMenu | loading -> Modal
## AHUD pattern (idiomatic UE5)
- AZMMOHUD : AHUD em Source/ZMMO/Game/Modes/
- AZMMOGameMode::HUDClass = AZMMOHUD::StaticClass()
- AZMMOHUD::BeginPlay chama UIInGameFlowSubsystem::StartInGame
- Pawn não cria mais HUD (separação de responsabilidades)
## PlayerState + Component Registry (Open-Closed)
- AZMMOPlayerState : APlayerState em Source/ZMMO/Game/Modes/
- AZMMOGameMode::PlayerStateClass = AZMMOPlayerState::StaticClass()
- UPROPERTY Config TArray<TSubclassOf<UActorComponent>> ComponentClasses
- DefaultGame.ini: +ComponentClasses=/Script/ZMMOAttributes.ZMMOAttributeComponent
- Ctor lê via GConfig (mais robusto que UPROPERTY Config + parsing) e
instancia cada componente como CreateDefaultSubobject
Por que componentes no PlayerState e não no Pawn:
- Sobrevive ao despawn (morte/respawn)
- Spectator-friendly
- 1:1 com player (Pawn pode trocar: vehicle, mount, possession)
- Padrão MMO clássico (rathena, TrinityCore)
Adicionar novo módulo (Inventory, Skills, Guild) = 1 linha no
DefaultGame.ini, sem editar AZMMOPlayerState.cpp.
## HUD composite
- UZMMOHudWidget : UCommonActivatableWidget em ZMMO core (UI/InGame/)
- WBP_HUD em Content/ZMMO/UI/HUD/ herda UZMMOHudWidget
- HpSpBar (UZMMOHudHpSpWidget — sub-modulo ZMMOAttributes) embedded via
BindWidgetOptional. Sub-widgets futuros: PlayerInfo, Minimap, QuickBar,
Buffs, Chat, TargetInfo
- UZMMOHudHpSpWidget volta a ser UUserWidget puro (sub-widget, não root)
- UZMMOHudWidget::NativeOnActivated:
- SetVisibility(HitTestInvisible) — mouse passa direto pro Pawn
- BindToLocalPlayer: busca AttributeComponent no PC->PlayerState
- Subscreve OnAttributesChanged/OnHpSpChanged/OnLevelUp
- Propaga snapshots pra sub-widgets via HpSpBar->ApplySnapshot
- GetDesiredInputConfig override: ECommonInputMode::Game (sem cursor,
movimento livre). Menus override pra GameAndMenu/Menu.
## NetworkHandler refatorado (AttributeComponent agora no PlayerState)
- ZMMOAttributeNetworkHandler::FindComponentForEntity: busca via
GameState->PlayerArray (O(N_players)), não TActorIterator (O(N_actors))
- ZMMOAttributes.Build.cs += CommonUI, CommonInput
## EnsureRootLayout (fix crítico)
- OpenLevel invalida widgets no viewport ("InvalidateAllWidgets")
- RootLayout (criado pelo FrontEndFlow no boot) sobrevive como UObject
(LocalPlayerSubsystem) mas fica órfão fora do viewport
- Push do WBP_HUD no Stack_Game funcionava mas widget pai (RootLayout)
estava fora do viewport → invisível
- UUIFrontEndFlowSubsystem::EnsureRootLayout (público) recria via
UIManagerSubsystem (idempotente)
- UUIInGameFlowSubsystem::StartInGame chama EnsureRootLayout antes do push
## PlayerCharacter simplificado
- Removido AttributeComponent (vai pro PlayerState via Registry)
- Removido HudHpSpWidgetClass + spawn manual de HUD
- HandleLocalSpawnReady só faz seed do EntityId via PlayerState
- UI lifecycle agora é responsabilidade do AZMMOHUD::BeginPlay
## Verificação (smoke test)
PIE: Login -> CharSelect -> Lobby -> EnterWorld
↓
LogZMMO: AZMMOPlayerState ctor: 1 componente(s) no registry
LogZMMO: ZMMOPlayerState: componente registrado [0] ZMMOAttributeComponent
LogZMMO: FrontEndFlow::EnsureRootLayout: RootLayout recriado
LogZMMO: InGameFlow: state None -> Playing
LogZMMO: InGameFlow: tela Playing pushed em UI.Layer.Game (widget=WBP_HUD_C_0)
LogZMMO: ZMMOHudWidget activated (HpSpBar=WBP_HUD_HpSpBar_C_0)
↓
HUD aparece com Lv 5 / HP 10/200 / SP 5/20 (valores do DB)
Regen funciona (HP/SP enchem a cada 6s/8s)
Movimento normal (input flui pro Pawn)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
182 lines
5.2 KiB
C++
182 lines
5.2 KiB
C++
#include "ZMMOAttributeNetworkHandler.h"
|
|
|
|
#include "Engine/GameInstance.h"
|
|
#include "Engine/World.h"
|
|
#include "GameFramework/GameStateBase.h"
|
|
#include "GameFramework/PlayerState.h"
|
|
#include "ZMMOAttributeComponent.h"
|
|
#include "ZMMOAttributeTypes.h"
|
|
#include "ZeusNetworkSubsystem.h"
|
|
|
|
namespace
|
|
{
|
|
// Helper privado — converte o payload binario do plugin pra USTRUCT
|
|
// Blueprint que o componente exibe.
|
|
FZMMOAttributesSnapshot ToSnapshot(const FZeusAttributesPayload& P)
|
|
{
|
|
FZMMOAttributesSnapshot S;
|
|
S.EntityId = P.EntityId;
|
|
S.ClassId = P.ClassId;
|
|
S.BaseLevel = P.BaseLevel;
|
|
S.BaseExp = P.BaseExp;
|
|
S.JobLevel = P.JobLevel;
|
|
S.JobExp = P.JobExp;
|
|
S.Str = P.Str;
|
|
S.Agi = P.Agi;
|
|
S.Vit = P.Vit;
|
|
S.Int = P.Int;
|
|
S.Dex = P.Dex;
|
|
S.Luk = P.Luk;
|
|
S.StatusPoint = P.StatusPoint;
|
|
S.SkillPoint = P.SkillPoint;
|
|
S.Hp = P.Hp;
|
|
S.MaxHp = P.MaxHp;
|
|
S.Sp = P.Sp;
|
|
S.MaxSp = P.MaxSp;
|
|
S.Money = P.Money;
|
|
S.Atk = P.Atk;
|
|
S.Matk = P.Matk;
|
|
S.Def = P.Def;
|
|
S.Mdef = P.Mdef;
|
|
S.Hit = P.Hit;
|
|
S.Flee = P.Flee;
|
|
S.CritX10 = P.CritX10;
|
|
S.Aspd = P.Aspd;
|
|
return S;
|
|
}
|
|
|
|
// Resolve `EntityId -> UZMMOAttributeComponent` via PlayerArray do GameState.
|
|
// AttributeComponent agora vive no PlayerState (vide AZMMOPlayerState +
|
|
// Component Registry em DefaultGame.ini). Itera apenas players (1 por
|
|
// conexao), nao 1000+ atores — O(N_players).
|
|
//
|
|
// Cada UZMMOAttributeComponent carrega seu proprio EntityId (seed em
|
|
// AZMMOPlayerCharacter::HandleLocalSpawnReady ou no primeiro snapshot).
|
|
UZMMOAttributeComponent* FindComponentForEntity(UWorld* World, int32 EntityId)
|
|
{
|
|
if (!World || EntityId == 0) { return nullptr; }
|
|
const AGameStateBase* GS = World->GetGameState();
|
|
if (!GS) { return nullptr; }
|
|
for (APlayerState* PS : GS->PlayerArray)
|
|
{
|
|
if (!PS) { continue; }
|
|
UZMMOAttributeComponent* Comp = PS->FindComponentByClass<UZMMOAttributeComponent>();
|
|
if (!Comp) { continue; }
|
|
if (Comp->GetSnapshot().EntityId == EntityId)
|
|
{
|
|
return Comp;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
bool UZMMOAttributeNetworkHandler::ShouldCreateSubsystem(UObject* Outer) const
|
|
{
|
|
// Cria apenas para mundos de gameplay (PIE/Game), nao para editor preview.
|
|
UWorld* World = Cast<UWorld>(Outer);
|
|
if (!World) { return false; }
|
|
return World->WorldType == EWorldType::Game || World->WorldType == EWorldType::PIE;
|
|
}
|
|
|
|
void UZMMOAttributeNetworkHandler::Initialize(FSubsystemCollectionBase& Collection)
|
|
{
|
|
Super::Initialize(Collection);
|
|
|
|
if (UZeusNetworkSubsystem* Net = GetZeusNetSubsystem())
|
|
{
|
|
SnapshotFullHandle = Net->OnAttributeSnapshotFull.AddUObject(
|
|
this, &UZMMOAttributeNetworkHandler::HandleAttributeSnapshotFull);
|
|
HpSpUpdateHandle = Net->OnHpSpUpdate.AddUObject(
|
|
this, &UZMMOAttributeNetworkHandler::HandleHpSpUpdate);
|
|
LevelUpHandle = Net->OnLevelUp.AddUObject(
|
|
this, &UZMMOAttributeNetworkHandler::HandleLevelUp);
|
|
}
|
|
}
|
|
|
|
void UZMMOAttributeNetworkHandler::Deinitialize()
|
|
{
|
|
if (UZeusNetworkSubsystem* Net = GetZeusNetSubsystem())
|
|
{
|
|
if (SnapshotFullHandle.IsValid())
|
|
{
|
|
Net->OnAttributeSnapshotFull.Remove(SnapshotFullHandle);
|
|
SnapshotFullHandle.Reset();
|
|
}
|
|
if (HpSpUpdateHandle.IsValid())
|
|
{
|
|
Net->OnHpSpUpdate.Remove(HpSpUpdateHandle);
|
|
HpSpUpdateHandle.Reset();
|
|
}
|
|
if (LevelUpHandle.IsValid())
|
|
{
|
|
Net->OnLevelUp.Remove(LevelUpHandle);
|
|
LevelUpHandle.Reset();
|
|
}
|
|
}
|
|
Super::Deinitialize();
|
|
}
|
|
|
|
UZeusNetworkSubsystem* UZMMOAttributeNetworkHandler::GetZeusNetSubsystem() const
|
|
{
|
|
const UWorld* World = GetWorld();
|
|
if (!World) { return nullptr; }
|
|
UGameInstance* GI = World->GetGameInstance();
|
|
if (!GI) { return nullptr; }
|
|
return GI->GetSubsystem<UZeusNetworkSubsystem>();
|
|
}
|
|
|
|
void UZMMOAttributeNetworkHandler::HandleAttributeSnapshotFull(const FZeusAttributesPayload& Payload)
|
|
{
|
|
UWorld* World = GetWorld();
|
|
if (!World) { return; }
|
|
UZMMOAttributeComponent* Comp = FindComponentForEntity(World, Payload.EntityId);
|
|
if (!Comp)
|
|
{
|
|
// Fallback chicken-and-egg: primeiro snapshot chega antes do EntityId
|
|
// estar seeded no componente. Aplica ao primeiro componente do
|
|
// PlayerArray que ainda tem EntityId=0. Apos o primeiro apply, o
|
|
// caminho normal por lookup funciona.
|
|
const AGameStateBase* GS = World->GetGameState();
|
|
if (GS)
|
|
{
|
|
for (APlayerState* PS : GS->PlayerArray)
|
|
{
|
|
if (!PS) { continue; }
|
|
UZMMOAttributeComponent* Candidate = PS->FindComponentByClass<UZMMOAttributeComponent>();
|
|
if (Candidate && Candidate->GetSnapshot().EntityId == 0)
|
|
{
|
|
Comp = Candidate;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (Comp)
|
|
{
|
|
Comp->ApplySnapshot(ToSnapshot(Payload));
|
|
}
|
|
}
|
|
|
|
void UZMMOAttributeNetworkHandler::HandleHpSpUpdate(int32 EntityId, int32 Hp, int32 Sp)
|
|
{
|
|
UWorld* World = GetWorld();
|
|
if (!World) { return; }
|
|
UZMMOAttributeComponent* Comp = FindComponentForEntity(World, EntityId);
|
|
if (Comp)
|
|
{
|
|
Comp->ApplyHpSpUpdate(Hp, Sp);
|
|
}
|
|
}
|
|
|
|
void UZMMOAttributeNetworkHandler::HandleLevelUp(int32 EntityId, int32 NewBaseLevel, int32 StatusPointDelta)
|
|
{
|
|
UWorld* World = GetWorld();
|
|
if (!World) { return; }
|
|
UZMMOAttributeComponent* Comp = FindComponentForEntity(World, EntityId);
|
|
if (Comp)
|
|
{
|
|
Comp->NotifyLevelUp(NewBaseLevel, StatusPointDelta);
|
|
}
|
|
}
|