feat: ZMMO client base (Game/Entity|Controller|Modes|Network)

Estrutura inicial do cliente Unreal Zeus MMO alinhada ao padrao
"cliente solto + servidor valida input/velocidade" (ADR 0038):

- Source/ZMMO/Game/Entity/ — IZMMOEntityInterface, AZMMOEntity (base
  AActor para Mob/NPC/Object), AZMMOPlayerCharacter (player local com
  CMC livre, Enhanced Input, envio de C_INPUT_AXIS), AZMMOPlayerProxy
  (snapshot-only para players remotos).
- Source/ZMMO/Game/Controller/ — AZMMOPlayerController com IMC defaults
  (IMC_Default + IMC_MouseLook) e suporte opcional a virtual joystick.
- Source/ZMMO/Game/Modes/ — AZMMOGameMode (defaults para PlayerCharacter
  / PlayerController), UZMMOGameInstance (auto-connect ao servidor Zeus
  em Init e logging dos eventos OnConnected/OnDisconnected/...).
- Source/ZMMO/Game/Network/ — UZMMOWorldSubsystem (registry
  EntityId -> AActor*, dispatch dos delegates OnPlayerSpawned/Despawned/
  StateUpdate; ignora snapshots locais do cliente solto).
- Config/DefaultEngine.ini — GlobalDefaultGameMode aponta para
  /Script/ZMMO.ZMMOGameMode; GameInstanceClass para
  /Script/ZMMO.ZMMOGameInstance; redirects para a nova hierarquia.
- ZMMO.Build.cs — depende de ZeusNetwork; PublicIncludePaths para a
  nova arvore Game/Entity|Controller|Modes|Network.
- Content — assets do template ThirdPerson (Mannequins, IAs/IMCs,
  Lvl_ThirdPerson + TestWorld). Os Variant_* levels ficam no commit
  inicial mas serao limpos numa proxima sessao com aprovacao explicita
  (Master Rule para .umap/.uasset).

Notas:
- BP_ThirdPersonCharacter/GameMode/PlayerController ainda apontam para
  a hierarquia antiga e estao a aguardar aprovacao para remocao
  (Master Rule).
- README.md descreve a arquitectura e o smoke test de conexao.
This commit is contained in:
2026-05-07 20:21:21 -03:00
parent 9a5a0eef61
commit 3d4d0de40b
828 changed files with 7799 additions and 0 deletions

15
Source/ZMMO.Target.cs Normal file
View File

@@ -0,0 +1,15 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
using System.Collections.Generic;
public class ZMMOTarget : TargetRules
{
public ZMMOTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V6;
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7;
ExtraModuleNames.Add("ZMMO");
}
}

View File

@@ -0,0 +1,80 @@
#include "ZMMOPlayerController.h"
#include "Blueprint/UserWidget.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/LocalPlayer.h"
#include "InputMappingContext.h"
#include "UObject/ConstructorHelpers.h"
#include "Widgets/Input/SVirtualJoystick.h"
#include "ZMMO.h"
AZMMOPlayerController::AZMMOPlayerController()
{
// Defaults dos Input Mapping Contexts (alinhados ao ZClientMMO). Permitem
// instanciar AZMMOPlayerController directamente como
// `PlayerControllerClass` do AZMMOGameMode sem exigir um BP filho. Se o
// projeto adicionar mais IMCs, podemos extender via BP filho ou via
// arquivos de Config (UPROPERTY EditAnywhere abaixo).
static ConstructorHelpers::FObjectFinder<UInputMappingContext> DefaultImc(
TEXT("/Game/Input/IMC_Default.IMC_Default"));
static ConstructorHelpers::FObjectFinder<UInputMappingContext> MouseLookImc(
TEXT("/Game/Input/IMC_MouseLook.IMC_MouseLook"));
if (DefaultImc.Succeeded())
{
DefaultMappingContexts.Add(DefaultImc.Object);
}
if (MouseLookImc.Succeeded())
{
MobileExcludedMappingContexts.Add(MouseLookImc.Object);
}
}
void AZMMOPlayerController::BeginPlay()
{
Super::BeginPlay();
if (ShouldUseTouchControls() && IsLocalPlayerController())
{
MobileControlsWidget = CreateWidget<UUserWidget>(this, MobileControlsWidgetClass);
if (MobileControlsWidget)
{
MobileControlsWidget->AddToPlayerScreen(0);
}
else
{
UE_LOG(LogZMMO, Error, TEXT("Could not spawn mobile controls widget."));
}
}
}
void AZMMOPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
if (!IsLocalPlayerController())
{
return;
}
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
{
for (UInputMappingContext* CurrentContext : DefaultMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
if (!ShouldUseTouchControls())
{
for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
}
}
}
bool AZMMOPlayerController::ShouldUseTouchControls() const
{
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
}

View File

@@ -0,0 +1,50 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "ZMMOPlayerController.generated.h"
class UInputMappingContext;
class UUserWidget;
/**
* AZMMOPlayerController
*
* Player controller do cliente Zeus MMO. Responsavel apenas pela camada de
* input do Unreal (Enhanced Input Mapping Contexts e widget opcional de
* controlos touch). Toda a logica de identidade Zeus, envio de input e
* reconciliacao vive em `AZMMOPlayerCharacter`.
*/
UCLASS(Blueprintable, BlueprintType)
class ZMMO_API AZMMOPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AZMMOPlayerController();
protected:
/** Mapping contexts default (sempre adicionados). */
UPROPERTY(EditAnywhere, Category = "Input|Input Mappings")
TArray<UInputMappingContext*> DefaultMappingContexts;
/** Mapping contexts saltados quando estamos em modo touch. */
UPROPERTY(EditAnywhere, Category = "Input|Input Mappings")
TArray<UInputMappingContext*> MobileExcludedMappingContexts;
/** Widget opcional de controlos touch (mobile / forced). */
UPROPERTY(EditAnywhere, Category = "Input|Touch Controls")
TSubclassOf<UUserWidget> MobileControlsWidgetClass;
UPROPERTY()
TObjectPtr<UUserWidget> MobileControlsWidget;
/** Forca controlos touch mesmo fora de plataformas mobile (uso em desktop dev). */
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
bool bForceTouchControls = false;
virtual void BeginPlay() override;
virtual void SetupInputComponent() override;
bool ShouldUseTouchControls() const;
};

View File

@@ -0,0 +1,32 @@
#include "ZMMOEntity.h"
AZMMOEntity::AZMMOEntity()
{
PrimaryActorTick.bCanEverTick = false;
bReplicates = false;
}
void AZMMOEntity::SetZMMOIdentity(const int64 InEntityId, const EZMMOEntityType InEntityType)
{
EntityId = InEntityId;
EntityType = InEntityType;
}
void AZMMOEntity::ApplyEntitySnapshot(const FZMMOEntitySnapshot& Snapshot)
{
const FRotator NewRotation(0.0f, Snapshot.YawDeg, 0.0f);
SetActorLocationAndRotation(Snapshot.PositionCm, NewRotation);
}
void AZMMOEntity::SetEntityRelevant(const bool bRelevant)
{
if (bIsZMMORelevant == bRelevant)
{
return;
}
bIsZMMORelevant = bRelevant;
SetActorHiddenInGame(!bRelevant);
SetActorEnableCollision(bRelevant);
SetActorTickEnabled(bRelevant);
}

View File

