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:
15
Source/ZMMO.Target.cs
Normal file
15
Source/ZMMO.Target.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
80
Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp
Normal file
80
Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp
Normal 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;
|
||||
}
|
||||
50
Source/ZMMO/Game/Controller/ZMMOPlayerController.h
Normal file
50
Source/ZMMO/Game/Controller/ZMMOPlayerController.h
Normal 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;
|
||||
};
|
||||
32
Source/ZMMO/Game/Entity/ZMMOEntity.cpp
Normal file
32
Source/ZMMO/Game/Entity/ZMMOEntity.cpp
Normal 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);
|
||||
}
|
||||
57
Source/ZMMO/Game/Entity/ZMMOEntity.h
Normal file
57
Source/ZMMO/Game/Entity/ZMMOEntity.h
Normal 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;
|
||||
};
|
||||
75
Source/ZMMO/Game/Entity/ZMMOEntityInterface.h
Normal file
75
Source/ZMMO/Game/Entity/ZMMOEntityInterface.h
Normal 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;
|
||||
};
|
||||
64
Source/ZMMO/Game/Entity/ZMMOEntityTypes.h
Normal file
64
Source/ZMMO/Game/Entity/ZMMOEntityTypes.h
Normal 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;
|
||||
};
|
||||
359
Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp
Normal file
359
Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp
Normal 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;
|
||||
}
|
||||
158
Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h
Normal file
158
Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h
Normal 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;
|
||||
};
|
||||
91
Source/ZMMO/Game/Entity/ZMMOPlayerProxy.cpp
Normal file
91
Source/ZMMO/Game/Entity/ZMMOPlayerProxy.cpp
Normal 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);
|
||||
}
|
||||
78
Source/ZMMO/Game/Entity/ZMMOPlayerProxy.h
Normal file
78
Source/ZMMO/Game/Entity/ZMMOPlayerProxy.h
Normal 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();
|
||||
};
|
||||
93
Source/ZMMO/Game/Modes/ZMMOGameInstance.cpp
Normal file
93
Source/ZMMO/Game/Modes/ZMMOGameInstance.cpp
Normal 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);
|
||||
}
|
||||
68
Source/ZMMO/Game/Modes/ZMMOGameInstance.h
Normal file
68
Source/ZMMO/Game/Modes/ZMMOGameInstance.h
Normal 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);
|
||||
};
|
||||
10
Source/ZMMO/Game/Modes/ZMMOGameMode.cpp
Normal file
10
Source/ZMMO/Game/Modes/ZMMOGameMode.cpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#include "ZMMOGameMode.h"
|
||||
|
||||
#include "ZMMOPlayerCharacter.h"
|
||||
#include "ZMMOPlayerController.h"
|
||||
|
||||
AZMMOGameMode::AZMMOGameMode()
|
||||
{
|
||||
DefaultPawnClass = AZMMOPlayerCharacter::StaticClass();
|
||||
PlayerControllerClass = AZMMOPlayerController::StaticClass();
|
||||
}
|
||||
25
Source/ZMMO/Game/Modes/ZMMOGameMode.h
Normal file
25
Source/ZMMO/Game/Modes/ZMMOGameMode.h
Normal 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();
|
||||
};
|
||||
239
Source/ZMMO/Game/Network/ZMMOWorldSubsystem.cpp
Normal file
239
Source/ZMMO/Game/Network/ZMMOWorldSubsystem.cpp
Normal 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>();
|
||||
}
|
||||
95
Source/ZMMO/Game/Network/ZMMOWorldSubsystem.h
Normal file
95
Source/ZMMO/Game/Network/ZMMOWorldSubsystem.h
Normal 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
33
Source/ZMMO/ZMMO.Build.cs
Normal 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
8
Source/ZMMO/ZMMO.cpp
Normal 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
8
Source/ZMMO/ZMMO.h
Normal 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);
|
||||
15
Source/ZMMOEditor.Target.cs
Normal file
15
Source/ZMMOEditor.Target.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user