process TODO.md step 5
This commit is contained in:
@@ -6,6 +6,7 @@ using Vector3 = UnityEngine.Vector3;
|
||||
public sealed class ClientAuthoritativePlayerState
|
||||
{
|
||||
public ClientAuthoritativePlayerStateSnapshot Current { get; private set; }
|
||||
public ClientCombatPresentationSnapshot CombatPresentation { get; private set; } = ClientCombatPresentationSnapshot.Empty;
|
||||
|
||||
public bool TryAccept(PlayerState state, out ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
@@ -22,8 +23,67 @@ public sealed class ClientAuthoritativePlayerState
|
||||
|
||||
snapshot = new ClientAuthoritativePlayerStateSnapshot(state);
|
||||
Current = snapshot;
|
||||
CombatPresentation = CombatPresentation.WithAuthoritativeSnapshot(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryApplyCombatEvent(CombatEvent combatEvent, string playerId, out ClientAuthoritativePlayerStateSnapshot snapshot, out ClientCombatPresentationSnapshot combatSnapshot)
|
||||
{
|
||||
if (combatEvent == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(combatEvent));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(playerId))
|
||||
{
|
||||
throw new ArgumentException("Player id is required.", nameof(playerId));
|
||||
}
|
||||
|
||||
if (!ClientCombatEventRouting.TryGetAffectedPlayerId(combatEvent, out var affectedPlayerId)
|
||||
|| !string.Equals(affectedPlayerId, playerId, StringComparison.Ordinal))
|
||||
{
|
||||
snapshot = Current;
|
||||
combatSnapshot = CombatPresentation;
|
||||
return false;
|
||||
}
|
||||
|
||||
Current = ApplyEventToCurrentSnapshot(combatEvent);
|
||||
CombatPresentation = CombatPresentation.WithCombatEvent(combatEvent, Current);
|
||||
|
||||
snapshot = Current;
|
||||
combatSnapshot = CombatPresentation;
|
||||
return true;
|
||||
}
|
||||
|
||||
private ClientAuthoritativePlayerStateSnapshot ApplyEventToCurrentSnapshot(CombatEvent combatEvent)
|
||||
{
|
||||
if (Current == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (combatEvent.EventType)
|
||||
{
|
||||
case CombatEventType.DamageApplied:
|
||||
return CloneSnapshotWithHp(Mathf.Max(0, Current.Hp - Mathf.Max(0, combatEvent.Damage)));
|
||||
case CombatEventType.Death:
|
||||
return CloneSnapshotWithHp(0);
|
||||
default:
|
||||
return Current;
|
||||
}
|
||||
}
|
||||
|
||||
private ClientAuthoritativePlayerStateSnapshot CloneSnapshotWithHp(int hp)
|
||||
{
|
||||
if (Current == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourceState = Current.SourceState.Clone();
|
||||
sourceState.Hp = hp;
|
||||
return new ClientAuthoritativePlayerStateSnapshot(sourceState);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ClientAuthoritativePlayerStateSnapshot
|
||||
@@ -60,3 +120,74 @@ public sealed class ClientAuthoritativePlayerStateSnapshot
|
||||
|
||||
public Quaternion RotationQuaternion => Quaternion.Euler(0f, Rotation, 0f);
|
||||
}
|
||||
|
||||
public sealed class ClientCombatPresentationSnapshot
|
||||
{
|
||||
public static readonly ClientCombatPresentationSnapshot Empty = new(false, CombatEventType.Unspecified, 0, 0, Vector3.zero, false);
|
||||
|
||||
public ClientCombatPresentationSnapshot(
|
||||
bool hasLastEvent,
|
||||
CombatEventType lastEventType,
|
||||
long lastEventTick,
|
||||
int lastDamage,
|
||||
Vector3 lastHitPosition,
|
||||
bool isDead)
|
||||
{
|
||||
HasLastEvent = hasLastEvent;
|
||||
LastEventType = lastEventType;
|
||||
LastEventTick = lastEventTick;
|
||||
LastDamage = lastDamage;
|
||||
LastHitPosition = lastHitPosition;
|
||||
IsDead = isDead;
|
||||
}
|
||||
|
||||
public bool HasLastEvent { get; }
|
||||
|
||||
public CombatEventType LastEventType { get; }
|
||||
|
||||
public long LastEventTick { get; }
|
||||
|
||||
public int LastDamage { get; }
|
||||
|
||||
public Vector3 LastHitPosition { get; }
|
||||
|
||||
public bool IsDead { get; }
|
||||
|
||||
public ClientCombatPresentationSnapshot WithAuthoritativeSnapshot(ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
if (snapshot == null)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
return new ClientCombatPresentationSnapshot(
|
||||
HasLastEvent,
|
||||
LastEventType,
|
||||
LastEventTick,
|
||||
LastDamage,
|
||||
LastHitPosition,
|
||||
snapshot.Hp <= 0);
|
||||
}
|
||||
|
||||
public ClientCombatPresentationSnapshot WithCombatEvent(CombatEvent combatEvent, ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
if (combatEvent == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(combatEvent));
|
||||
}
|
||||
|
||||
var isDead = combatEvent.EventType == CombatEventType.Death;
|
||||
if (!isDead && snapshot != null)
|
||||
{
|
||||
isDead = snapshot.Hp <= 0;
|
||||
}
|
||||
|
||||
return new ClientCombatPresentationSnapshot(
|
||||
true,
|
||||
combatEvent.EventType,
|
||||
combatEvent.Tick,
|
||||
combatEvent.Damage,
|
||||
combatEvent.HitPosition != null ? combatEvent.HitPosition.ToVector3() : Vector3.zero,
|
||||
isDead);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using Network.Defines;
|
||||
|
||||
public static class ClientCombatEventRouting
|
||||
{
|
||||
public static bool TryGetAffectedPlayerId(CombatEvent combatEvent, out string playerId)
|
||||
{
|
||||
if (combatEvent == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(combatEvent));
|
||||
}
|
||||
|
||||
switch (combatEvent.EventType)
|
||||
{
|
||||
case CombatEventType.ShootRejected:
|
||||
playerId = combatEvent.AttackerId ?? string.Empty;
|
||||
return !string.IsNullOrEmpty(playerId);
|
||||
case CombatEventType.Hit:
|
||||
case CombatEventType.DamageApplied:
|
||||
case CombatEventType.Death:
|
||||
playerId = combatEvent.TargetId ?? string.Empty;
|
||||
return !string.IsNullOrEmpty(playerId);
|
||||
default:
|
||||
playerId = string.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f12e9d15fab40204e9740ac5641c7b95
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -73,8 +73,31 @@ public class MasterManager : MonoBehaviour
|
||||
else Debug.LogWarning("Player not found");
|
||||
}
|
||||
|
||||
public void ApplyCombatEvent(CombatEvent combatEvent)
|
||||
{
|
||||
if (combatEvent == null)
|
||||
{
|
||||
Debug.LogWarning("CombatEvent is null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ClientCombatEventRouting.TryGetAffectedPlayerId(combatEvent, out var playerId))
|
||||
{
|
||||
Debug.LogWarning($"CombatEvent has no routable player: {combatEvent.EventType}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_players.TryGetValue(playerId, out var player))
|
||||
{
|
||||
player.ApplyCombatEvent(combatEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"Player not found for CombatEvent: {playerId}");
|
||||
}
|
||||
|
||||
public Player GetCurrentPlayer()
|
||||
{
|
||||
return _players[LocalPlayerId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ public class NetworkManager : MonoBehaviour
|
||||
{
|
||||
_networkRuntime.MessageManager.RegisterHandler(MessageType.LoginResponse, HandleLoginResponse);
|
||||
_networkRuntime.MessageManager.RegisterHandler(MessageType.PlayerState, HandlePlayerState);
|
||||
_networkRuntime.MessageManager.RegisterHandler(MessageType.CombatEvent, HandleCombatEvent);
|
||||
_networkRuntime.MessageManager.RegisterHandler(MessageType.HeartbeatResponse, HandleHeartbeatResponse);
|
||||
_networkRuntime.MessageManager.RegisterHandler(MessageType.LogoutRequest, HandleLogoutRequest);
|
||||
_networkRuntime.MessageManager.RegisterHandler(MessageType.PlayerJoin, HandlePlayerJoin);
|
||||
@@ -178,6 +179,14 @@ public class NetworkManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCombatEvent(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
_networkRuntime.NotifyInboundActivity();
|
||||
var combatEvent = CombatEvent.Parser.ParseFrom(data);
|
||||
MasterManager.Instance.ApplyCombatEvent(combatEvent);
|
||||
Debug.Log($"收到CombatEvent::Type={combatEvent.EventType},Attacker={combatEvent.AttackerId},Target={combatEvent.TargetId},Damage={combatEvent.Damage}");
|
||||
}
|
||||
|
||||
private void HandleLogoutRequest(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
_networkRuntime.NotifyInboundActivity();
|
||||
|
||||
@@ -6,6 +6,7 @@ public class Player : MonoBehaviour
|
||||
//[SerializeField] private float _moveSpeed = 10f;
|
||||
public string PlayerId { get; private set; } = "1001";
|
||||
public ClientAuthoritativePlayerStateSnapshot AuthoritativeState => _authoritativeState.Current;
|
||||
public ClientCombatPresentationSnapshot CombatPresentation => _authoritativeState.CombatPresentation;
|
||||
[SerializeField] private MeshRenderer _meshRenderer;
|
||||
[SerializeField] private Material[] _materials;
|
||||
[SerializeField] private Camera _camera;
|
||||
@@ -56,10 +57,21 @@ public class Player : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
_playerUI?.SyncAuthoritativeState(snapshot);
|
||||
_playerUI?.SyncAuthoritativeState(snapshot, CombatPresentation);
|
||||
_movement?.OnAuthoritativeState(snapshot);
|
||||
}
|
||||
|
||||
public bool ApplyCombatEvent(CombatEvent combatEvent)
|
||||
{
|
||||
if (!_authoritativeState.TryApplyCombatEvent(combatEvent, PlayerId, out var snapshot, out var combatSnapshot))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_playerUI?.SyncAuthoritativeState(snapshot, combatSnapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SyncTick(long serverTick)
|
||||
{
|
||||
_movement.SetServerTick(serverTick);
|
||||
|
||||
@@ -8,6 +8,8 @@ public class PlayerUI : MonoBehaviour
|
||||
private Player _master;
|
||||
private Camera _mainCamera;
|
||||
private bool _isVisible = true;
|
||||
private ClientAuthoritativePlayerStateSnapshot _authoritativeSnapshot;
|
||||
private ClientCombatPresentationSnapshot _combatSnapshot = ClientCombatPresentationSnapshot.Empty;
|
||||
|
||||
public void Init(Player master)
|
||||
{
|
||||
@@ -17,9 +19,11 @@ public class PlayerUI : MonoBehaviour
|
||||
RefreshText();
|
||||
}
|
||||
|
||||
public void SyncAuthoritativeState(ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
public void SyncAuthoritativeState(ClientAuthoritativePlayerStateSnapshot snapshot, ClientCombatPresentationSnapshot combatSnapshot)
|
||||
{
|
||||
RefreshText(snapshot);
|
||||
_authoritativeSnapshot = snapshot;
|
||||
_combatSnapshot = combatSnapshot ?? ClientCombatPresentationSnapshot.Empty;
|
||||
RefreshText();
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
@@ -47,19 +51,30 @@ public class PlayerUI : MonoBehaviour
|
||||
_isVisible = false;
|
||||
}
|
||||
|
||||
private void RefreshText(ClientAuthoritativePlayerStateSnapshot snapshot = null)
|
||||
private void RefreshText()
|
||||
{
|
||||
if (_text == null || _master == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (snapshot == null)
|
||||
if (_authoritativeSnapshot == null)
|
||||
{
|
||||
_text.text = _master.PlayerId;
|
||||
_text.text = $"{_master.PlayerId}\nHP:?\nCombat:{FormatCombatLine()}";
|
||||
return;
|
||||
}
|
||||
|
||||
_text.text = $"{_master.PlayerId}\nHP:{snapshot.Hp} Tick:{snapshot.Tick}";
|
||||
_text.text = $"{_master.PlayerId}\nHP:{_authoritativeSnapshot.Hp} Tick:{_authoritativeSnapshot.Tick}\nCombat:{FormatCombatLine()}";
|
||||
}
|
||||
|
||||
private string FormatCombatLine()
|
||||
{
|
||||
if (_combatSnapshot == null || !_combatSnapshot.HasLastEvent)
|
||||
{
|
||||
return _combatSnapshot != null && _combatSnapshot.IsDead ? "Dead" : "None";
|
||||
}
|
||||
|
||||
var deadSuffix = _combatSnapshot.IsDead ? " Dead" : string.Empty;
|
||||
return $"{_combatSnapshot.LastEventType} Dmg:{_combatSnapshot.LastDamage} Tick:{_combatSnapshot.LastEventTick}{deadSuffix}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user