@@ -0,0 +1,57 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ZMMOEntityInterface.h"
#include "ZMMOEntityTypes.h"
#include "ZMMOEntity.generated.h"
/**
* AZMMOEntity
*
* Classe base para entidades replicadas pelo servidor que NAO precisam de
* capsula + `UCharacterMovementComponent` — tipicamente NPCs estaticos,
* objetos dinamicos do mundo, mobs simples sem locomotion fisica.
*
* Para personagens com capsula + CMC (proxy de jogador, mobs com locomotion
* por capsula), use `AZMMOPlayerProxy` ou uma futura `AZMMOMob` que herde
* de `ACharacter`.
*
* Implementa `IZMMOEntityInterface` com:
* - getters para `EntityId` / `EntityType`;
* - `ApplyEntitySnapshot` default: `SetActorLocationAndRotation` (sem
* interpolacao temporal — V1 podera adicionar ring-buffer + lerp);
* - `SetEntityRelevant(false)` default: oculta + desativa collision +
* desativa tick (preserva o ator no mundo para futuro pooling).
*/
UCLASS(Blueprintable, BlueprintType)
class ZMMO_API AZMMOEntity : public AActor, public IZMMOEntityInterface
{
GENERATED_BODY()
public:
AZMMOEntity();
// --- IZMMOEntityInterface ---
virtual int64 GetZMMOEntityId() const override { return EntityId; }
virtual EZMMOEntityType GetZMMOEntityType() const override { return EntityType; }
virtual void ApplyEntitySnapshot(const FZMMOEntitySnapshot& Snapshot) override;
virtual void SetEntityRelevant(bool bRelevant) override;
/** Setter usado pelo `UZMMOWorldSubsystem` apos o spawn para registrar a identidade. */
UFUNCTION(BlueprintCallable, Category = "ZMMO|Entity")
void SetZMMOIdentity(int64 InEntityId, EZMMOEntityType InEntityType);
protected:
/** Identificador autoritativo do servidor. Atribuido em `SetZMMOIdentity`. */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity")
int64 EntityId = 0;
/** Categoria autoritativa (Player/Mob/NPC/Object). */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity")
EZMMOEntityType EntityType = EZMMOEntityType::Object;
/** Estado de relevancia atual (espelha as transicoes Hidden<->Relevant do servidor). */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity")
bool bIsZMMORelevant = true;
};

View File

@@ -0,0 +1,75 @@
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ZMMOEntityTypes.h"
#include "ZMMOEntityInterface.generated.h"
/**
* UZMMOEntityInterface
*
* Marcador `UInterface` necessario para o sistema de reflexao da Unreal. Todo o
* contrato vive em `IZMMOEntityInterface` (puro C++).
*/
UINTERFACE(BlueprintType, MinimalAPI, meta = (CannotImplementInterfaceInBlueprint))
class UZMMOEntityInterface : public UInterface
{
GENERATED_BODY()
};
/**
* IZMMOEntityInterface
*
* Contrato comum de todas as entidades replicadas pelo Zeus Server (player
* local, players remotos, mobs, NPCs, objetos). Inspirado em `IZeusEntityInterface`
* (ZeusV1) e adaptado para o nosso modelo "cliente solto + servidor valida input".
*
* Decisoes:
* 1. **Identidade autoritativa**: `EntityId` e `EntityType` vem do servidor; o
* cliente nunca os altera.
* 2. **Visibilidade gated por AOI**: o servidor envia spawn/despawn quando o
* target entra/sai da area de interesse. O cliente reage via
* `SetEntityRelevant(bool)`.
* 3. **Player local nao reconcilia em V0**: para o `AZMMOPlayerCharacter` o
* `ApplyEntitySnapshot` e no-op (cliente solto controla landscape). Quando
* a colisao de objetos chegar no servidor podemos ligar reconciliacao
* horizontal opcional.
*
* Por que UInterface (e nao heranca classica)?
* - `AZMMOPlayerCharacter` ja herda de `ACharacter`. Forcar uma classe base
* `AZMMOEntity : AActor` para todos perderia o `ACharacter` gratuito.
* A interface permite que o player local (`ACharacter`), o proxy remoto
* (`ACharacter`) e entidades genericas (`AActor`) implementem o mesmo
* contrato sem heranca multipla concreta.
*/
class ZMMO_API IZMMOEntityInterface
{
GENERATED_BODY()
public:
/** Identificador autoritativo da entidade no servidor. */
virtual int64 GetZMMOEntityId() const = 0;
/** Categoria da entidade (Player, Mob, NPC, Object). */
virtual EZMMOEntityType GetZMMOEntityType() const = 0;
/**
* Aplica um snapshot autoritativo. Implementacoes podem extender com
* interpolacao temporal ou logica especifica (anim drivers, etc.).
*
* Para o jogador local, este metodo e no-op em V0: o cliente conduz a
* fisica do landscape de forma autoritativa local, e o servidor apenas
* valida input/velocidade (ADR 0038).
*/
virtual void ApplyEntitySnapshot(const FZMMOEntitySnapshot& Snapshot) = 0;
/**
* Alterna entre estado visivel/ativo (`true`) e oculto/desativado (`false`).
* Implementacao default das classes do projeto: `Hidden + DisableCollision +
* DisableTick` ao desativar (preserva o ator para futuro pooling).
*
* Para o jogador local, `bRelevant=false` e ignorado — nunca despawnamos o
* proprio personagem do cliente local.
*/
virtual void SetEntityRelevant(bool bRelevant) = 0;
};

View File

@@ -0,0 +1,64 @@
#pragma once
#include "CoreMinimal.h"
#include "ZMMOEntityTypes.generated.h"
/**
* Categoria autoritativa replicada pelo servidor. Determina que classe de ator
* o `UZMMOWorldSubsystem` deve spawnar para um snapshot recebido.
*/
UENUM(BlueprintType)
enum class EZMMOEntityType : uint8
{
Player UMETA(DisplayName = "Player"),
Mob UMETA(DisplayName = "Mob"),
NPC UMETA(DisplayName = "NPC"),
Object UMETA(DisplayName = "Object")
};
/**
* Snapshot autoritativo aplicado por `IZMMOEntityInterface::ApplyEntitySnapshot`.
*
* Construido pelo `UZMMOWorldSubsystem` a partir dos delegates do
* `UZeusNetworkSubsystem` (`OnPlayerStateUpdate` em V0; opcodes para mobs/NPCs
* em V1+). Mantemos os campos minimos que o servidor ja replica hoje
* (`S_PLAYER_STATE`); novos campos sao adicionados aqui sempre que o protocolo
* evoluir, sem mexer nas classes de proxy.
*/
USTRUCT(BlueprintType)
struct FZMMOEntitySnapshot
{
GENERATED_BODY()
/** EntityId autoritativo do servidor. */
UPROPERTY(BlueprintReadOnly, Category = "ZMMO|Entity")
int64 EntityId = 0;
/** Categoria do ator (define qual proxy class spawnar). */
UPROPERTY(BlueprintReadOnly, Category = "ZMMO|Entity")
EZMMOEntityType EntityType = EZMMOEntityType::Player;
/** Sequencia de input processada no servidor (so faz sentido para players). */
UPROPERTY(BlueprintReadOnly, Category = "ZMMO|Entity")
int32 LastProcessedInputSeq = 0;
/** Posicao mundo (cm). */
UPROPERTY(BlueprintReadOnly, Category = "ZMMO|Entity")
FVector PositionCm = FVector::ZeroVector;
/** Velocidade mundo (cm/s) — usada para network smoothing e drivers de animacao. */
UPROPERTY(BlueprintReadOnly, Category = "ZMMO|Entity")
FVector VelocityCmS = FVector::ZeroVector;
/** Yaw em graus (orientacao visual). */
UPROPERTY(BlueprintReadOnly, Category = "ZMMO|Entity")
float YawDeg = 0.0f;
/** Indica se o servidor considera o ator em solo. */
UPROPERTY(BlueprintReadOnly, Category = "ZMMO|Entity")
bool bGrounded = false;
/** Tempo do servidor (ms desde epoch) quando este snapshot foi gerado. */
UPROPERTY(BlueprintReadOnly, Category = "ZMMO|Entity")
int64 ServerTimeMs = 0;
};

