收口 Client 端,添加服务端约束
This commit is contained in:
@@ -29,6 +29,24 @@ public static class ClientGameplayInputFlow
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryCreateShootInput(
|
||||
string playerId,
|
||||
long tick,
|
||||
bool fireTriggered,
|
||||
Vector3 aimDirection,
|
||||
out ShootInput message,
|
||||
string targetId = "")
|
||||
{
|
||||
if (!fireTriggered)
|
||||
{
|
||||
message = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
message = CreateShootInput(playerId, tick, aimDirection, targetId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ShootInput CreateShootInput(string playerId, long tick, Vector3 aimDirection, string targetId = "")
|
||||
{
|
||||
var planarDirection = new Vector3(aimDirection.x, 0f, aimDirection.z);
|
||||
@@ -210,12 +228,14 @@ public class MovementComponent : MonoBehaviour
|
||||
|
||||
private ShootInput CaptureShootInput()
|
||||
{
|
||||
if (!Input.GetMouseButtonDown(0))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ClientGameplayInputFlow.CreateShootInput(_master.PlayerId, Tick, ResolveAimDirection());
|
||||
return ClientGameplayInputFlow.TryCreateShootInput(
|
||||
_master.PlayerId,
|
||||
Tick,
|
||||
Input.GetMouseButtonDown(0),
|
||||
ResolveAimDirection(),
|
||||
out var shootInput)
|
||||
? shootInput
|
||||
: null;
|
||||
}
|
||||
|
||||
private Vector3 ResolveAimDirection()
|
||||
|
||||
@@ -165,8 +165,6 @@ namespace Network.NetworkHost
|
||||
|
||||
if (input == null ||
|
||||
string.IsNullOrWhiteSpace(input.PlayerId) ||
|
||||
string.IsNullOrWhiteSpace(input.TargetId) ||
|
||||
string.Equals(input.PlayerId, input.TargetId, StringComparison.Ordinal) ||
|
||||
!IsFinite(input.DirX) ||
|
||||
!IsFinite(input.DirY))
|
||||
{
|
||||
@@ -179,8 +177,13 @@ namespace Network.NetworkHost
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!movementCoordinator.TryGetState(sender, out attackerState) ||
|
||||
!string.Equals(attackerState.PlayerId, input.PlayerId, StringComparison.Ordinal) ||
|
||||
if (!movementCoordinator.TryGetState(sender, out attackerState) &&
|
||||
!movementCoordinator.EnsureState(sender, input.PlayerId, out attackerState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(attackerState.PlayerId, input.PlayerId, StringComparison.Ordinal) ||
|
||||
attackerState.IsDead ||
|
||||
attackerState.Hp <= 0 ||
|
||||
input.Tick <= attackerState.LastAcceptedShootTick)
|
||||
@@ -188,14 +191,93 @@ namespace Network.NetworkHost
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!movementCoordinator.TryGetStateByPlayerId(input.TargetId, out targetState) ||
|
||||
targetState.IsDead ||
|
||||
targetState.Hp <= 0)
|
||||
return TryResolveTargetState(input, attackerState, out targetState);
|
||||
}
|
||||
|
||||
private bool TryResolveTargetState(
|
||||
ShootInput input,
|
||||
ServerAuthoritativeMovementState attackerState,
|
||||
out ServerAuthoritativeMovementState targetState)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(input.TargetId))
|
||||
{
|
||||
if (string.Equals(attackerState.PlayerId, input.TargetId, StringComparison.Ordinal))
|
||||
{
|
||||
targetState = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!movementCoordinator.TryGetStateByPlayerId(input.TargetId, out targetState) ||
|
||||
targetState.IsDead ||
|
||||
targetState.Hp <= 0)
|
||||
{
|
||||
targetState = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return TryResolveTargetStateFromAim(input, attackerState, out targetState);
|
||||
}
|
||||
|
||||
private bool TryResolveTargetStateFromAim(
|
||||
ShootInput input,
|
||||
ServerAuthoritativeMovementState attackerState,
|
||||
out ServerAuthoritativeMovementState targetState)
|
||||
{
|
||||
targetState = null;
|
||||
|
||||
var aimLength = MathF.Sqrt((input.DirX * input.DirX) + (input.DirY * input.DirY));
|
||||
if (aimLength <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
var aimX = input.DirX / aimLength;
|
||||
var aimY = input.DirY / aimLength;
|
||||
var bestAlignment = float.NegativeInfinity;
|
||||
var bestDistanceSquared = float.PositiveInfinity;
|
||||
|
||||
foreach (var candidate in movementCoordinator.States)
|
||||
{
|
||||
if (candidate == null ||
|
||||
string.Equals(candidate.PlayerId, attackerState.PlayerId, StringComparison.Ordinal) ||
|
||||
candidate.IsDead ||
|
||||
candidate.Hp <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var offsetX = candidate.PositionX - attackerState.PositionX;
|
||||
var offsetY = candidate.PositionZ - attackerState.PositionZ;
|
||||
var distanceSquared = (offsetX * offsetX) + (offsetY * offsetY);
|
||||
if (distanceSquared <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var inverseDistance = 1f / MathF.Sqrt(distanceSquared);
|
||||
var alignment = (offsetX * inverseDistance * aimX) + (offsetY * inverseDistance * aimY);
|
||||
if (alignment <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (targetState == null ||
|
||||
alignment > bestAlignment + 0.0001f ||
|
||||
(MathF.Abs(alignment - bestAlignment) <= 0.0001f &&
|
||||
(distanceSquared < bestDistanceSquared - 0.0001f ||
|
||||
(MathF.Abs(distanceSquared - bestDistanceSquared) <= 0.0001f &&
|
||||
string.CompareOrdinal(candidate.PlayerId, targetState.PlayerId) < 0))))
|
||||
{
|
||||
targetState = candidate;
|
||||
bestAlignment = alignment;
|
||||
bestDistanceSquared = distanceSquared;
|
||||
}
|
||||
}
|
||||
|
||||
return targetState != null;
|
||||
}
|
||||
|
||||
private void BroadcastRejectedShot(ShootInput input)
|
||||
|
||||
@@ -41,6 +41,46 @@ namespace Network.NetworkHost
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnsureState(IPEndPoint remoteEndPoint, string playerId, out ServerAuthoritativeMovementState state)
|
||||
{
|
||||
if (remoteEndPoint == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(playerId))
|
||||
{
|
||||
state = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedSender = Normalize(remoteEndPoint);
|
||||
var key = normalizedSender.ToString();
|
||||
|
||||
lock (gate)
|
||||
{
|
||||
if (statesByPeer.TryGetValue(key, out var existingState))
|
||||
{
|
||||
if (!string.Equals(existingState.PlayerId, playerId, StringComparison.Ordinal))
|
||||
{
|
||||
state = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
state = CloneState(existingState);
|
||||
return true;
|
||||
}
|
||||
|
||||
var createdState = new ServerAuthoritativeMovementState(
|
||||
normalizedSender,
|
||||
playerId,
|
||||
configuration.DefaultHp);
|
||||
statesByPeer.Add(key, createdState);
|
||||
state = CloneState(createdState);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public Task HandleMoveInputAsync(byte[] payload, IPEndPoint sender)
|
||||
{
|
||||
if (payload == null || sender == null)
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace Network.NetworkHost
|
||||
private readonly MessageManager messageManager;
|
||||
private readonly ServerAuthoritativeMovementCoordinator authoritativeMovementCoordinator;
|
||||
private readonly ServerAuthoritativeCombatCoordinator authoritativeCombatCoordinator;
|
||||
private readonly object playerIdentityGate = new();
|
||||
private readonly Dictionary<string, string> playerIdsByPeer = new();
|
||||
|
||||
public ServerNetworkHost(
|
||||
ITransport transport,
|
||||
@@ -97,6 +99,10 @@ namespace Network.NetworkHost
|
||||
SessionCoordinator.RemoveAllSessions("Transport stopped");
|
||||
authoritativeMovementCoordinator.Clear();
|
||||
authoritativeCombatCoordinator.Clear();
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
playerIdsByPeer.Clear();
|
||||
}
|
||||
PublishMetricsSessionSnapshots();
|
||||
}
|
||||
|
||||
@@ -140,12 +146,20 @@ namespace Network.NetworkHost
|
||||
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
SessionCoordinator.NotifyLoginSucceeded(remoteEndPoint);
|
||||
BootstrapAuthoritativeMovementState(remoteEndPoint);
|
||||
PublishMetricsSessionSnapshot(remoteEndPoint);
|
||||
}
|
||||
|
||||
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint, string playerId)
|
||||
{
|
||||
RememberPlayerId(remoteEndPoint, playerId);
|
||||
NotifyLoginSucceeded(remoteEndPoint);
|
||||
}
|
||||
|
||||
public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null)
|
||||
{
|
||||
SessionCoordinator.NotifyLoginFailed(remoteEndPoint, reason);
|
||||
ForgetPlayerId(remoteEndPoint);
|
||||
PublishMetricsSessionSnapshot(remoteEndPoint);
|
||||
}
|
||||
|
||||
@@ -188,6 +202,7 @@ namespace Network.NetworkHost
|
||||
|
||||
authoritativeMovementCoordinator.RemoveState(remoteEndPoint);
|
||||
authoritativeCombatCoordinator.RemoveState(remoteEndPoint);
|
||||
ForgetPlayerId(remoteEndPoint);
|
||||
|
||||
RecordMetricsSessionSnapshot(transport, "server-host", session, ConnectionState.Disconnected);
|
||||
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
|
||||
@@ -198,12 +213,111 @@ namespace Network.NetworkHost
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleTransportReceive(byte[] _, IPEndPoint sender)
|
||||
private void HandleTransportReceive(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
SessionCoordinator.ObserveTransportActivity(sender);
|
||||
ObservePlayerIdentity(data, sender);
|
||||
PublishMetricsSessionSnapshot(sender);
|
||||
}
|
||||
|
||||
private void BootstrapAuthoritativeMovementState(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
authoritativeMovementCoordinator.EnsureState(remoteEndPoint, playerId, out _);
|
||||
}
|
||||
|
||||
private void ObservePlayerIdentity(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
if (data == null || sender == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Envelope envelope;
|
||||
try
|
||||
{
|
||||
envelope = Envelope.Parser.ParseFrom(data);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((MessageType)envelope.Type != MessageType.LoginRequest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LoginRequest request;
|
||||
try
|
||||
{
|
||||
request = LoginRequest.Parser.ParseFrom(envelope.Payload);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RememberPlayerId(sender, request.PlayerId);
|
||||
}
|
||||
|
||||
private void RememberPlayerId(IPEndPoint remoteEndPoint, string playerId)
|
||||
{
|
||||
if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = Normalize(remoteEndPoint).ToString();
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
playerIdsByPeer[key] = playerId;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetKnownPlayerId(IPEndPoint remoteEndPoint, out string playerId)
|
||||
{
|
||||
playerId = null;
|
||||
if (remoteEndPoint == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var key = Normalize(remoteEndPoint).ToString();
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
return playerIdsByPeer.TryGetValue(key, out playerId);
|
||||
}
|
||||
}
|
||||
|
||||
private void ForgetPlayerId(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (remoteEndPoint == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = Normalize(remoteEndPoint).ToString();
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
playerIdsByPeer.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private static IPEndPoint Normalize(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (remoteEndPoint == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||
}
|
||||
|
||||
return new IPEndPoint(remoteEndPoint.Address, remoteEndPoint.Port);
|
||||
}
|
||||
|
||||
private void PublishMetricsSessionSnapshots()
|
||||
{
|
||||
foreach (var session in ManagedSessions)
|
||||
|
||||
@@ -109,6 +109,133 @@ namespace Tests.EditMode.Network
|
||||
Assert.That(remoteCombatPresentation.IsDead, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FakeTransportRoundTrip_IdleLoggedInPlayer_ReceivesPlayerState_AndCanShootWithoutMoveInput()
|
||||
{
|
||||
var clientReliableTransport = new GameplayFlowFakeTransport();
|
||||
var clientSyncTransport = new GameplayFlowFakeTransport();
|
||||
var clientRuntime = new SharedNetworkRuntime(
|
||||
clientReliableTransport,
|
||||
new MainThreadNetworkDispatcher(),
|
||||
syncTransport: clientSyncTransport);
|
||||
var clientHarness = new ClientGameplayTestHarness("player-a");
|
||||
clientHarness.Register(clientRuntime.MessageManager);
|
||||
|
||||
var serverTransports = new Dictionary<int, GameplayFlowFakeTransport>();
|
||||
var configuration = new ServerRuntimeConfiguration(9000)
|
||||
{
|
||||
SyncPort = 9001,
|
||||
Dispatcher = new MainThreadNetworkDispatcher(),
|
||||
TransportFactory = port => CreateTransport(serverTransports, port),
|
||||
AuthoritativeMovement = new ServerAuthoritativeMovementConfiguration
|
||||
{
|
||||
MoveSpeed = 10f,
|
||||
BroadcastInterval = TimeSpan.FromMilliseconds(50),
|
||||
DefaultHp = 100
|
||||
},
|
||||
AuthoritativeCombat = new ServerAuthoritativeCombatConfiguration
|
||||
{
|
||||
DamagePerShot = 30
|
||||
}
|
||||
};
|
||||
|
||||
clientRuntime.StartAsync().GetAwaiter().GetResult();
|
||||
using var serverRuntime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||
|
||||
serverTransports[9000].EmitReceive(
|
||||
GameplayFlowTestSupport.BuildEnvelope(
|
||||
MessageType.LoginRequest,
|
||||
new LoginRequest
|
||||
{
|
||||
PlayerId = "player-a",
|
||||
Speed = 5
|
||||
}),
|
||||
ClientPeer);
|
||||
serverTransports[9000].EmitReceive(
|
||||
GameplayFlowTestSupport.BuildEnvelope(
|
||||
MessageType.LoginRequest,
|
||||
new LoginRequest
|
||||
{
|
||||
PlayerId = "player-b",
|
||||
Speed = 5
|
||||
}),
|
||||
RemotePeer);
|
||||
|
||||
serverRuntime.Host.NotifyLoginStarted(ClientPeer);
|
||||
serverRuntime.Host.NotifyLoginSucceeded(ClientPeer);
|
||||
serverRuntime.Host.NotifyLoginStarted(RemotePeer);
|
||||
serverRuntime.Host.NotifyLoginSucceeded(RemotePeer);
|
||||
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender);
|
||||
clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(clientHarness.TryGetState("player-a", out var idleLocalState), Is.True);
|
||||
Assert.That(idleLocalState.Tick, Is.EqualTo(1));
|
||||
Assert.That(idleLocalState.Position.x, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(idleLocalState.Position.z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(idleLocalState.Hp, Is.EqualTo(100));
|
||||
|
||||
serverTransports[9001].ClearOutgoing();
|
||||
serverTransports[9000].ClearOutgoing();
|
||||
|
||||
serverTransports[9001].EmitReceive(
|
||||
GameplayFlowTestSupport.BuildEnvelope(
|
||||
MessageType.MoveInput,
|
||||
new MoveInput
|
||||
{
|
||||
PlayerId = "player-b",
|
||||
Tick = 1,
|
||||
MoveX = 1f,
|
||||
MoveY = 0f
|
||||
}),
|
||||
RemotePeer);
|
||||
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(100));
|
||||
serverTransports[9001].ClearOutgoing();
|
||||
serverTransports[9000].ClearOutgoing();
|
||||
|
||||
ClientGameplayInputFlow.SendShootInput(
|
||||
clientRuntime.MessageManager,
|
||||
"player-a",
|
||||
3,
|
||||
Vector3.right);
|
||||
|
||||
Assert.That(clientReliableTransport.SentMessages.Count, Is.EqualTo(1));
|
||||
var outboundEnvelope = Envelope.Parser.ParseFrom(clientReliableTransport.SentMessages[0]);
|
||||
var outboundShootInput = ShootInput.Parser.ParseFrom(outboundEnvelope.Payload);
|
||||
Assert.That((MessageType)outboundEnvelope.Type, Is.EqualTo(MessageType.ShootInput));
|
||||
Assert.That(outboundShootInput.TargetId, Is.EqualTo(string.Empty));
|
||||
|
||||
TransferSentMessages(clientReliableTransport, serverTransports[9000], ClientPeer);
|
||||
|
||||
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
TransferBroadcastMessages(serverTransports[9000], clientReliableTransport, ServerSender);
|
||||
clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender);
|
||||
clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var localMovementState), Is.True);
|
||||
Assert.That(localMovementState.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(localMovementState.LastAcceptedMoveTick, Is.EqualTo(0));
|
||||
Assert.That(localMovementState.PositionX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(localMovementState.PositionZ, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(serverRuntime.TryGetAuthoritativeCombatState(ClientPeer, out var localCombatState), Is.True);
|
||||
Assert.That(localCombatState.LastAcceptedShootTick, Is.EqualTo(3));
|
||||
Assert.That(serverRuntime.TryGetAuthoritativeCombatState(RemotePeer, out var remoteCombatState), Is.True);
|
||||
Assert.That(remoteCombatState.PlayerId, Is.EqualTo("player-b"));
|
||||
Assert.That(remoteCombatState.Hp, Is.EqualTo(70));
|
||||
|
||||
Assert.That(clientHarness.TryGetState("player-b", out var remoteClientState), Is.True);
|
||||
Assert.That(remoteClientState.Hp, Is.EqualTo(70));
|
||||
Assert.That(clientHarness.TryGetCombatPresentation("player-b", out var remoteCombatPresentation), Is.True);
|
||||
Assert.That(remoteCombatPresentation.LastEventType, Is.EqualTo(CombatEventType.DamageApplied));
|
||||
Assert.That(remoteCombatPresentation.LastDamage, Is.EqualTo(30));
|
||||
Assert.That(remoteCombatPresentation.IsDead, Is.False);
|
||||
}
|
||||
|
||||
private static GameplayFlowFakeTransport CreateTransport(IDictionary<int, GameplayFlowFakeTransport> serverTransports, int port)
|
||||
{
|
||||
var transport = new GameplayFlowFakeTransport();
|
||||
|
||||
@@ -138,6 +138,53 @@ namespace Tests.EditMode.Network
|
||||
Assert.That(secondBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NotifyLoginSucceeded_CreatesIdleAuthoritativeState_AndBroadcastsPlayerStateWithoutMoveInput()
|
||||
{
|
||||
var createdTransports = new Dictionary<int, FakeTransport>();
|
||||
var configuration = new ServerRuntimeConfiguration(9000)
|
||||
{
|
||||
SyncPort = 9001,
|
||||
Dispatcher = new MainThreadNetworkDispatcher(),
|
||||
TransportFactory = port => CreateTransport(createdTransports, port),
|
||||
AuthoritativeMovement = new ServerAuthoritativeMovementConfiguration
|
||||
{
|
||||
MoveSpeed = 10f,
|
||||
BroadcastInterval = TimeSpan.FromMilliseconds(50),
|
||||
DefaultHp = 100
|
||||
}
|
||||
};
|
||||
|
||||
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||
|
||||
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.LoginRequest, new LoginRequest
|
||||
{
|
||||
PlayerId = "player-a",
|
||||
Speed = 5
|
||||
}), PeerA);
|
||||
|
||||
runtime.Host.NotifyLoginStarted(PeerA);
|
||||
runtime.Host.NotifyLoginSucceeded(PeerA);
|
||||
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var state), Is.True);
|
||||
Assert.That(state.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(state.LastAcceptedMoveTick, Is.EqualTo(0));
|
||||
Assert.That(state.PositionX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(state.PositionZ, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(state.Hp, Is.EqualTo(100));
|
||||
Assert.That(createdTransports[9001].BroadcastMessages.Count, Is.EqualTo(1));
|
||||
|
||||
var broadcast = ParsePlayerState(createdTransports[9001].BroadcastMessages[0]);
|
||||
Assert.That(broadcast.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(broadcast.Tick, Is.EqualTo(1));
|
||||
Assert.That(broadcast.Position.X, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(broadcast.Position.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(broadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(broadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(broadcast.Hp, Is.EqualTo(100));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UpdateAuthoritativeMovement_UsesReliableLaneWhenSyncTransportIsUnavailable()
|
||||
{
|
||||
|
||||
@@ -54,6 +54,33 @@ namespace Tests.EditMode.Network
|
||||
Assert.That(shootInput.TargetId, Is.EqualTo(string.Empty));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientGameplayInputFlow_TryCreateShootInput_LocalFirePathKeepsTargetOptional()
|
||||
{
|
||||
var created = ClientGameplayInputFlow.TryCreateShootInput(
|
||||
"player-1",
|
||||
21,
|
||||
true,
|
||||
new Vector3(2f, 0f, 0f),
|
||||
out var shootInput);
|
||||
var ignored = ClientGameplayInputFlow.TryCreateShootInput(
|
||||
"player-1",
|
||||
22,
|
||||
false,
|
||||
Vector3.forward,
|
||||
out var ignoredShootInput);
|
||||
|
||||
Assert.That(created, Is.True);
|
||||
Assert.That(shootInput, Is.Not.Null);
|
||||
Assert.That(shootInput.PlayerId, Is.EqualTo("player-1"));
|
||||
Assert.That(shootInput.Tick, Is.EqualTo(21));
|
||||
Assert.That(shootInput.DirX, Is.EqualTo(1f));
|
||||
Assert.That(shootInput.DirY, Is.EqualTo(0f));
|
||||
Assert.That(shootInput.TargetId, Is.EqualTo(string.Empty));
|
||||
Assert.That(ignored, Is.False);
|
||||
Assert.That(ignoredShootInput, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientPredictionBuffer_AuthoritativeState_PrunesAcknowledgedMoveInputs()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user