单帧测试进行中

This commit is contained in:
SepComet
2026-04-07 16:06:46 +08:00
parent 90f1832397
commit 1e90de11ce
23 changed files with 2063 additions and 205 deletions
@@ -99,7 +99,7 @@ namespace Tests.EditMode.Network
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));
Assert.That(localServerState.PositionZ, Is.EqualTo(0.5f).Within(0.0001f));
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));
@@ -107,7 +107,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(localClientState.Position.z, 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));
Assert.That(clientHarness.TryGetCombatPresentation("player-b", out var remoteCombatPresentation), Is.True);
@@ -228,7 +228,6 @@ namespace Tests.EditMode.Network
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));
@@ -0,0 +1,277 @@
using System;
using Network.Defines;
using NUnit.Framework;
using UnityEngine;
namespace Tests.EditMode.Network
{
/// <summary>
/// 验证客户端 ApplyTankMovement 和服务端 IntegrateState
/// 在相同输入下产生相同的移动结果。
///
/// 使用纯函数对比,不依赖任何状态对象。
/// </summary>
public class MovementAlgorithmConsistencyTests
{
private const float MoveSpeed = 4f;
private const float TurnSpeed = 180f;
private const float DeltaTime = 0.05f; // 50ms
[Test]
public void ServerForward_ZeroRotation_MovesInPositiveZ()
{
// Arrange: 服务端 rotation=0,throttle=1
float rotation = 0f;
float throttleInput = 1f;
// Act: 服务端 forward 计算(新公式)
var rotationRadians = rotation * (MathF.PI / 180f);
var forwardX = MathF.Sin(rotationRadians); // 新公式
var forwardZ = MathF.Cos(rotationRadians); // 新公式
var velocityX = forwardX * (throttleInput * MoveSpeed);
var velocityZ = forwardZ * (throttleInput * MoveSpeed);
// Assert: rotation=0° → forward=(0,0,1) = +Z
Assert.That(forwardX, Is.EqualTo(0f).Within(0.0001f));
Assert.That(forwardZ, Is.EqualTo(1f).Within(0.0001f));
Assert.That(velocityX, Is.EqualTo(0f).Within(0.0001f));
Assert.That(velocityZ, Is.EqualTo(4f).Within(0.0001f));
}
[Test]
public void ServerForward_90DegreeRotation_MovesInPositiveX()
{
// Arrange: 服务端 rotation=90°,throttle=1
float rotation = 90f;
float throttleInput = 1f;
// Act: 服务端 forward 计算(新公式)
var rotationRadians = rotation * (MathF.PI / 180f);
var forwardX = MathF.Sin(rotationRadians); // 新公式
var forwardZ = MathF.Cos(rotationRadians); // 新公式
var velocityX = forwardX * (throttleInput * MoveSpeed);
var velocityZ = forwardZ * (throttleInput * MoveSpeed);
// Assert: rotation=90° → forward=(1,0,0) = +X
Assert.That(forwardX, Is.EqualTo(1f).Within(0.0001f));
Assert.That(forwardZ, Is.EqualTo(0f).Within(0.0001f));
Assert.That(velocityX, Is.EqualTo(4f).Within(0.0001f));
Assert.That(velocityZ, Is.EqualTo(0f).Within(0.0001f));
}
[Test]
public void ClientForward_UnityYawZero_MovesInPositiveZ()
{
// Arrange: Unity Yaw = 0°,客户端转换后 heading = 90°
float unityYaw = 0f;
float throttleInput = 1f;
// Act: 客户端 heading 计算
var heading = NormalizeDegrees(UnityYawToHeading(unityYaw));
// 客户端 ResolveHeadingForward: forward = (cos, 0, sin)
var rotationRadians = heading * Mathf.Deg2Rad;
var forwardX = Mathf.Cos(rotationRadians);
var forwardZ = Mathf.Sin(rotationRadians);
var velocityX = forwardX * throttleInput * MoveSpeed;
var velocityZ = forwardZ * throttleInput * MoveSpeed;
// Assert: Unity Yaw=0 → heading=90° → forward=(cos(90°), sin(90°))=(0,1) = +Z
Assert.That(heading, Is.EqualTo(90f).Within(0.0001f));
Assert.That(forwardX, Is.EqualTo(0f).Within(0.0001f));
Assert.That(forwardZ, Is.EqualTo(1f).Within(0.0001f));
Assert.That(velocityX, Is.EqualTo(0f).Within(0.0001f));
Assert.That(velocityZ, Is.EqualTo(4f).Within(0.0001f));
}
[Test]
public void ClientServer_IdenticalInputs_ProduceIdenticalOutput()
{
// 这个测试验证:相同的输入在客户端和服务端产生相同的最终位置
//
// 场景:初始位置 (0,0,0),初始 heading=90°(Unity Yaw=0),
// 向前移动 4 个 50ms 步长
// ===== 共享参数 =====
float moveSpeed = MoveSpeed;
float turnSpeed = TurnSpeed;
float dt = DeltaTime;
// ===== 服务端计算 =====
float serverPosX = 0f, serverPosZ = 0f;
float serverRotation = 0f; // 服务端 rotation 直接是 heading
float serverThrottle = 1f;
for (int i = 0; i < 4; i++)
{
// 速度计算(服务端新 forward 公式:forwardX = sin, forwardZ = cos)
var rotRad = serverRotation * (MathF.PI / 180f);
var fwdX = MathF.Sin(rotRad);
var fwdZ = MathF.Cos(rotRad);
var velX = fwdX * (serverThrottle * moveSpeed);
var velZ = fwdZ * (serverThrottle * moveSpeed);
// 位置积分
serverPosX += velX * dt;
serverPosZ += velZ * dt;
}
// ===== 客户端计算 =====
float clientPosX = 0f, clientPosZ = 0f;
float clientUnityYaw = 0f; // Unity Yaw = 0 → heading = 90°
float clientThrottle = 1f;
for (int i = 0; i < 4; i++)
{
// 旋转(客户端需要转换)
var heading = NormalizeDegrees(UnityYawToHeading(clientUnityYaw) + (0f * turnSpeed * dt));
clientUnityYaw = HeadingToUnityYaw(heading);
// 客户端 ResolveHeadingForward: forward = (cos, 0, sin)
var headingRad = heading * Mathf.Deg2Rad;
var fwdX = Mathf.Cos(headingRad);
var fwdZ = Mathf.Sin(headingRad);
var velX = fwdX * (clientThrottle * moveSpeed);
var velZ = fwdZ * (clientThrottle * moveSpeed);
// 位置积分
clientPosX += velX * dt;
clientPosZ += velZ * dt;
}
// ===== 对比 =====
// 服务端期望:rotation=0° → forward=(0,0,1) → velocity=(0,0,4) → 每步 0.2
// 4步后:pos = (0, 0, 0.8)
Assert.That(serverPosX, Is.EqualTo(0f).Within(0.0001f));
Assert.That(serverPosZ, Is.EqualTo(0.8f).Within(0.0001f));
// 客户端应与服务端一致
Assert.That(clientPosX, Is.EqualTo(serverPosX).Within(0.0001f),
$"Client X ({clientPosX}) should match Server X ({serverPosX})");
Assert.That(clientPosZ, Is.EqualTo(serverPosZ).Within(0.0001f),
$"Client Z ({clientPosZ}) should match Server Z ({serverPosZ})");
}
[Test]
public void ClientServer_TurnRight90Degrees_ThenMove_ProduceIdenticalOutput()
{
// 场景:初始向前走,然后右转90°,再走
// 验证转向后的方向一致
float moveSpeed = MoveSpeed;
float turnSpeed = TurnSpeed;
float dt = DeltaTime;
// ===== 服务端计算 =====
float serverPosX = 0f, serverPosZ = 0f;
float serverRotation = 0f; // heading=0°,向前走
float serverThrottle = 1f;
float serverTurnInput = 1f; // 右转
// 先走2步
for (int i = 0; i < 2; i++)
{
var rotRad = serverRotation * (MathF.PI / 180f);
var fwdX = MathF.Sin(rotRad);
var fwdZ = MathF.Cos(rotRad);
var velX = fwdX * serverThrottle * moveSpeed;
var velZ = fwdZ * serverThrottle * moveSpeed;
serverPosX += velX * dt;
serverPosZ += velZ * dt;
}
// 右转1步(turnInput=1,转 180*0.05=9°)
serverRotation = NormalizeDegreesServer(serverRotation + (serverTurnInput * turnSpeed * dt));
// 再走2步
for (int i = 0; i < 2; i++)
{
var rotRad = serverRotation * (MathF.PI / 180f);
var fwdX = MathF.Sin(rotRad);
var fwdZ = MathF.Cos(rotRad);
var velX = fwdX * serverThrottle * moveSpeed;
var velZ = fwdZ * serverThrottle * moveSpeed;
serverPosX += velX * dt;
serverPosZ += velZ * dt;
}
// ===== 客户端计算 =====
float clientPosX = 0f, clientPosZ = 0f;
float clientUnityYaw = 0f; // 初始朝前
float clientThrottle = 1f;
float clientTurnInput = -1f; // Unity 中右转 = -input.x
// 先走2步
for (int i = 0; i < 2; i++)
{
var heading = NormalizeDegrees(UnityYawToHeading(clientUnityYaw));
var headingRad = heading * Mathf.Deg2Rad;
var fwdX = Mathf.Cos(headingRad);
var fwdZ = Mathf.Sin(headingRad);
var velX = fwdX * clientThrottle * moveSpeed;
var velZ = fwdZ * clientThrottle * moveSpeed;
clientPosX += velX * dt;
clientPosZ += velZ * dt;
}
// 右转1步
var newHeading = NormalizeDegrees(UnityYawToHeading(clientUnityYaw) + (clientTurnInput * turnSpeed * dt));
clientUnityYaw = HeadingToUnityYaw(newHeading);
// 再走2步
for (int i = 0; i < 2; i++)
{
var heading = NormalizeDegrees(UnityYawToHeading(clientUnityYaw));
var headingRad = heading * Mathf.Deg2Rad;
var fwdX = Mathf.Cos(headingRad);
var fwdZ = Mathf.Sin(headingRad);
var velX = fwdX * clientThrottle * moveSpeed;
var velZ = fwdZ * clientThrottle * moveSpeed;
clientPosX += velX * dt;
clientPosZ += velZ * dt;
}
// ===== 对比 =====
Assert.That(clientPosX, Is.EqualTo(serverPosX).Within(0.0001f),
$"Client X ({clientPosX}) should match Server X ({serverPosX})");
Assert.That(clientPosZ, Is.EqualTo(serverPosZ).Within(0.0001f),
$"Client Z ({clientPosZ}) should match Server Z ({serverPosZ})");
}
// ===== 辅助方法(从 MovementComponent 复制) =====
private static float UnityYawToHeading(float unityYawDegrees)
{
return NormalizeDegrees(90f - unityYawDegrees);
}
private static float HeadingToUnityYaw(float headingDegrees)
{
return NormalizeDegrees(90f - headingDegrees);
}
private static float NormalizeDegrees(float degrees)
{
var normalized = degrees % 360f;
if (normalized < 0f)
{
normalized += 360f;
}
return normalized;
}
private static float NormalizeDegreesServer(float degrees)
{
var normalized = degrees % 360f;
if (normalized <= -180f)
{
normalized += 360f;
}
else if (normalized > 180f)
{
normalized -= 360f;
}
return normalized;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 351f8ed6c1e2cf047bb173a387d41942
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -55,13 +55,13 @@ namespace Tests.EditMode.Network
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(stateAfterFirstStep.PositionZ, 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(stateAfterSecondStep.PositionZ, Is.EqualTo(0.4f).Within(0.0001f));
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(1));
}
@@ -116,12 +116,12 @@ namespace Tests.EditMode.Network
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerB, out var stateB), Is.True);
Assert.That(stateA.PlayerId, Is.EqualTo("player-a"));
Assert.That(stateA.LastAcceptedMoveTick, Is.EqualTo(10));
Assert.That(stateA.PositionX, Is.EqualTo(0.2f).Within(0.0001f));
Assert.That(stateA.PositionZ, Is.EqualTo(0f).Within(0.0001f));
Assert.That(stateA.PositionZ, Is.EqualTo(0.2f).Within(0.0001f));
Assert.That(stateA.PositionX, Is.EqualTo(0f).Within(0.0001f));
Assert.That(stateB.PlayerId, Is.EqualTo("player-b"));
Assert.That(stateB.LastAcceptedMoveTick, Is.EqualTo(3));
Assert.That(stateB.PositionX, Is.EqualTo(-0.2f).Within(0.0001f));
Assert.That(stateB.PositionZ, Is.EqualTo(0f).Within(0.0001f));
Assert.That(stateB.PositionZ, Is.EqualTo(-0.2f).Within(0.0001f));
Assert.That(stateB.PositionX, Is.EqualTo(0f).Within(0.0001f));
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(2));
}
@@ -168,9 +168,9 @@ namespace Tests.EditMode.Network
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));
Assert.That(firstBroadcast.Position.Z, Is.EqualTo(1f).Within(0.0001f));
Assert.That(firstBroadcast.Velocity.Z, Is.EqualTo(10f).Within(0.0001f));
Assert.That(firstBroadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
createdTransports[9001].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
{
@@ -192,9 +192,9 @@ 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.Position.Z, Is.EqualTo(1f).Within(0.0001f));
Assert.That(secondBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
Assert.That(secondBroadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
}
[Test]
@@ -281,9 +281,9 @@ 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));
Assert.That(broadcast.Position.Z, Is.EqualTo(-0.3f).Within(0.0001f));
Assert.That(broadcast.Position.X, Is.EqualTo(0f).Within(0.0001f));
Assert.That(broadcast.Velocity.Z, Is.EqualTo(-6f).Within(0.0001f));
}
private static FakeTransport CreateTransport(IDictionary<int, FakeTransport> createdTransports, int port)