View File

@@ -0,0 +1,359 @@
#include "ZMMOPlayerCharacter.h"
#include "Animation/AnimInstance.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "EnhancedInputComponent.h"
#include "Engine/GameInstance.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/World.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "GameFramework/SpringArmComponent.h"
#include "InputAction.h"
#include "InputActionValue.h"
#include "Subsystems/WorldSubsystem.h"
#include "UObject/ConstructorHelpers.h"
#include "ZMMO.h"
#include "ZMMOWorldSubsystem.h"
#include "ZeusNetworkSubsystem.h"
DEFINE_LOG_CATEGORY(LogZMMOPlayer);
AZMMOPlayerCharacter::AZMMOPlayerCharacter()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
GetCapsuleComponent()->InitCapsuleSize(42.0f, 96.0f);
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
UCharacterMovementComponent* CMC = GetCharacterMovement();
CMC->bOrientRotationToMovement = true;
CMC->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
CMC->JumpZVelocity = 500.0f;
CMC->AirControl = 0.35f;
CMC->MaxWalkSpeed = 500.0f;
CMC->MinAnalogWalkSpeed = 20.0f;
CMC->BrakingDecelerationWalking = 2000.0f;
CMC->BrakingDecelerationFalling = 1500.0f;
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 400.0f;
CameraBoom->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
// Defaults visuais (mesh + AnimBP) — alinhados ao ZClientMMO. Permitem ao
// motor spawnar AZMMOPlayerCharacter directamente como DefaultPawnClass do
// AZMMOGameMode sem exigir um BP filho. Um BP filho continua opcional para
// trocar mesh/AnimBP por mapa.
if (USkeletalMeshComponent* MeshComponent = GetMesh())
{
static ConstructorHelpers::FObjectFinder<USkeletalMesh> QuinnMesh(
TEXT("/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple.SKM_Quinn_Simple"));
static ConstructorHelpers::FClassFinder<UAnimInstance> QuinnAnimBp(
TEXT("/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed"));
if (QuinnMesh.Succeeded())
{
MeshComponent->SetSkeletalMesh(QuinnMesh.Object);
MeshComponent->SetRelativeLocation(FVector(0.0f, 0.0f, -90.0f));
MeshComponent->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
}
else
{
UE_LOG(LogZMMOPlayer, Warning, TEXT("Default mesh SKM_Quinn_Simple not found for AZMMOPlayerCharacter."));
}
if (QuinnAnimBp.Succeeded())
{
MeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint);
MeshComponent->SetAnimInstanceClass(QuinnAnimBp.Class);
}
else
{
UE_LOG(LogZMMOPlayer, Warning, TEXT("Default anim blueprint ABP_Unarmed not found for AZMMOPlayerCharacter."));
}
}
// Input Actions defaults (Enhanced Input) — IMCs sao adicionados pelo
// AZMMOPlayerController. As IAs aqui resolvem o asset por path; um BP filho
// pode sobrescrever caso queira inputs custom.
static ConstructorHelpers::FObjectFinder<UInputAction> MoveActionAsset(
TEXT("/Game/Input/Actions/IA_Move.IA_Move"));
static ConstructorHelpers::FObjectFinder<UInputAction> LookActionAsset(
TEXT("/Game/Input/Actions/IA_Look.IA_Look"));
static ConstructorHelpers::FObjectFinder<UInputAction> MouseLookActionAsset(
TEXT("/Game/Input/Actions/IA_MouseLook.IA_MouseLook"));
static ConstructorHelpers::FObjectFinder<UInputAction> JumpActionAsset(
TEXT("/Game/Input/Actions/IA_Jump.IA_Jump"));
if (MoveActionAsset.Succeeded()) { MoveAction = MoveActionAsset.Object; }
if (LookActionAsset.Succeeded()) { LookAction = LookActionAsset.Object; }
if (MouseLookActionAsset.Succeeded()) { MouseLookAction = MouseLookActionAsset.Object; }
if (JumpActionAsset.Succeeded()) { JumpAction = JumpActionAsset.Object; }
}
void AZMMOPlayerCharacter::BeginPlay()
{
Super::BeginPlay();
ResolveZeusNetworkSubsystem();
BindZeusSpawnDelegate();
TryRegisterLocalEntityFromCachedSpawn();
}
void AZMMOPlayerCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
UnbindZeusSpawnDelegate();
ZeusNetwork = nullptr;
Super::EndPlay(EndPlayReason);
}
void AZMMOPlayerCharacter::Tick(const float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
FlushInputAxisToServer(DeltaSeconds);
}
void AZMMOPlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &AZMMOPlayerCharacter::OnJumpPressed);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AZMMOPlayerCharacter::OnJumpReleased);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AZMMOPlayerCharacter::Move);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Completed, this, &AZMMOPlayerCharacter::MoveCompleted);
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AZMMOPlayerCharacter::Look);
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AZMMOPlayerCharacter::Look);
}
else
{
UE_LOG(LogZMMOPlayer, Error, TEXT("'%s' Failed to find an Enhanced Input component."), *GetNameSafe(this));
}
}
void AZMMOPlayerCharacter::Move(const FInputActionValue& Value)
{
const FVector2D MovementVector = Value.Get<FVector2D>();
DoMove(MovementVector.X, MovementVector.Y);
}
void AZMMOPlayerCharacter::MoveCompleted(const FInputActionValue& Value)
{
(void)Value;
DoMove(0.0f, 0.0f);
}
void AZMMOPlayerCharacter::Look(const FInputActionValue& Value)
{
const FVector2D LookAxisVector = Value.Get<FVector2D>();
DoLook(LookAxisVector.X, LookAxisVector.Y);
}
void AZMMOPlayerCharacter::OnJumpPressed()
{
bPendingJumpPressed = true;
DoJumpStart();
}
void AZMMOPlayerCharacter::OnJumpReleased()
{
bPendingJumpReleased = true;
DoJumpEnd();
}
void AZMMOPlayerCharacter::DoMove(const float Right, const float Forward)
{
PendingMoveRight = FMath::Clamp(Right, -1.0f, 1.0f);
PendingMoveForward = FMath::Clamp(Forward, -1.0f, 1.0f);
if (GetController() != nullptr)
{
const FRotator Rotation = GetController()->GetControlRotation();
const FRotator YawRotation(0.0f, Rotation.Yaw, 0.0f);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, Forward);
AddMovementInput(RightDirection, Right);
}
}
void AZMMOPlayerCharacter::DoLook(const float Yaw, const float Pitch)
{
if (GetController() != nullptr)
{
AddControllerYawInput(Yaw);
AddControllerPitchInput(Pitch);
}
}
void AZMMOPlayerCharacter::DoJumpStart()
{
Jump();
}
void AZMMOPlayerCharacter::DoJumpEnd()
{
StopJumping();
}
void AZMMOPlayerCharacter::ApplyEntitySnapshot(const FZMMOEntitySnapshot& /*Snapshot*/)
{
// Cliente local solto: o servidor nao reconcilia posicao em V0 (ADR 0038).
// Quando colisao de objetos chegar, podemos ligar uma reconciliacao opcional
// horizontal aqui (XY only), preservando o Z do CMC local.
}
void AZMMOPlayerCharacter::SetEntityRelevant(bool /*bRelevant*/)
{
// Nunca despawnamos o jogador local via AOI.
}
void AZMMOPlayerCharacter::ResolveZeusNetworkSubsystem()
{
if (ZeusNetwork)
{
return;
}
UGameInstance* GI = GetGameInstance();
if (!GI)
{
return;
}
ZeusNetwork = GI->GetSubsystem<UZeusNetworkSubsystem>();
}
void AZMMOPlayerCharacter::BindZeusSpawnDelegate()
{
if (bSpawnDelegateBound || !IsLocallyControlled())
{
return;
}
if (!ZeusNetwork)
{
return;
}
ZeusNetwork->OnPlayerSpawned.AddDynamic(this, &AZMMOPlayerCharacter::HandleZeusPlayerSpawned);
bSpawnDelegateBound = true;
}
void AZMMOPlayerCharacter::UnbindZeusSpawnDelegate()
{
if (!bSpawnDelegateBound || !ZeusNetwork)
{
bSpawnDelegateBound = false;
return;
}
ZeusNetwork->OnPlayerSpawned.RemoveDynamic(this, &AZMMOPlayerCharacter::HandleZeusPlayerSpawned);
bSpawnDelegateBound = false;
}
void AZMMOPlayerCharacter::TryRegisterLocalEntityFromCachedSpawn()
{
if (!ZeusNetwork)
{
return;
}
int32 CachedEntityId = 0;
FVector CachedPosCm = FVector::ZeroVector;
float CachedYawDeg = 0.0f;
int64 CachedServerTimeMs = 0;
if (ZeusNetwork->TryGetLastLocalSpawn(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs))
{
HandleLocalSpawnReady(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs);
}
}
void AZMMOPlayerCharacter::HandleZeusPlayerSpawned(const int32 InEntityId, const bool bIsLocal,
const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
{
if (!bIsLocal)
{
// Remote spawns sao tratados pelo `UZMMOWorldSubsystem`; aqui so reagimos
// ao spawn do pawn possessivel local (S_SPAWN_PLAYER com bIsLocal=true).
return;
}
HandleLocalSpawnReady(InEntityId, PosCm, YawDeg, ServerTimeMs);
}
void AZMMOPlayerCharacter::HandleLocalSpawnReady(const int32 InEntityId, const FVector PosCm,
const float YawDeg, const int64 ServerTimeMs)
{
if (InEntityId == 0)
{
return;
}
const int64 EntityIdAsInt64 = static_cast<int64>(InEntityId);
if (ZMMOEntityId == EntityIdAsInt64)
{
return;
}
ZMMOEntityId = EntityIdAsInt64;
UE_LOG(LogZMMOPlayer, Log,
TEXT("AZMMOPlayerCharacter: local spawn captured EntityId=%d pos=(%s) yaw=%.1f t=%lld"),
InEntityId, *PosCm.ToString(), YawDeg, ServerTimeMs);
if (UWorld* World = GetWorld())
{
if (UZMMOWorldSubsystem* WorldSubsystem = World->GetSubsystem<UZMMOWorldSubsystem>())
{
WorldSubsystem->RegisterLocalEntity(EntityIdAsInt64, this);
}
}
}
void AZMMOPlayerCharacter::FlushInputAxisToServer(const float DeltaSeconds)
{
ResolveZeusNetworkSubsystem();
if (!ZeusNetwork || !ZeusNetwork->IsConnected())
{
return;
}
const float SendInterval = 1.0f / FMath::Max(1, InputSendRateHz);
SendAccumulatorSec += DeltaSeconds;
TimeSinceLastSendSec += DeltaSeconds;
const bool bMovingInput = !FMath::IsNearlyZero(PendingMoveForward) || !FMath::IsNearlyZero(PendingMoveRight);
const bool bJumpEvent = bPendingJumpPressed || bPendingJumpReleased;
const bool bRateReached = SendAccumulatorSec >= SendInterval;
const bool bHeartbeatReached = TimeSinceLastSendSec >= HeartbeatIntervalSec;
const bool bShouldSend = bJumpEvent || (bMovingInput && bRateReached) || (!bMovingInput && bHeartbeatReached);
if (!bShouldSend)
{
return;
}
++InputSequence;
const int32 ClientTimeMs = static_cast<int32>(FPlatformTime::Seconds() * 1000.0);
ZeusNetwork->SendInputAxis(
PendingMoveForward,
PendingMoveRight,
bPendingJumpPressed,
bPendingJumpReleased,
InputSequence,
ClientTimeMs);
SendAccumulatorSec = 0.0f;
TimeSinceLastSendSec = 0.0f;
bPendingJumpPressed = false;
bPendingJumpReleased = false;
}

View File

@@ -0,0 +1,158 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Logging/LogMacros.h"
#include "ZMMOEntityInterface.h"
#include "ZMMOEntityTypes.h"
#include "ZMMOPlayerCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputAction;
class UZeusNetworkSubsystem;
struct FInputActionValue;
DECLARE_LOG_CATEGORY_EXTERN(LogZMMOPlayer, Log, All);
/**
* AZMMOPlayerCharacter
*
* Personagem do jogador local. Diferente do template `ThirdPersonCharacter`
* e do antigo `AZMMOCharacter`, este ator e parte do pipeline de rede Zeus:
*
* - Continua a usar o `UCharacterMovementComponent` LIVRE para correr a
* fisica do landscape (ADR 0038): o cliente e autoritativo localmente
* sobre Z/colisao com terreno; o servidor apenas valida input/velocidade.
* - Envia `C_INPUT_AXIS` ao `UZeusNetworkSubsystem` na cadencia
* `InputSendRateHz` (com heartbeat 5 Hz quando parado para evitar
* starvation do `MovementSystem` no servidor).
* - Implementa `IZMMOEntityInterface` para participar do registry do
* `UZMMOWorldSubsystem`, mas `ApplyEntitySnapshot` e `SetEntityRelevant`
* sao no-op em V0 — o cliente local nao reconcilia (cliente solto).
*
* TODO V1: reconciliacao opcional XY quando colisao de objetos chegar; AOI
* debug spheres; visual smoothing apos correcao do servidor.
*/
UCLASS(Blueprintable, BlueprintType)
class ZMMO_API AZMMOPlayerCharacter : public ACharacter, public IZMMOEntityInterface
{
GENERATED_BODY()
/** Camera boom positioning the camera behind the character */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
USpringArmComponent* CameraBoom;
/** Follow camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UCameraComponent* FollowCamera;
protected:
UPROPERTY(EditAnywhere, Category = "Input")
UInputAction* JumpAction;
UPROPERTY(EditAnywhere, Category = "Input")
UInputAction* MoveAction;
UPROPERTY(EditAnywhere, Category = "Input")
UInputAction* LookAction;
UPROPERTY(EditAnywhere, Category = "Input")
UInputAction* MouseLookAction;
/** Cadencia de envio dos pacotes `C_INPUT_AXIS` (Hz). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ZMMO|Networking", meta = (ClampMin = "1", ClampMax = "120"))
int32 InputSendRateHz = 30;
/**
* Intervalo maximo (s) entre envios de `C_INPUT_AXIS` quando os inputs sao
* vazios (forward=0, right=0, sem jump). Mantem ~5 Hz de heartbeat para
* evitar starvation do servidor caso pacotes sejam perdidos. Em movimento
* o envio segue `InputSendRateHz` (30 Hz).
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ZMMO|Networking", meta = (ClampMin = "0.05", ClampMax = "5.0"))
float HeartbeatIntervalSec = 0.2f;
public:
AZMMOPlayerCharacter();
virtual void Tick(float DeltaSeconds) override;
// --- IZMMOEntityInterface ---
virtual int64 GetZMMOEntityId() const override { return ZMMOEntityId; }
virtual EZMMOEntityType GetZMMOEntityType() const override { return EZMMOEntityType::Player; }
virtual void ApplyEntitySnapshot(const FZMMOEntitySnapshot& Snapshot) override;
virtual void SetEntityRelevant(bool bRelevant) override;
UFUNCTION(BlueprintCallable, Category = "ZMMO|Entity")
void SetZMMOEntityId(int64 InEntityId) { ZMMOEntityId = InEntityId; }
FORCEINLINE USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
FORCEINLINE UCameraComponent* GetFollowCamera() const { return FollowCamera; }
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
void Move(const FInputActionValue& Value);
void MoveCompleted(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void OnJumpPressed();
void OnJumpReleased();
public:
/** Wrappers expostos a Blueprint (UI / mobile / automation). */
UFUNCTION(BlueprintCallable, Category = "Input")
virtual void DoMove(float Right, float Forward);
UFUNCTION(BlueprintCallable, Category = "Input")
virtual void DoLook(float Yaw, float Pitch);
UFUNCTION(BlueprintCallable, Category = "Input")
virtual void DoJumpStart();
UFUNCTION(BlueprintCallable, Category = "Input")
virtual void DoJumpEnd();
private:
void ResolveZeusNetworkSubsystem();
void FlushInputAxisToServer(float DeltaSeconds);
/** Liga `OnPlayerSpawned` para captar EntityId local apos o handshake. */
void BindZeusSpawnDelegate();
void UnbindZeusSpawnDelegate();
/** Caso o spawn ja tenha chegado antes deste pawn existir, aplica via cache. */
void TryRegisterLocalEntityFromCachedSpawn();
/**
* Memoriza `EntityId` autoritativo e regista o pawn no `UZMMOWorldSubsystem`
* para o registry partilhado por proxies remotos (filtra snapshots locais).
*/
void HandleLocalSpawnReady(int32 InEntityId, FVector PosCm, float YawDeg, int64 ServerTimeMs);
UFUNCTION()
void HandleZeusPlayerSpawned(int32 InEntityId, bool bIsLocal, FVector PosCm, float YawDeg, int64 ServerTimeMs);
UPROPERTY()
TObjectPtr<UZeusNetworkSubsystem> ZeusNetwork;
bool bSpawnDelegateBound = false;
/** Identidade autoritativa atribuida quando o servidor envia S_SPAWN_PLAYER local. */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity", meta = (AllowPrivateAccess = "true"))
int64 ZMMOEntityId = 0;
float PendingMoveForward = 0.0f;
float PendingMoveRight = 0.0f;
bool bPendingJumpPressed = false;
bool bPendingJumpReleased = false;
bool bPreviousJumpHeld = false;
float SendAccumulatorSec = 0.0f;
float TimeSinceLastSendSec = 0.0f;
int32 InputSequence = 0;
};

View File

@@ -0,0 +1,91 @@
#include "ZMMOPlayerProxy.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
AZMMOPlayerProxy::AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
UCapsuleComponent* Capsule = GetCapsuleComponent();
if (Capsule)
{
Capsule->InitCapsuleSize(42.0f, 96.0f);
Capsule->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
}
UCharacterMovementComponent* CMC = GetCharacterMovement();
if (CMC)
{
CMC->bOrientRotationToMovement = false;
CMC->SetMovementMode(MOVE_None);
}
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
}
void AZMMOPlayerProxy::Tick(const float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
RefreshAnimationDrivers();
}
void AZMMOPlayerProxy::SetZMMOIdentity(const int64 InEntityId, const EZMMOEntityType InEntityType)
{
EntityId = InEntityId;
EntityType = InEntityType;
}
void AZMMOPlayerProxy::ApplyEntitySnapshot(const FZMMOEntitySnapshot& Snapshot)
{
const FRotator NewRotation(0.0f, Snapshot.YawDeg, 0.0f);
SetActorLocationAndRotation(Snapshot.PositionCm, NewRotation);
LastSnapshotPosition = Snapshot.PositionCm;
VisualVelocity = Snapshot.VelocityCmS;
bHasFirstSnapshot = true;
RefreshAnimationDrivers();
ApplyCollisionPolicy();
}
void AZMMOPlayerProxy::SetEntityRelevant(const bool bRelevant)
{
if (bIsZMMORelevant == bRelevant)
{
return;
}
bIsZMMORelevant = bRelevant;
SetActorHiddenInGame(!bRelevant);
SetActorTickEnabled(bRelevant);
ApplyCollisionPolicy();
}
void AZMMOPlayerProxy::RefreshAnimationDrivers()
{
UCharacterMovementComponent* CMC = GetCharacterMovement();
if (!CMC)
{
return;
}
// `Acceleration` em UCharacterMovementComponent e protected — o AnimBP usa
// `Velocity` como driver primario; quando precisarmos de Acceleration para
// blends mais finos, expomos via AddInputVector ou um CMC custom.
CMC->Velocity = VisualVelocity;
}
void AZMMOPlayerProxy::ApplyCollisionPolicy()
{
UCapsuleComponent* Capsule = GetCapsuleComponent();
if (!Capsule)
{
return;
}
const bool bShouldCollide = bIsZMMORelevant && bHasFirstSnapshot;
Capsule->SetCollisionEnabled(bShouldCollide ? ECollisionEnabled::QueryOnly : ECollisionEnabled::NoCollision);
}

