Files
ZMMO/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.h
Mateus Rodrigues 385ebf6a7b feat(client): proxy animation via custom CMC + injected acceleration
Porta a estratégia do ZClientMMO para o ZMMO: novo
UZMMOProxyMovementComponent expõe escrita externa de Acceleration,
e AZMMOPlayerProxy deriva accel a partir da variação de velocidade
entre snapshots (com smoothing e fallback sintético em locomotion
constante). Tick do proxy roda antes do mesh para alimentar
Velocity+Acceleration antes do NativeUpdateAnimation. Cliente local
agora também envia Vel.Z e bIsFalling para que proxies vejam pulo
completo (subida + queda).
2026-05-08 22:54:54 -03:00

163 lines
5.9 KiB
C++

#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;
/** Último estado IsFalling enviado ao servidor. Usado para forçar um envio
* imediato em transições walking↔falling, evitando que o rate limit segure
* a transição até o próximo heartbeat. */
bool bPreviousFalling = false;
float SendAccumulatorSec = 0.0f;
float TimeSinceLastSendSec = 0.0f;
int32 InputSequence = 0;
};