clearup
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using NUnit.Framework;
|
||||
@@ -125,6 +126,71 @@ namespace Tests.EditMode.Network
|
||||
Assert.That(combatPresentation.IsDead, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientGameplayFlow_ControlledPlayerReconciliation_ReplacesActiveCorrectionForConsecutiveSmallSnapshots()
|
||||
{
|
||||
var gameObject = new GameObject("controlled-player");
|
||||
try
|
||||
{
|
||||
var rigidbody = gameObject.AddComponent<Rigidbody>();
|
||||
rigidbody.useGravity = false;
|
||||
var movement = gameObject.AddComponent<MovementComponent>();
|
||||
typeof(MovementComponent)
|
||||
.GetField("_rigid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.SetValue(movement, rigidbody);
|
||||
movement.Init(true, master: null, speed: 10, serverTick: 0);
|
||||
|
||||
movement.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 1, new Vector3(0.75f, 0f, 0f), acknowledgedMoveTick: 0)));
|
||||
InvokeControlledFixedUpdate(movement);
|
||||
Assert.That(rigidbody.position.x, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
|
||||
movement.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 2, new Vector3(1f, 0f, 0f), acknowledgedMoveTick: 0)));
|
||||
InvokeControlledFixedUpdate(movement);
|
||||
Assert.That(rigidbody.position.x, Is.EqualTo(1f).Within(0.0001f));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientGameplayFlow_ControlledPlayerReconciliation_EscalatesToSnapAfterFailedConvergence()
|
||||
{
|
||||
var gameObject = new GameObject("controlled-player");
|
||||
try
|
||||
{
|
||||
var rigidbody = gameObject.AddComponent<Rigidbody>();
|
||||
rigidbody.useGravity = false;
|
||||
var movement = gameObject.AddComponent<MovementComponent>();
|
||||
typeof(MovementComponent)
|
||||
.GetField("_rigid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.SetValue(movement, rigidbody);
|
||||
movement.Init(true, master: null, speed: 10, serverTick: 0);
|
||||
|
||||
movement.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 1, new Vector3(0.75f, 0f, 0f), acknowledgedMoveTick: 0)));
|
||||
InvokeControlledFixedUpdate(movement);
|
||||
Assert.That(rigidbody.position.x, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
|
||||
movement.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 2, new Vector3(1.25f, 0f, 0f), acknowledgedMoveTick: 0)));
|
||||
InvokeControlledFixedUpdate(movement);
|
||||
Assert.That(rigidbody.position.x, Is.EqualTo(1f).Within(0.0001f));
|
||||
|
||||
movement.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 3, new Vector3(1.75f, 0f, 0f), acknowledgedMoveTick: 0)));
|
||||
InvokeControlledFixedUpdate(movement);
|
||||
Assert.That(rigidbody.position.x, Is.EqualTo(1.75f).Within(0.0001f));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientGameplayHarness_RemotePlayerStateFlow_RejectsStaleSnapshots_AndUsesInterpolationOrLatestClamp()
|
||||
{
|
||||
@@ -157,5 +223,11 @@ namespace Tests.EditMode.Network
|
||||
Assert.That(clamped.LatestSnapshot.Tick, Is.EqualTo(11));
|
||||
Assert.That(clamped.Position, Is.EqualTo(new Vector3(10f, 0f, 0f)));
|
||||
}
|
||||
private static void InvokeControlledFixedUpdate(MovementComponent movement)
|
||||
{
|
||||
typeof(MovementComponent)
|
||||
.GetMethod("FixedUpdate", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.Invoke(movement, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ namespace Tests.EditMode.Network
|
||||
TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender);
|
||||
clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(serverRuntime.AuthoritativeMovementCadence, Is.EqualTo(TimeSpan.FromMilliseconds(50)));
|
||||
Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var localServerState), Is.True);
|
||||
Assert.That(localServerState.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(localServerState.PositionX, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
@@ -105,6 +106,7 @@ namespace Tests.EditMode.Network
|
||||
|
||||
Assert.That(clientHarness.TryGetState("player-a", out var localClientState), Is.True);
|
||||
Assert.That(localClientState.Tick, Is.EqualTo(1));
|
||||
Assert.That(localClientState.AcknowledgedMoveTick, Is.EqualTo(1));
|
||||
Assert.That(localClientState.Position.x, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
Assert.That(clientHarness.TryGetState("player-b", out var remoteClientState), Is.True);
|
||||
Assert.That(remoteClientState.Hp, Is.EqualTo(70));
|
||||
@@ -177,6 +179,7 @@ namespace Tests.EditMode.Network
|
||||
|
||||
Assert.That(clientHarness.TryGetState("player-a", out var idleLocalState), Is.True);
|
||||
Assert.That(idleLocalState.Tick, Is.EqualTo(1));
|
||||
Assert.That(idleLocalState.AcknowledgedMoveTick, Is.EqualTo(0));
|
||||
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));
|
||||
|
||||
@@ -213,7 +213,7 @@ namespace Tests.EditMode.Network
|
||||
}.ToByteArray();
|
||||
}
|
||||
|
||||
public static PlayerState CreatePlayerState(string playerId, long tick, Vector3 position, int hp = 100, float rotation = 0f, Vector3? velocity = null)
|
||||
public static PlayerState CreatePlayerState(string playerId, long tick, Vector3 position, int hp = 100, float rotation = 0f, Vector3? velocity = null, long acknowledgedMoveTick = 0)
|
||||
{
|
||||
var resolvedVelocity = velocity ?? Vector3.zero;
|
||||
return new PlayerState
|
||||
@@ -223,7 +223,8 @@ namespace Tests.EditMode.Network
|
||||
Position = new global::Network.Defines.Vector3 { X = position.x, Y = position.y, Z = position.z },
|
||||
Velocity = new global::Network.Defines.Vector3 { X = resolvedVelocity.x, Y = resolvedVelocity.y, Z = resolvedVelocity.z },
|
||||
Rotation = rotation,
|
||||
Hp = hp
|
||||
Hp = hp,
|
||||
AcknowledgedMoveTick = acknowledgedMoveTick
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,55 @@ namespace Tests.EditMode.Network
|
||||
private static readonly IPEndPoint PeerA = new(IPAddress.Loopback, 9101);
|
||||
private static readonly IPEndPoint PeerB = new(IPAddress.Loopback, 9102);
|
||||
|
||||
[Test]
|
||||
public void UpdateAuthoritativeMovement_UsesConfiguredSimulationCadence_AndExposesItOnRuntime()
|
||||
{
|
||||
var createdTransports = new Dictionary<int, FakeTransport>();
|
||||
var configuration = new ServerRuntimeConfiguration(9000)
|
||||
{
|
||||
Dispatcher = new MainThreadNetworkDispatcher(),
|
||||
TransportFactory = port => CreateTransport(createdTransports, port),
|
||||
AuthoritativeMovement = new ServerAuthoritativeMovementConfiguration
|
||||
{
|
||||
MoveSpeed = 4f,
|
||||
SimulationInterval = TimeSpan.FromMilliseconds(50),
|
||||
BroadcastInterval = TimeSpan.FromMilliseconds(100)
|
||||
}
|
||||
};
|
||||
|
||||
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||
|
||||
runtime.Host.NotifyLoginStarted(PeerA);
|
||||
runtime.Host.NotifyLoginSucceeded(PeerA, "player-a");
|
||||
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
||||
{
|
||||
PlayerId = "player-a",
|
||||
Tick = 1,
|
||||
TurnInput = 0f,
|
||||
ThrottleInput = 1f
|
||||
}), PeerA);
|
||||
|
||||
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(49));
|
||||
|
||||
Assert.That(runtime.AuthoritativeMovementCadence, Is.EqualTo(TimeSpan.FromMilliseconds(50)));
|
||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var stateBeforeCadence), Is.True);
|
||||
Assert.That(stateBeforeCadence.PositionX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(0));
|
||||
|
||||
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(1));
|
||||
|
||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var stateAfterFirstStep), Is.True);
|
||||
Assert.That(stateAfterFirstStep.PositionX, Is.EqualTo(0.2f).Within(0.0001f));
|
||||
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(0));
|
||||
|
||||
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var stateAfterSecondStep), Is.True);
|
||||
Assert.That(stateAfterSecondStep.PositionX, Is.EqualTo(0.4f).Within(0.0001f));
|
||||
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UpdateAuthoritativeMovement_AcceptsLatestTickPerPeer_AndKeepsStaleFilteringIndependent()
|
||||
{
|
||||
@@ -118,6 +167,7 @@ namespace Tests.EditMode.Network
|
||||
var firstBroadcast = ParsePlayerState(createdTransports[9001].BroadcastMessages[0]);
|
||||
Assert.That(firstBroadcast.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(firstBroadcast.Tick, Is.EqualTo(1));
|
||||
Assert.That(firstBroadcast.AcknowledgedMoveTick, Is.EqualTo(1));
|
||||
Assert.That(firstBroadcast.Position.X, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(firstBroadcast.Velocity.X, Is.EqualTo(10f).Within(0.0001f));
|
||||
Assert.That(firstBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
@@ -141,6 +191,7 @@ namespace Tests.EditMode.Network
|
||||
|
||||
var secondBroadcast = ParsePlayerState(createdTransports[9001].BroadcastMessages[1]);
|
||||
Assert.That(secondBroadcast.Tick, Is.EqualTo(2));
|
||||
Assert.That(secondBroadcast.AcknowledgedMoveTick, Is.EqualTo(2));
|
||||
Assert.That(secondBroadcast.Position.X, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(secondBroadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(secondBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
@@ -186,6 +237,7 @@ namespace Tests.EditMode.Network
|
||||
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.AcknowledgedMoveTick, Is.EqualTo(0));
|
||||
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));
|
||||
@@ -228,6 +280,7 @@ namespace Tests.EditMode.Network
|
||||
|
||||
var broadcast = ParsePlayerState(createdTransports[9000].BroadcastMessages[0]);
|
||||
Assert.That(broadcast.Tick, Is.EqualTo(1));
|
||||
Assert.That(broadcast.AcknowledgedMoveTick, Is.EqualTo(5));
|
||||
Assert.That(broadcast.Position.X, Is.EqualTo(-0.3f).Within(0.0001f));
|
||||
Assert.That(broadcast.Position.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(broadcast.Velocity.X, Is.EqualTo(-6f).Within(0.0001f));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using Google.Protobuf;
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
@@ -90,11 +91,12 @@ namespace Tests.EditMode.Network
|
||||
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 12, ThrottleInput = 1f });
|
||||
|
||||
var accepted = buffer.TryApplyAuthoritativeState(
|
||||
new PlayerState { PlayerId = "player-1", Tick = 11 },
|
||||
new PlayerState { PlayerId = "player-1", Tick = 11, AcknowledgedMoveTick = 11 },
|
||||
out var replayInputs);
|
||||
|
||||
Assert.That(accepted, Is.True);
|
||||
Assert.That(buffer.LastAuthoritativeTick, Is.EqualTo(11));
|
||||
Assert.That(buffer.LastAcknowledgedMoveTick, Is.EqualTo(11));
|
||||
Assert.That(replayInputs.Count, Is.EqualTo(1));
|
||||
Assert.That(replayInputs[0].Input.Tick, Is.EqualTo(12));
|
||||
Assert.That(replayInputs[0].SimulatedDurationSeconds, Is.EqualTo(0f));
|
||||
@@ -106,15 +108,121 @@ namespace Tests.EditMode.Network
|
||||
{
|
||||
var buffer = new ClientPredictionBuffer();
|
||||
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, ThrottleInput = 1f });
|
||||
buffer.TryApplyAuthoritativeState(new PlayerState { PlayerId = "player-1", Tick = 10 }, out _);
|
||||
buffer.TryApplyAuthoritativeState(new PlayerState { PlayerId = "player-1", Tick = 10, AcknowledgedMoveTick = 10 }, out _);
|
||||
|
||||
var accepted = buffer.TryApplyAuthoritativeState(
|
||||
new PlayerState { PlayerId = "player-1", Tick = 9 },
|
||||
new PlayerState { PlayerId = "player-1", Tick = 9, AcknowledgedMoveTick = 9 },
|
||||
out var replayInputs);
|
||||
|
||||
Assert.That(accepted, Is.False);
|
||||
Assert.That(replayInputs, Is.Empty);
|
||||
Assert.That(buffer.LastAuthoritativeTick, Is.EqualTo(10));
|
||||
Assert.That(buffer.LastAcknowledgedMoveTick, Is.EqualTo(10));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientMovementBootstrap_LoginResponse_UsesServerConfirmedMovementParameters()
|
||||
{
|
||||
var bootstrap = ClientMovementBootstrap.FromLoginResponse(new LoginResponse
|
||||
{
|
||||
Speed = 12,
|
||||
ServerTick = 34
|
||||
});
|
||||
|
||||
Assert.That(bootstrap.AuthoritativeMoveSpeed, Is.EqualTo(12));
|
||||
Assert.That(bootstrap.ServerTick, Is.EqualTo(34));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ControlledPlayerCorrection_SmallError_UsesBoundedCorrection()
|
||||
{
|
||||
var result = ControlledPlayerCorrection.Resolve(
|
||||
Vector3.zero,
|
||||
Quaternion.identity,
|
||||
new Vector3(0.75f, 0f, 0f),
|
||||
Quaternion.Euler(0f, 15f, 0f),
|
||||
new ControlledPlayerCorrectionSettings(0.05f, 10f, 180f));
|
||||
|
||||
Assert.That(result.UsedHardSnap, Is.False);
|
||||
Assert.That(result.Position, Is.EqualTo(new Vector3(0.5f, 0f, 0f)));
|
||||
Assert.That(result.Rotation.eulerAngles.y, Is.EqualTo(9f).Within(0.01f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ControlledPlayerCorrection_LargeError_UsesHardSnap()
|
||||
{
|
||||
var targetRotation = Quaternion.Euler(0f, 40f, 0f);
|
||||
var result = ControlledPlayerCorrection.Resolve(
|
||||
Vector3.zero,
|
||||
Quaternion.identity,
|
||||
new Vector3(2f, 0f, 0f),
|
||||
targetRotation,
|
||||
new ControlledPlayerCorrectionSettings(0.05f, 10f, 180f));
|
||||
|
||||
Assert.That(result.UsedHardSnap, Is.True);
|
||||
Assert.That(result.Position, Is.EqualTo(new Vector3(2f, 0f, 0f)));
|
||||
Assert.That(result.Rotation.eulerAngles.y, Is.EqualTo(targetRotation.eulerAngles.y).Within(0.01f));
|
||||
Assert.That(result.NextState.IsActive, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ControlledPlayerCorrection_RepeatedSmallCorrections_UpdateActiveCorrectionState()
|
||||
{
|
||||
var settings = new ControlledPlayerCorrectionSettings(0.05f, 10f, 180f);
|
||||
var first = ControlledPlayerCorrection.Resolve(
|
||||
Vector3.zero,
|
||||
Quaternion.identity,
|
||||
new Vector3(0.75f, 0f, 0f),
|
||||
Quaternion.identity,
|
||||
settings);
|
||||
var second = ControlledPlayerCorrection.Resolve(
|
||||
first.Position,
|
||||
first.Rotation,
|
||||
new Vector3(1f, 0f, 0f),
|
||||
Quaternion.identity,
|
||||
settings,
|
||||
first.NextState);
|
||||
|
||||
Assert.That(first.UsedHardSnap, Is.False);
|
||||
Assert.That(first.NextState.IsActive, Is.True);
|
||||
Assert.That(first.NextState.RemainingStepBudget, Is.EqualTo(2));
|
||||
Assert.That(second.UsedHardSnap, Is.False);
|
||||
Assert.That(second.Position.x, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(second.NextState.IsActive, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ControlledPlayerCorrection_RepeatedNonConvergentSmallCorrections_EventuallyUseHardSnap()
|
||||
{
|
||||
var settings = new ControlledPlayerCorrectionSettings(0.05f, 10f, 180f);
|
||||
var first = ControlledPlayerCorrection.Resolve(
|
||||
Vector3.zero,
|
||||
Quaternion.identity,
|
||||
new Vector3(0.75f, 0f, 0f),
|
||||
Quaternion.identity,
|
||||
settings);
|
||||
var second = ControlledPlayerCorrection.Resolve(
|
||||
first.Position,
|
||||
first.Rotation,
|
||||
new Vector3(1.25f, 0f, 0f),
|
||||
Quaternion.identity,
|
||||
settings,
|
||||
first.NextState);
|
||||
var third = ControlledPlayerCorrection.Resolve(
|
||||
second.Position,
|
||||
second.Rotation,
|
||||
new Vector3(1.75f, 0f, 0f),
|
||||
Quaternion.identity,
|
||||
settings,
|
||||
second.NextState);
|
||||
|
||||
Assert.That(first.UsedHardSnap, Is.False);
|
||||
Assert.That(second.UsedHardSnap, Is.False);
|
||||
Assert.That(second.NextState.IsActive, Is.True);
|
||||
Assert.That(second.NextState.RemainingStepBudget, Is.EqualTo(1));
|
||||
Assert.That(third.UsedHardSnap, Is.True);
|
||||
Assert.That(third.Position.x, Is.EqualTo(1.75f).Within(0.0001f));
|
||||
Assert.That(third.NextState.IsActive, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -129,7 +237,8 @@ namespace Tests.EditMode.Network
|
||||
Position = new global::Network.Defines.Vector3 { X = 5f, Y = 0f, Z = -3f },
|
||||
Velocity = new global::Network.Defines.Vector3 { X = 1.5f, Y = 0f, Z = 0.25f },
|
||||
Rotation = 90f,
|
||||
Hp = 73
|
||||
Hp = 73,
|
||||
AcknowledgedMoveTick = 9
|
||||
},
|
||||
out var snapshot);
|
||||
|
||||
@@ -137,6 +246,7 @@ namespace Tests.EditMode.Network
|
||||
Assert.That(owner.Current, Is.SameAs(snapshot));
|
||||
Assert.That(snapshot.PlayerId, Is.EqualTo("player-1"));
|
||||
Assert.That(snapshot.Tick, Is.EqualTo(14));
|
||||
Assert.That(snapshot.AcknowledgedMoveTick, Is.EqualTo(9));
|
||||
Assert.That(snapshot.Position, Is.EqualTo(new Vector3(5f, 0f, -3f)));
|
||||
Assert.That(snapshot.Velocity, Is.EqualTo(new Vector3(1.5f, 0f, 0.25f)));
|
||||
Assert.That(snapshot.Rotation, Is.EqualTo(90f));
|
||||
@@ -360,7 +470,7 @@ namespace Tests.EditMode.Network
|
||||
Assert.That(sample.HasValue, Is.True);
|
||||
Assert.That(sample.UsedInterpolation, Is.False);
|
||||
Assert.That(sample.Position, Is.EqualTo(new Vector3(2f, 0f, -1f)));
|
||||
Assert.That(sample.Rotation.eulerAngles.y, Is.EqualTo(15f).Within(0.01f));
|
||||
Assert.That(sample.Rotation.eulerAngles.y, Is.EqualTo(75f).Within(0.01f));
|
||||
Assert.That(sample.LatestSnapshot.Tick, Is.EqualTo(12));
|
||||
}
|
||||
|
||||
@@ -392,6 +502,172 @@ namespace Tests.EditMode.Network
|
||||
Assert.That(runtime.ClockSync.CurrentServerTick, Is.EqualTo(88));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReplayPendingInputs_StepByStepMatchesAccumulated_ForZeroTurnInput()
|
||||
{
|
||||
// Arrange: set up MovementComponent with initial state.
|
||||
var gameObject = new GameObject("replay-test");
|
||||
try
|
||||
{
|
||||
var rigidbody = gameObject.AddComponent<Rigidbody>();
|
||||
rigidbody.useGravity = false;
|
||||
var movement = gameObject.AddComponent<MovementComponent>();
|
||||
typeof(MovementComponent)
|
||||
.GetField("_rigid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.SetValue(movement, rigidbody);
|
||||
movement.Init(true, master: null, speed: 10, serverTick: 0);
|
||||
|
||||
ResetMovementState(rigidbody, Vector3.zero, Quaternion.identity);
|
||||
|
||||
var turnInput = 0f;
|
||||
var throttleInput = 1f;
|
||||
var stepDuration = 0.05f;
|
||||
var totalDuration = stepDuration * 3; // 0.15s
|
||||
|
||||
// Act — step-by-step path (live prediction shape).
|
||||
ApplyTankMovementStepByStep(movement, turnInput, throttleInput, stepDuration, steps: 3);
|
||||
var stepByStepPosition = rigidbody.position;
|
||||
var stepByStepRotation = rigidbody.rotation;
|
||||
|
||||
// Reset to initial state.
|
||||
ResetMovementState(rigidbody, Vector3.zero, Quaternion.identity);
|
||||
|
||||
// Act — accumulated replay shape.
|
||||
var accumulatedReplayInputs = new List<PredictedMoveStep>
|
||||
{
|
||||
new PredictedMoveStep(
|
||||
new MoveInput { PlayerId = "player-1", Tick = 1, TurnInput = turnInput, ThrottleInput = throttleInput },
|
||||
totalDuration)
|
||||
};
|
||||
InvokeReplayPendingInputs(movement, accumulatedReplayInputs);
|
||||
var accumulatedPosition = rigidbody.position;
|
||||
var accumulatedRotation = rigidbody.rotation;
|
||||
|
||||
// Assert: for straight movement (turn=0), both paths should be identical.
|
||||
Assert.That(Vector3.Distance(accumulatedPosition, stepByStepPosition), Is.LessThan(0.0001f),
|
||||
"Accumulated replay produced a different position than step-by-step for straight movement.");
|
||||
Assert.That(Quaternion.Angle(accumulatedRotation, stepByStepRotation), Is.LessThan(0.01f),
|
||||
"Accumulated replay produced a different rotation than step-by-step for straight movement.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReplayPendingInputs_StepByStepDiffersFromAccumulated_ForNonZeroTurnInput()
|
||||
{
|
||||
// Arrange: use many small steps and a large turn input to make non-linearity visible.
|
||||
var gameObject = new GameObject("replay-test");
|
||||
try
|
||||
{
|
||||
var rigidbody = gameObject.AddComponent<Rigidbody>();
|
||||
rigidbody.useGravity = false;
|
||||
var movement = gameObject.AddComponent<MovementComponent>();
|
||||
typeof(MovementComponent)
|
||||
.GetField("_rigid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.SetValue(movement, rigidbody);
|
||||
movement.Init(true, master: null, speed: 10, serverTick: 0);
|
||||
|
||||
ResetMovementState(rigidbody, Vector3.zero, Quaternion.identity);
|
||||
|
||||
// Use 20 substeps of 0.05s (1 second total) with full turn.
|
||||
// This amplifies the non-linearity so the one-shot and step-by-step diverge.
|
||||
var turnInput = 1f;
|
||||
var throttleInput = 1f;
|
||||
var stepDuration = 0.05f;
|
||||
var steps = 20;
|
||||
var totalDuration = stepDuration * steps;
|
||||
|
||||
// Act — step-by-step (correct approach).
|
||||
ApplyTankMovementStepByStep(movement, turnInput, throttleInput, stepDuration, steps);
|
||||
var stepByStepPosition = rigidbody.position;
|
||||
|
||||
// Reset.
|
||||
ResetMovementState(rigidbody, Vector3.zero, Quaternion.identity);
|
||||
|
||||
// Act — ONE big step simulating the old buggy accumulated behavior.
|
||||
ApplyTankMovementStepByStep(movement, turnInput, throttleInput, totalDuration, steps: 1);
|
||||
var oneShotPosition = rigidbody.position;
|
||||
|
||||
// Assert: for non-zero turn with many steps, the old one-shot and correct step-by-step MUST differ.
|
||||
Assert.That(Vector3.Distance(oneShotPosition, stepByStepPosition), Is.GreaterThan(0.001f),
|
||||
"One-shot accumulated and step-by-step produced the same result for turn input — " +
|
||||
"the non-linearity should cause a visible divergence with many steps.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReplayPendingInputs_NonMultipleOfCadence_HandlesRemainingDuration()
|
||||
{
|
||||
// Arrange: simulate 0.12s — not a multiple of 50ms.
|
||||
var gameObject = new GameObject("replay-test");
|
||||
try
|
||||
{
|
||||
var rigidbody = gameObject.AddComponent<Rigidbody>();
|
||||
rigidbody.useGravity = false;
|
||||
var movement = gameObject.AddComponent<MovementComponent>();
|
||||
typeof(MovementComponent)
|
||||
.GetField("_rigid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.SetValue(movement, rigidbody);
|
||||
movement.Init(true, master: null, speed: 10, serverTick: 0);
|
||||
|
||||
ResetMovementState(rigidbody, Vector3.zero, Quaternion.identity);
|
||||
|
||||
var turnInput = 0f;
|
||||
var throttleInput = 1f;
|
||||
var totalDuration = 0.12f; // 0.05 + 0.05 + 0.02
|
||||
|
||||
var replayInputs = new List<PredictedMoveStep>
|
||||
{
|
||||
new PredictedMoveStep(
|
||||
new MoveInput { PlayerId = "player-1", Tick = 1, TurnInput = turnInput, ThrottleInput = throttleInput },
|
||||
totalDuration)
|
||||
};
|
||||
InvokeReplayPendingInputs(movement, replayInputs);
|
||||
var finalPosition = rigidbody.position;
|
||||
|
||||
// Expected: 0.12s at speed=10 → 1.2 units forward.
|
||||
var expectedPosition = new Vector3(0f, 0f, 1.2f);
|
||||
Assert.That(finalPosition.z, Is.EqualTo(expectedPosition.z).Within(0.0001f),
|
||||
"Non-multiple of cadence (0.12s) had remaining duration lost or misapplied.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyTankMovementStepByStep(MovementComponent movement, float turnInput, float throttleInput, float stepDuration, int steps)
|
||||
{
|
||||
var method = typeof(MovementComponent)
|
||||
.GetMethod("ApplyTankMovement", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
for (var i = 0; i < steps; i++)
|
||||
{
|
||||
method.Invoke(movement, new object[] { turnInput, throttleInput, stepDuration });
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetMovementState(Rigidbody rigidbody, Vector3 position, Quaternion rotation)
|
||||
{
|
||||
rigidbody.position = position;
|
||||
rigidbody.rotation = rotation;
|
||||
rigidbody.velocity = Vector3.zero;
|
||||
rigidbody.angularVelocity = Vector3.zero;
|
||||
}
|
||||
|
||||
private static void InvokeReplayPendingInputs(MovementComponent movement, IReadOnlyList<PredictedMoveStep> inputs)
|
||||
{
|
||||
typeof(MovementComponent)
|
||||
.GetMethod("ReplayPendingInputs", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.Invoke(movement, new object[] { inputs });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ServerNetworkHost_RejectsStaleMoveInputPerPeerWithoutCrossPeerInterference()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user