View File

@@ -0,0 +1,78 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "ZMMOEntityInterface.h"
#include "ZMMOEntityTypes.h"
#include "ZMMOPlayerProxy.generated.h"
/**
* AZMMOPlayerProxy
*
* Proxy visual para jogadores remotos (e, no futuro, mobs com locomotion
* por capsula) cuja posicao vem 100% do servidor via snapshots
* (`UZeusNetworkSubsystem::OnPlayerStateUpdate`).
*
* Inspirado em `AZeusRemoteCharacter` (ZeusV1). Diferencas chave vs.
* `AZMMOPlayerCharacter` (jogador local):
* - SEM `EnhancedInput`, sem envio de `C_INPUT_AXIS`.
* - SEM buffer de pending inputs nem reconciliacao.
* - Posicao aplicada por snapshot (`ApplyEntitySnapshot`); a `ACharacter`
* continua a existir para preservar capsula + skeletal mesh + AnimBP,
* mas o CMC fica em modo "kinematic-ish" (nao integra fisica).
*
* TODO V1: interpolacao temporal entre snapshots (ring-buffer + lerp),
* network smoothing visual no mesh, AnimBP-friendly velocity drivers.
*/
UCLASS(Blueprintable, BlueprintType)
class ZMMO_API AZMMOPlayerProxy : public ACharacter, public IZMMOEntityInterface
{
GENERATED_BODY()
public:
AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer);
virtual void Tick(float DeltaSeconds) override;
// --- IZMMOEntityInterface ---
virtual int64 GetZMMOEntityId() const override { return EntityId; }
virtual EZMMOEntityType GetZMMOEntityType() const override { return EntityType; }
virtual void ApplyEntitySnapshot(const FZMMOEntitySnapshot& Snapshot) override;
virtual void SetEntityRelevant(bool bRelevant) override;
UFUNCTION(BlueprintCallable, Category = "ZMMO|Entity")
void SetZMMOIdentity(int64 InEntityId, EZMMOEntityType InEntityType);
protected:
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity")
int64 EntityId = 0;
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity")
EZMMOEntityType EntityType = EZMMOEntityType::Player;
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity")
bool bIsZMMORelevant = true;
/** True apos o primeiro snapshot ser aplicado — antes disso o ator pode ficar oculto. */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity")
bool bHasFirstSnapshot = false;
/** Ultima posicao recebida por snapshot (mundo, cm). */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity")
FVector LastSnapshotPosition = FVector::ZeroVector;
/**
* Velocidade visual derivada para o AnimBP. Espelha `Snapshot.VelocityCmS`
* e e re-imposta no `CMC->Velocity` em cada `Tick` para manter os drivers
* de animacao alimentados entre updates de rede.
*/
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Entity")
FVector VisualVelocity = FVector::ZeroVector;
private:
/** Reaplica `CMC->Velocity` a partir de `VisualVelocity`. */
void RefreshAnimationDrivers();
/** Liga/desliga colisao da capsula consoante a relevancia + visual ready. */
void ApplyCollisionPolicy();
};

