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>
211 lines
6.0 KiB
C++
211 lines
6.0 KiB
C++
#include "UIInGameFlowSubsystem.h"
|
|
|
|
#include "CommonActivatableWidget.h" // Necessario pra TSoftClassPtr<UCommonActivatableWidget>::Get()
|
|
#include "Engine/AssetManager.h"
|
|
#include "Engine/GameInstance.h"
|
|
#include "Engine/StreamableManager.h"
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "GameplayTagContainer.h"
|
|
#include "ZMMO.h"
|
|
#include "Data/UI/UILayerTags.h"
|
|
#include "FrontEnd/UIFrontEndFlowSubsystem.h"
|
|
#include "FrontEnd/UIManagerSubsystem.h"
|
|
#include "InGame/UIInGameScreenSet.h"
|
|
|
|
void UUIInGameFlowSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
|
{
|
|
Super::Initialize(Collection);
|
|
UE_LOG(LogZMMO, Log, TEXT("UIInGameFlowSubsystem initialized"));
|
|
}
|
|
|
|
void UUIInGameFlowSubsystem::Deinitialize()
|
|
{
|
|
ScreenSet = nullptr;
|
|
CurrentState = EZMMOInGameUIState::None;
|
|
Super::Deinitialize();
|
|
}
|
|
|
|
void UUIInGameFlowSubsystem::StartInGame()
|
|
{
|
|
if (CurrentState != EZMMOInGameUIState::None)
|
|
{
|
|
// Idempotente: chamadas extras (ex.: respawn) sao no-op se ja in-world.
|
|
UE_LOG(LogZMMO, Verbose,
|
|
TEXT("InGameFlow: StartInGame ignorado (ja em estado %s)"),
|
|
*UEnum::GetValueAsString(CurrentState));
|
|
return;
|
|
}
|
|
|
|
// CRITICAL: OpenLevel invalida widgets no viewport ("InvalidateAllWidgets"),
|
|
// orfanando o RootLayout (UUIPrimaryGameLayout_Base) que foi criado pelo
|
|
// FrontEndFlow no boot. Sem RootLayout no viewport, PushScreenToLayer
|
|
// silenciosamente adiciona widgets ao stack do RootLayout — mas o stack
|
|
// nao e' visivel porque o parent (RootLayout) nao esta no viewport.
|
|
//
|
|
// Re-cria via EnsureRootLayout (idempotente) ANTES de qualquer push.
|
|
if (UGameInstance* GI = GetGameInstance())
|
|
{
|
|
if (UUIFrontEndFlowSubsystem* FE = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
|
|
{
|
|
FE->EnsureRootLayout();
|
|
}
|
|
}
|
|
|
|
// Limpa qualquer residuo do front-end (Menu/Modal podem ter "Loading" ou
|
|
// dialogs orfaos do handoff). Layer.Game vai receber o HUD novo.
|
|
if (UUIManagerSubsystem* Mgr = GetUIManager())
|
|
{
|
|
Mgr->ClearLayer(ZMMOUITags::UI_Layer_Menu.GetTag());
|
|
Mgr->ClearLayer(ZMMOUITags::UI_Layer_Modal.GetTag());
|
|
Mgr->ClearLayer(ZMMOUITags::UI_Layer_GameMenu.GetTag());
|
|
}
|
|
|
|
SetState(EZMMOInGameUIState::Playing);
|
|
}
|
|
|
|
void UUIInGameFlowSubsystem::SetState(EZMMOInGameUIState NewState)
|
|
{
|
|
if (NewState == CurrentState)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const EZMMOInGameUIState OldState = CurrentState;
|
|
CurrentState = NewState;
|
|
|
|
UE_LOG(LogZMMO, Log, TEXT("InGameFlow: state %s -> %s"),
|
|
*UEnum::GetValueAsString(OldState),
|
|
*UEnum::GetValueAsString(NewState));
|
|
|
|
// Decisao de stack: ao voltar pra `Playing`, fechamos quaisquer overlays
|
|
// (Status/Inventory/Menu) limpando o Layer.GameMenu — HUD em Layer.Game
|
|
// permanece. Para qualquer outro estado, mantemos HUD e empilhamos no
|
|
// layer correspondente.
|
|
if (UUIManagerSubsystem* Mgr = GetUIManager())
|
|
{
|
|
const bool bGoingToPlaying = (NewState == EZMMOInGameUIState::Playing);
|
|
if (bGoingToPlaying)
|
|
{
|
|
Mgr->ClearLayer(ZMMOUITags::UI_Layer_GameMenu.GetTag());
|
|
}
|
|
}
|
|
|
|
ResolveAndPushScreen(NewState);
|
|
OnStateChanged.Broadcast(NewState);
|
|
}
|
|
|
|
void UUIInGameFlowSubsystem::ToggleScreen(EZMMOInGameUIState Screen)
|
|
{
|
|
if (Screen == EZMMOInGameUIState::Playing || Screen == EZMMOInGameUIState::None)
|
|
{
|
|
// Toggle para Playing nao faz sentido (estado base). Use SetState direto.
|
|
return;
|
|
}
|
|
if (CurrentState == Screen)
|
|
{
|
|
SetState(EZMMOInGameUIState::Playing);
|
|
}
|
|
else
|
|
{
|
|
SetState(Screen);
|
|
}
|
|
}
|
|
|
|
UUIInGameScreenSet* UUIInGameFlowSubsystem::GetScreenSet()
|
|
{
|
|
if (ScreenSet) { return ScreenSet; }
|
|
if (ScreenSetAsset.IsNull())
|
|
{
|
|
UE_LOG(LogZMMO, Warning,
|
|
TEXT("InGameFlow: ScreenSetAsset nao configurado em DefaultGame.ini "
|
|
"[/Script/ZMMO.UIInGameFlowSubsystem] ScreenSetAsset=..."));
|
|
return nullptr;
|
|
}
|
|
// Sync load — DA e' pequeno (so' soft refs); PIE/dev OK.
|
|
ScreenSet = ScreenSetAsset.LoadSynchronous();
|
|
return ScreenSet;
|
|
}
|
|
|
|
UUIManagerSubsystem* UUIInGameFlowSubsystem::GetUIManager() const
|
|
{
|
|
if (const UGameInstance* GI = GetGameInstance())
|
|
{
|
|
if (ULocalPlayer* LP = GI->GetFirstGamePlayer())
|
|
{
|
|
return LP->GetSubsystem<UUIManagerSubsystem>();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
FGameplayTag UUIInGameFlowSubsystem::LayerForState(EZMMOInGameUIState State) const
|
|
{
|
|
switch (State)
|
|
{
|
|
case EZMMOInGameUIState::Playing:
|
|
return ZMMOUITags::UI_Layer_Game.GetTag();
|
|
case EZMMOInGameUIState::Loading:
|
|
return ZMMOUITags::UI_Layer_Modal.GetTag();
|
|
case EZMMOInGameUIState::StatusWindow:
|
|
case EZMMOInGameUIState::Inventory:
|
|
case EZMMOInGameUIState::SkillTree:
|
|
case EZMMOInGameUIState::EscapeMenu:
|
|
default:
|
|
return ZMMOUITags::UI_Layer_GameMenu.GetTag();
|
|
}
|
|
}
|
|
|
|
void UUIInGameFlowSubsystem::ResolveAndPushScreen(EZMMOInGameUIState State)
|
|
{
|
|
if (State == EZMMOInGameUIState::None) { return; }
|
|
|
|
UUIInGameScreenSet* SS = GetScreenSet();
|
|
if (!SS)
|
|
{
|
|
UE_LOG(LogZMMO, Warning,
|
|
TEXT("InGameFlow: ScreenSet ausente; tela de %s ignorada"),
|
|
*UEnum::GetValueAsString(State));
|
|
return;
|
|
}
|
|
|
|
const TSoftClassPtr<UCommonActivatableWidget> Soft = SS->GetScreenForState(State);
|
|
if (Soft.IsNull())
|
|
{
|
|
UE_LOG(LogZMMO, Warning,
|
|
TEXT("InGameFlow: Screen for state %s not configured em DA_InGameScreenSet (verificar StateScreens)"),
|
|
*UEnum::GetValueAsString(State));
|
|
return;
|
|
}
|
|
|
|
UUIManagerSubsystem* Mgr = GetUIManager();
|
|
if (!Mgr)
|
|
{
|
|
UE_LOG(LogZMMO, Warning, TEXT("InGameFlow: UIManagerSubsystem ausente"));
|
|
return;
|
|
}
|
|
|
|
const FGameplayTag Layer = LayerForState(State);
|
|
const FSoftObjectPath Path = Soft.ToSoftObjectPath();
|
|
|
|
UAssetManager::GetStreamableManager().RequestAsyncLoad(
|
|
Path,
|
|
FStreamableDelegate::CreateWeakLambda(this, [this, Soft, Layer, State]()
|
|
{
|
|
UUIManagerSubsystem* M = GetUIManager();
|
|
if (!M) { return; }
|
|
UClass* Cls = Soft.Get();
|
|
if (!Cls)
|
|
{
|
|
UE_LOG(LogZMMO, Warning,
|
|
TEXT("InGameFlow: async load falhou pra %s"),
|
|
*UEnum::GetValueAsString(State));
|
|
return;
|
|
}
|
|
UCommonActivatableWidget* W = M->PushScreenToLayer(Layer, Cls);
|
|
UE_LOG(LogZMMO, Log,
|
|
TEXT("InGameFlow: tela %s pushed em %s (widget=%s)"),
|
|
*UEnum::GetValueAsString(State), *Layer.ToString(),
|
|
W ? *W->GetName() : TEXT("null"));
|
|
}));
|
|
}
|