process TODO.md step 5
This commit is contained in:
+3361
-158
File diff suppressed because it is too large
Load Diff
@@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +156,119 @@ namespace Tests.EditMode.Network
|
||||
Assert.That(snapshot.SourceState.Hp, Is.EqualTo(88));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientCombatEventRouting_DamageEvent_RoutesToTargetPlayer()
|
||||
{
|
||||
var routed = ClientCombatEventRouting.TryGetAffectedPlayerId(
|
||||
new CombatEvent
|
||||
{
|
||||
EventType = CombatEventType.DamageApplied,
|
||||
AttackerId = "player-a",
|
||||
TargetId = "player-b",
|
||||
Damage = 15
|
||||
},
|
||||
out var playerId);
|
||||
|
||||
Assert.That(routed, Is.True);
|
||||
Assert.That(playerId, Is.EqualTo("player-b"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientCombatEventRouting_ShootRejected_RoutesToAttackerPlayer()
|
||||
{
|
||||
var routed = ClientCombatEventRouting.TryGetAffectedPlayerId(
|
||||
new CombatEvent
|
||||
{
|
||||
EventType = CombatEventType.ShootRejected,
|
||||
AttackerId = "player-a",
|
||||
TargetId = "player-b"
|
||||
},
|
||||
out var playerId);
|
||||
|
||||
Assert.That(routed, Is.True);
|
||||
Assert.That(playerId, Is.EqualTo("player-a"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientAuthoritativePlayerState_DamageCombatEvent_ReducesHpAndRecordsCombatResult()
|
||||
{
|
||||
var owner = new ClientAuthoritativePlayerState();
|
||||
owner.TryAccept(new PlayerState { PlayerId = "player-1", Tick = 10, Hp = 100 }, out _);
|
||||
|
||||
var applied = owner.TryApplyCombatEvent(
|
||||
new CombatEvent
|
||||
{
|
||||
Tick = 11,
|
||||
EventType = CombatEventType.DamageApplied,
|
||||
AttackerId = "player-2",
|
||||
TargetId = "player-1",
|
||||
Damage = 30
|
||||
},
|
||||
"player-1",
|
||||
out var snapshot,
|
||||
out var combatSnapshot);
|
||||
|
||||
Assert.That(applied, Is.True);
|
||||
Assert.That(snapshot, Is.Not.Null);
|
||||
Assert.That(snapshot.Hp, Is.EqualTo(70));
|
||||
Assert.That(owner.Current.Hp, Is.EqualTo(70));
|
||||
Assert.That(combatSnapshot.HasLastEvent, Is.True);
|
||||
Assert.That(combatSnapshot.LastEventType, Is.EqualTo(CombatEventType.DamageApplied));
|
||||
Assert.That(combatSnapshot.LastDamage, Is.EqualTo(30));
|
||||
Assert.That(combatSnapshot.IsDead, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientAuthoritativePlayerState_ShootRejected_LeavesHpUnchangedAndRecordsVisibility()
|
||||
{
|
||||
var owner = new ClientAuthoritativePlayerState();
|
||||
owner.TryAccept(new PlayerState { PlayerId = "player-1", Tick = 10, Hp = 90 }, out _);
|
||||
|
||||
var applied = owner.TryApplyCombatEvent(
|
||||
new CombatEvent
|
||||
{
|
||||
Tick = 12,
|
||||
EventType = CombatEventType.ShootRejected,
|
||||
AttackerId = "player-1",
|
||||
TargetId = "player-2"
|
||||
},
|
||||
"player-1",
|
||||
out var snapshot,
|
||||
out var combatSnapshot);
|
||||
|
||||
Assert.That(applied, Is.True);
|
||||
Assert.That(snapshot.Hp, Is.EqualTo(90));
|
||||
Assert.That(combatSnapshot.LastEventType, Is.EqualTo(CombatEventType.ShootRejected));
|
||||
Assert.That(combatSnapshot.LastDamage, Is.EqualTo(0));
|
||||
Assert.That(combatSnapshot.IsDead, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientAuthoritativePlayerState_DeathCombatEvent_MarksPlayerDeadAndAllowsLaterSnapshotRefresh()
|
||||
{
|
||||
var owner = new ClientAuthoritativePlayerState();
|
||||
owner.TryAccept(new PlayerState { PlayerId = "player-1", Tick = 10, Hp = 20 }, out _);
|
||||
owner.TryApplyCombatEvent(
|
||||
new CombatEvent
|
||||
{
|
||||
Tick = 11,
|
||||
EventType = CombatEventType.Death,
|
||||
AttackerId = "player-2",
|
||||
TargetId = "player-1"
|
||||
},
|
||||
"player-1",
|
||||
out var deathSnapshot,
|
||||
out var deathCombatSnapshot);
|
||||
|
||||
var accepted = owner.TryAccept(new PlayerState { PlayerId = "player-1", Tick = 12, Hp = 55 }, out var refreshedSnapshot);
|
||||
|
||||
Assert.That(deathSnapshot.Hp, Is.EqualTo(0));
|
||||
Assert.That(deathCombatSnapshot.IsDead, Is.True);
|
||||
Assert.That(accepted, Is.True);
|
||||
Assert.That(refreshedSnapshot.Hp, Is.EqualTo(55));
|
||||
Assert.That(owner.CombatPresentation.IsDead, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RemotePlayerSnapshotInterpolator_StaleOrDuplicateSnapshots_AreRejected()
|
||||
{
|
||||
|
||||
+6
-2
@@ -26,7 +26,7 @@ Material:
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 2800000, guid: 79cfbbc88ce488e41aca90a3043b4786, type: 3}
|
||||
m_Scale: {x: 2, y: 1}
|
||||
m_Scale: {x: 10, y: 5}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
@@ -50,7 +50,7 @@ Material:
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: 79cfbbc88ce488e41aca90a3043b4786, type: 3}
|
||||
m_Scale: {x: 2, y: 1}
|
||||
m_Scale: {x: 10, y: 5}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
@@ -83,7 +83,9 @@ Material:
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BlendOp: 0
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
@@ -93,6 +95,7 @@ Material:
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 1
|
||||
- _Glossiness: 0.5
|
||||
@@ -108,6 +111,7 @@ Material:
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _UVSec: 0
|
||||
- _WorkflowMode: 1
|
||||
|
||||
Reference in New Issue
Block a user