View File

@@ -0,0 +1,93 @@
#include "ZMMOGameInstance.h"
#include "ZMMO.h"
#include "ZeusNetworkSubsystem.h"
void UZMMOGameInstance::Init()
{
Super::Init();
UE_LOG(LogZMMO, Log, TEXT("ZMMOGameInstance::Init AutoConnect=%s Host=%s Port=%d"),
bAutoConnectOnStart ? TEXT("true") : TEXT("false"),
*ZeusServerHost,
ZeusServerPort);
UZeusNetworkSubsystem* ZeusNet = GetZeusNetwork();
if (!ZeusNet)
{
UE_LOG(LogZMMO, Warning, TEXT("ZMMOGameInstance: ZeusNetworkSubsystem unavailable; ZeusNetwork plugin enabled?"));
return;
}
BindZeusDelegates(ZeusNet);
if (!bAutoConnectOnStart)
{
return;
}
ZeusNet->ConnectToZeusServer(ZeusServerHost, ZeusServerPort);
}
void UZMMOGameInstance::Shutdown()
{
if (UZeusNetworkSubsystem* ZeusNet = GetZeusNetwork())
{
UnbindZeusDelegates(ZeusNet);
if (ZeusNet->IsConnected())
{
ZeusNet->DisconnectFromZeusServer();
}
}
Super::Shutdown();
}
UZeusNetworkSubsystem* UZMMOGameInstance::GetZeusNetwork() const
{
return GetSubsystem<UZeusNetworkSubsystem>();
}
void UZMMOGameInstance::BindZeusDelegates(UZeusNetworkSubsystem* Zeus)
{
if (!Zeus)
{
return;
}
Zeus->OnConnected.AddDynamic(this, &UZMMOGameInstance::HandleZeusConnected);
Zeus->OnConnectionFailed.AddDynamic(this, &UZMMOGameInstance::HandleZeusConnectionFailed);
Zeus->OnDisconnected.AddDynamic(this, &UZMMOGameInstance::HandleZeusDisconnected);
Zeus->OnNetworkLog.AddDynamic(this, &UZMMOGameInstance::HandleZeusNetworkLog);
}
void UZMMOGameInstance::UnbindZeusDelegates(UZeusNetworkSubsystem* Zeus)
{
if (!Zeus)
{
return;
}
Zeus->OnConnected.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusConnected);
Zeus->OnConnectionFailed.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusConnectionFailed);
Zeus->OnDisconnected.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusDisconnected);
Zeus->OnNetworkLog.RemoveDynamic(this, &UZMMOGameInstance::HandleZeusNetworkLog);
}
void UZMMOGameInstance::HandleZeusConnected()
{
UE_LOG(LogZMMO, Log, TEXT("[Zeus] Connected to server %s:%d"), *ZeusServerHost, ZeusServerPort);
}
void UZMMOGameInstance::HandleZeusConnectionFailed(FString Reason)
{
UE_LOG(LogZMMO, Warning, TEXT("[Zeus] Connection failed: %s"), *Reason);
}
void UZMMOGameInstance::HandleZeusDisconnected()
{
UE_LOG(LogZMMO, Log, TEXT("[Zeus] Disconnected."));
}
void UZMMOGameInstance::HandleZeusNetworkLog(FString Message)
{
UE_LOG(LogZMMO, Log, TEXT("[Zeus] %s"), *Message);
}

View File

@@ -0,0 +1,68 @@
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "ZMMOGameInstance.generated.h"
class UZeusNetworkSubsystem;
/**
* UZMMOGameInstance
*
* Game instance do cliente Zeus MMO. O `UZeusNetworkSubsystem` (do plugin
* `ZeusNetwork`) e um `UGameInstanceSubsystem` e portanto e criado
* automaticamente em qualquer `UGameInstance`; esta classe existe sobretudo
* como ponto de extensao para:
*
* - Auto-connect ao servidor Zeus em `Init()` (`bAutoConnectOnStart`).
* - Expor a configuracao default (`Host`, `Port`) ao Project Settings.
* - Hooks de logout / shutdown limpos em `Shutdown()`.
*
* Marcada `Blueprintable, BlueprintType` para permitir um BP filho com
* overrides de configuracao por build (dev / staging / prod).
*/
UCLASS(Blueprintable, BlueprintType)
class ZMMO_API UZMMOGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
virtual void Init() override;
virtual void Shutdown() override;
/**
* Se `true`, tenta ligar ao servidor Zeus em `Init()` usando
* `ZeusServerHost`/`ZeusServerPort`. Default `true` em V0 (smoke test);
* passar a `false` quando existir UI de login que controle a conexao.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Config, Category = "ZMMO|Network")
bool bAutoConnectOnStart = true;
/** Host (IPv4 ou hostname) do servidor Zeus. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Config, Category = "ZMMO|Network")
FString ZeusServerHost = TEXT("127.0.0.1");
/** Porta UDP do servidor Zeus (1..65535). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Config, Category = "ZMMO|Network", meta = (ClampMin = "1", ClampMax = "65535"))
int32 ZeusServerPort = 27777;
protected:
/** Devolve o subsystem Zeus desta game instance (criado automaticamente pelo plugin). */
UFUNCTION(BlueprintPure, Category = "ZMMO|Network")
UZeusNetworkSubsystem* GetZeusNetwork() const;
void BindZeusDelegates(UZeusNetworkSubsystem* Zeus);
void UnbindZeusDelegates(UZeusNetworkSubsystem* Zeus);
UFUNCTION()
void HandleZeusConnected();
UFUNCTION()
void HandleZeusConnectionFailed(FString Reason);
UFUNCTION()
void HandleZeusDisconnected();
UFUNCTION()
void HandleZeusNetworkLog(FString Message);
};

View File

@@ -0,0 +1,10 @@
#include "ZMMOGameMode.h"
#include "ZMMOPlayerCharacter.h"
#include "ZMMOPlayerController.h"
AZMMOGameMode::AZMMOGameMode()
{
DefaultPawnClass = AZMMOPlayerCharacter::StaticClass();
PlayerControllerClass = AZMMOPlayerController::StaticClass();
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "ZMMOGameMode.generated.h"
/**
* AZMMOGameMode
*
* Game mode do cliente Zeus MMO. Configura `AZMMOPlayerCharacter` como
* `DefaultPawnClass` e `AZMMOPlayerController` como `PlayerControllerClass`.
*
* A classe e instanciavel directamente (apontada por
* `GlobalDefaultGameMode=/Script/ZMMO.ZMMOGameMode` em
* `Config/DefaultEngine.ini`); um BP filho e opcional para overrides
* por mapa / build (skeletal mesh, AnimBP custom, etc.).
*/
UCLASS(Blueprintable, BlueprintType)
class ZMMO_API AZMMOGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
AZMMOGameMode();
};

View File

@@ -0,0 +1,239 @@
#include "ZMMOWorldSubsystem.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "ZMMO.h"
#include "ZMMOEntity.h"
#include "ZMMOEntityInterface.h"
#include "ZMMOPlayerProxy.h"
#include "ZeusNetworkSubsystem.h"
void UZMMOWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
if (RemoteEntityClasses.IsEmpty())
{
RemoteEntityClasses.Add(EZMMOEntityType::Player, AZMMOPlayerProxy::StaticClass());
RemoteEntityClasses.Add(EZMMOEntityType::Mob, AZMMOEntity::StaticClass());
RemoteEntityClasses.Add(EZMMOEntityType::NPC, AZMMOEntity::StaticClass());
RemoteEntityClasses.Add(EZMMOEntityType::Object, AZMMOEntity::StaticClass());
}
if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem())
{
ZeusNet->OnPlayerSpawned.AddDynamic(this, &UZMMOWorldSubsystem::HandlePlayerSpawned);
ZeusNet->OnPlayerDespawned.AddDynamic(this, &UZMMOWorldSubsystem::HandlePlayerDespawned);
ZeusNet->OnPlayerStateUpdate.AddDynamic(this, &UZMMOWorldSubsystem::HandlePlayerStateUpdate);
UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem bound to ZeusNetworkSubsystem delegates."));
}
else
{
UE_LOG(LogZMMO, Warning, TEXT("ZMMOWorldSubsystem: ZeusNetworkSubsystem not found at Initialize."));
}
}
void UZMMOWorldSubsystem::Deinitialize()
{
if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem())
{
ZeusNet->OnPlayerSpawned.RemoveDynamic(this, &UZMMOWorldSubsystem::HandlePlayerSpawned);
ZeusNet->OnPlayerDespawned.RemoveDynamic(this, &UZMMOWorldSubsystem::HandlePlayerDespawned);
ZeusNet->OnPlayerStateUpdate.RemoveDynamic(this, &UZMMOWorldSubsystem::HandlePlayerStateUpdate);
}
RemoteEntities.Reset();
LocalEntityId = 0;
Super::Deinitialize();
}
void UZMMOWorldSubsystem::RegisterLocalEntity(const int64 EntityId, AActor* LocalActor)
{
if (EntityId == 0 || LocalActor == nullptr)
{
return;
}
LocalEntityId = EntityId;
RemoteEntities.Add(EntityId, TWeakObjectPtr<AActor>(LocalActor));
UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem: local entity registered EntityId=%lld actor=%s"),
EntityId, *GetNameSafe(LocalActor));
}
void UZMMOWorldSubsystem::HandlePlayerSpawned(const int32 EntityId, const bool bIsLocal,
const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
{
if (EntityId == 0)
{
return;
}
if (bIsLocal)
{
// O proprio AZMMOPlayerCharacter cuida da sua identidade quando recebe o
// delegate por outro caminho. Aqui apenas memorizamos para filtrar
// snapshots locais (cliente solto, sem reconciliacao em V0).
LocalEntityId = EntityId;
return;
}
if (RemoteEntities.Contains(EntityId))
{
// Ja existe um proxy: reactivar e re-snap.
if (AActor* Existing = RemoteEntities[EntityId].Get())
{
if (IZMMOEntityInterface* AsEntity = Cast<IZMMOEntityInterface>(Existing))
{
AsEntity->SetEntityRelevant(true);
}
Existing->SetActorLocationAndRotation(PosCm, FRotator(0.0f, YawDeg, 0.0f));
}
return;
}
UWorld* World = GetWorld();
if (!World)
{
return;
}
UClass* ProxyClass = ResolveActorClass(EZMMOEntityType::Player);
if (!ProxyClass)
{
UE_LOG(LogZMMO, Warning, TEXT("ZMMOWorldSubsystem: no class registered for Player; spawn aborted."));
return;
}
FActorSpawnParameters Params;
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
const FRotator SpawnRot(0.0f, YawDeg, 0.0f);
AActor* SpawnedActor = World->SpawnActor<AActor>(ProxyClass, PosCm, SpawnRot, Params);
if (!SpawnedActor)
{
UE_LOG(LogZMMO, Warning, TEXT("ZMMOWorldSubsystem: SpawnActor failed for EntityId=%d"), EntityId);
return;
}
if (IZMMOEntityInterface* AsEntity = Cast<IZMMOEntityInterface>(SpawnedActor))
{
// Inject identity via dedicated setter when the spawned actor exposes one.
if (AZMMOPlayerProxy* Proxy = Cast<AZMMOPlayerProxy>(SpawnedActor))
{
Proxy->SetZMMOIdentity(EntityId, EZMMOEntityType::Player);
}
else if (AZMMOEntity* Entity = Cast<AZMMOEntity>(SpawnedActor))
{
Entity->SetZMMOIdentity(EntityId, EZMMOEntityType::Player);
}
AsEntity->SetEntityRelevant(true);
}
RemoteEntities.Add(EntityId, TWeakObjectPtr<AActor>(SpawnedActor));
UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem: spawned remote EntityId=%d at (%s) yaw=%.1f t=%lld"),
EntityId, *PosCm.ToString(), YawDeg, ServerTimeMs);
}
void UZMMOWorldSubsystem::HandlePlayerDespawned(const int32 EntityId)
{
const TWeakObjectPtr<AActor>* Entry = RemoteEntities.Find(EntityId);
if (!Entry)
{
return;
}
if (AActor* Actor = Entry->Get())
{
if (IZMMOEntityInterface* AsEntity = Cast<IZMMOEntityInterface>(Actor))
{
AsEntity->SetEntityRelevant(false);
}
}
UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem: despawn EntityId=%d"), EntityId);
}
void UZMMOWorldSubsystem::HandlePlayerStateUpdate(const int32 EntityId, const int32 InputSeq,
const FVector PosCm, const FVector VelCmS, const bool bGrounded, const int64 ServerTimeMs)
{
if (EntityId == 0)
{
return;
}
if (LocalEntityId != 0 && static_cast<int64>(EntityId) == LocalEntityId)
{
// Cliente local solto: ignoramos snapshots autoritativos para o nosso
// proprio personagem em V0 (ADR 0038). Reconciliacao opcional pode ser
// activada por flag em `AZMMOPlayerCharacter` quando colisao de objetos
// chegar.
return;
}
const TWeakObjectPtr<AActor>* Entry = RemoteEntities.Find(EntityId);
if (!Entry)
{
// Snapshot antes de spawn: o spawn vira em `OnPlayerSpawned` em breve.
return;
}
AActor* Actor = Entry->Get();
if (!Actor)
{
RemoteEntities.Remove(EntityId);
return;
}
IZMMOEntityInterface* AsEntity = Cast<IZMMOEntityInterface>(Actor);
if (!AsEntity)
{
return;
}
FZMMOEntitySnapshot Snapshot;
Snapshot.EntityId = EntityId;
Snapshot.EntityType = AsEntity->GetZMMOEntityType();
Snapshot.LastProcessedInputSeq = InputSeq;
Snapshot.PositionCm = PosCm;
Snapshot.VelocityCmS = VelCmS;
// Yaw nao chega no opcode S_PLAYER_STATE actual; derivamos do XY da
// velocidade quando significativo, caso contrario mantemos rotacao do ator.
if (!FVector(VelCmS.X, VelCmS.Y, 0.0f).IsNearlyZero(1.0f))
{
Snapshot.YawDeg = FMath::RadiansToDegrees(FMath::Atan2(VelCmS.Y, VelCmS.X));
}
else
{
Snapshot.YawDeg = Actor->GetActorRotation().Yaw;
}
Snapshot.bGrounded = bGrounded;
Snapshot.ServerTimeMs = ServerTimeMs;
AsEntity->ApplyEntitySnapshot(Snapshot);
}
UClass* UZMMOWorldSubsystem::ResolveActorClass(const EZMMOEntityType EntityType) const
{
if (const TSubclassOf<AActor>* Found = RemoteEntityClasses.Find(EntityType))
{
if (Found->Get())
{
return Found->Get();
}
}
return AZMMOEntity::StaticClass();
}
UZeusNetworkSubsystem* UZMMOWorldSubsystem::ResolveZeusNetworkSubsystem() const
{
const UWorld* World = GetWorld();
if (!World)
{
return nullptr;
}
UGameInstance* GI = World->GetGameInstance();
if (!GI)
{
return nullptr;
}
return GI->GetSubsystem<UZeusNetworkSubsystem>();
}

View File

@@ -0,0 +1,95 @@
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "ZMMOEntityTypes.h"
#include "ZMMOWorldSubsystem.generated.h"
class AActor;
class AZMMOPlayerCharacter;
class AZMMOPlayerProxy;
class UZeusNetworkSubsystem;
/**
* UZMMOWorldSubsystem
*
* Subsistema por-mundo que orquestra a aplicacao de pacotes de replicacao
* recebidos do servidor (`S_SPAWN_PLAYER`, `S_DESPAWN_PLAYER`,
* `S_PLAYER_STATE`) em atores na cena. Mantem um registry
* `EntityId -> AActor*` e despacha cada mensagem para o ator correto.
*
* Por que `UWorldSubsystem` (e nao `UGameInstanceSubsystem`)?
* - Atores so existem dentro de um `UWorld`. Quando o mapa troca, este
* registry e descartado naturalmente — sem leak de ponteiros para
* atores do mundo anterior.
* - O `UZeusNetworkSubsystem` (per-game-instance) continua dono da
* conexao UDP e dos delegates; este subsistema apenas escuta os
* delegates e materializa atores na cena atual.
*
* Resolucao de classe por `EZMMOEntityType`:
* - `RemoteEntityClasses` e um TMap configuravel (BlueprintReadWrite)
* com defaults seguros em `Initialize`. Permite trocar
* `AZMMOPlayerProxy` por um BP filho com mesh customizada sem
* recompilar C++.
*
* Despawn:
* - V0: chama `IZMMOEntityInterface::SetEntityRelevant(false)` (preserva
* o ator para futuro pooling).
* - V1: pool real por `EntityType`, limites de memoria, expiracao.
*/
UCLASS()
class ZMMO_API UZMMOWorldSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
/**
* Regista o jogador local no registry. Chamado por `AZMMOPlayerCharacter`
* apos receber o `EntityId` autoritativo do servidor (delegate
* `OnPlayerSpawned` com `bIsLocal=true`).
*/
UFUNCTION(BlueprintCallable, Category = "ZMMO|World")
void RegisterLocalEntity(int64 EntityId, AActor* LocalActor);
/** Numero de entidades remotas actualmente trackeadas. Util em debug overlays. */
UFUNCTION(BlueprintPure, Category = "ZMMO|World")
int32 GetTrackedEntityCount() const { return RemoteEntities.Num(); }
protected:
/**
* Mapa configuravel de classe a usar por `EZMMOEntityType`. Caso vazio,
* usamos defaults seguros (`AZMMOPlayerProxy` para Player, `AZMMOEntity`
* para Mob/NPC/Object — actualizado quando classes especificas existirem).
*
* Pode ser preenchido via Blueprint subclassing ou em runtime.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "ZMMO|World")
TMap<EZMMOEntityType, TSubclassOf<AActor>> RemoteEntityClasses;
private:
UFUNCTION()
void HandlePlayerSpawned(int32 EntityId, bool bIsLocal, FVector PosCm, float YawDeg, int64 ServerTimeMs);
UFUNCTION()
void HandlePlayerDespawned(int32 EntityId);
UFUNCTION()
void HandlePlayerStateUpdate(int32 EntityId, int32 InputSeq, FVector PosCm, FVector VelCmS, bool bGrounded, int64 ServerTimeMs);
UClass* ResolveActorClass(EZMMOEntityType EntityType) const;
UZeusNetworkSubsystem* ResolveZeusNetworkSubsystem() const;
/**
* Registry de proxies remotos. Chave = `EntityId` autoritativo do servidor.
* Usamos `TWeakObjectPtr` para nao impedir GC se algo der errado e o ator
* for destruido externamente (ex.: troca de mapa).
*/
UPROPERTY()
TMap<int64, TWeakObjectPtr<AActor>> RemoteEntities;
/** Cached id do jogador local, para ignorar snapshots dele (cliente solto). */
int64 LocalEntityId = 0;
};

33
Source/ZMMO/ZMMO.Build.cs Normal file
View File

@@ -0,0 +1,33 @@
using UnrealBuildTool;
public class ZMMO : ModuleRules
{
public ZMMO(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"InputCore",
"EnhancedInput",
"UMG",
"Slate",
"ZeusNetwork"
});
PrivateDependencyModuleNames.AddRange(new string[] {
"SlateCore"
});
PublicIncludePaths.AddRange(new string[] {
"ZMMO",
"ZMMO/Game",
"ZMMO/Game/Entity",
"ZMMO/Game/Controller",
"ZMMO/Game/Modes",
"ZMMO/Game/Network"
});
}
}

8
Source/ZMMO/ZMMO.cpp Normal file
View File

@@ -0,0 +1,8 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "ZMMO.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, ZMMO, "ZMMO" );
DEFINE_LOG_CATEGORY(LogZMMO)

8
Source/ZMMO/ZMMO.h Normal file
View File

@@ -0,0 +1,8 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
/** Main log category used across the project */
DECLARE_LOG_CATEGORY_EXTERN(LogZMMO, Log, All);

View File

@@ -0,0 +1,15 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
using System.Collections.Generic;
public class ZMMOEditorTarget : TargetRules
{
public ZMMOEditorTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Editor;
DefaultBuildSettings = BuildSettingsVersion.V6;
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7;
ExtraModuleNames.Add("ZMMO");
}
}