using System; using System.Collections; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using Google.Protobuf; using Network.Defines; using Network.NetworkApplication; using Network.NetworkHost; using Network.NetworkTransport; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; using Vector3 = UnityEngine.Vector3; namespace Tests.PlayMode.Network { /// /// PlayMode 测试:使用协程控制帧推进,验证客户端移动与服务端 authoritative state 的一致性。 /// 使用真实的 ServerRuntimeEntryPoint,直接获取服务端的 authoritative state 进行对比。 /// public class MovementConsistencyPlayModeTests { private static readonly IPEndPoint ClientPeer = new(IPAddress.Loopback, 9701); // 测试参数 private const float MoveSpeed = 4f; private const float TurnSpeed = 180f; private const float DeltaTime = 0.05f; // 50ms 模拟步长 [UnityTest] public IEnumerator OneFrame_MoveForward_ClientAndServerPositionMatch() { yield return RunMovementTest( playerId: "player-1frame", inputSequence: new[] { (0f, 1f) }, // 1帧:前进 expectedTotalMovement: MoveSpeed * DeltaTime // 4 * 0.05 = 0.2 ); } [UnityTest] public IEnumerator FiveFrames_MoveForward_ClientAndServerPositionMatch() { yield return RunMovementTest( playerId: "player-5frames", inputSequence: new[] { (0f, 1f), (0f, 1f), (0f, 1f), (0f, 1f), (0f, 1f) }, // 5帧:连续前进 expectedTotalMovement: MoveSpeed * DeltaTime * 5 // 4 * 0.05 * 5 = 1.0 ); } [UnityTest] public IEnumerator IntermittentMovement_StartStopStart_ClientAndServerPositionMatch() { // 场景:前进2帧 -> 停止1帧 -> 前进2帧 yield return RunMovementTest( playerId: "player-intermittent", inputSequence: new[] { (0f, 1f), // 帧1:前进 (0f, 1f), // 帧2:前进 (0f, 0f), // 帧3:停止 (0f, 1f), // 帧4:前进 (0f, 1f), // 帧5:前进 }, expectedTotalMovement: MoveSpeed * DeltaTime * 4 // 4 * 0.05 * 4 = 0.8(只有4帧在移动) ); } private IEnumerator RunMovementTest( string playerId, (float turn, float throttle)[] inputSequence, float expectedTotalMovement) { // ========== 创建服务器 ========== var serverTransports = new Dictionary(); var configuration = new ServerRuntimeConfiguration(9700) { SyncPort = 9701, Dispatcher = new MainThreadNetworkDispatcher(), TransportFactory = port => { var transport = new FakeTestTransport(); serverTransports[port] = transport; return transport; }, AuthoritativeMovement = new ServerAuthoritativeMovementConfiguration { MoveSpeed = MoveSpeed, SimulationInterval = TimeSpan.FromMilliseconds(50), BroadcastInterval = TimeSpan.FromMilliseconds(50) } }; var serverRuntime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult(); // ========== 登录 ========== serverRuntime.Host.NotifyLoginStarted(ClientPeer); serverRuntime.Host.NotifyLoginSucceeded(ClientPeer, playerId, MoveSpeed); yield return null; // 让服务器初始化 serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50)); Debug.Log($"[Setup] Login complete. BroadcastMessages on sync transport: {serverTransports[9701].BroadcastMessages.Count}"); // ========== 客户端本地计算状态 ========== float clientRotation = 0f; // Unity Yaw = 0 -> heading = 90 Vector3 clientPosition = Vector3.zero; // ========== 运行帧模拟 ========== int tick = 1; List serverPositions = new List(); List clientPositions = new List(); foreach (var (turnInput, throttleInput) in inputSequence) { // --- 构造 MoveInput --- var moveInput = new MoveInput { PlayerId = playerId, Tick = tick, TurnInput = turnInput, ThrottleInput = throttleInput }; // --- 发送到服务器 --- var envelope = new Envelope { Type = (int)MessageType.MoveInput, Payload = ByteString.CopyFrom(moveInput.ToByteArray()) }; Debug.Log($"[Tick {tick}] EmitReceive MoveInput to sync transport (turn={turnInput}, throttle={throttleInput}), BroadcastMessages before: {serverTransports[9701].BroadcastMessages.Count}"); serverTransports[9701].EmitReceive(envelope.ToByteArray(), ClientPeer); // --- 服务器处理 --- Debug.Log($"[Tick {tick}] Before DrainPendingMessagesAsync, BroadcastMessages: {serverTransports[9701].BroadcastMessages.Count}"); serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50)); Debug.Log($"[Tick {tick}] After DrainPendingMessagesAsync + UpdateAuthoritativeMovement, BroadcastMessages: {serverTransports[9701].BroadcastMessages.Count}"); // --- 获取服务端 authoritative state(不需要等待广播)--- Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var serverState), Is.True); var serverPos = new Vector3(serverState.PositionX, serverState.PositionY, serverState.PositionZ); serverPositions.Add(serverPos); // --- 客户端本地计算(与服务端相同的算法) --- var clampedTurnInput = Mathf.Clamp(turnInput, -1f, 1f); var clampedThrottleInput = Mathf.Clamp(throttleInput, -1f, 1f); // 旋转(客户端 Tank Control) var heading = NormalizeDegrees(UnityYawToHeading(clientRotation) + (clampedTurnInput * TurnSpeed * DeltaTime)); clientRotation = HeadingToUnityYaw(heading); // 速度(客户端 ResolveHeadingForward: forward = (cos, 0, sin)) var headingRad = heading * Mathf.Deg2Rad; var forwardX = Mathf.Cos(headingRad); var forwardZ = Mathf.Sin(headingRad); var velocityX = forwardX * (clampedThrottleInput * MoveSpeed); var velocityZ = forwardZ * (clampedThrottleInput * MoveSpeed); clientPosition.x += velocityX * DeltaTime; clientPosition.z += velocityZ * DeltaTime; clientPositions.Add(clientPosition); Debug.Log($"[Tick {tick}] Turn={turnInput}, Throttle={throttleInput} | " + $"Client=({clientPosition.x:F3}, {clientPosition.y:F3}, {clientPosition.z:F3}) | " + $"Server=({serverPos.x:F3}, {serverPos.y:F3}, {serverPos.z:F3})"); tick++; yield return null; // 推进一帧 } // ========== 验证 ========== var finalClientPos = clientPositions[^1]; var finalServerPos = serverPositions[^1]; Debug.Log($"========================================"); Debug.Log($"[Test: {playerId}]"); Debug.Log($"Expected Z Movement: {expectedTotalMovement}"); Debug.Log($"Client Final Z: {finalClientPos.z:F4}"); Debug.Log($"Server Final Z: {finalServerPos.z:F4}"); Debug.Log($"Server Final Full: ({finalServerPos.x:F4}, {finalServerPos.y:F4}, {finalServerPos.z:F4})"); Debug.Log($"========================================"); float clientMovement = finalClientPos.z; float serverMovement = finalServerPos.z; Assert.That(Math.Abs(clientMovement - serverMovement), Is.LessThan(0.01f), $"Client Z movement ({clientMovement:F4}) should match Server Z movement ({serverMovement:F4})"); Assert.That(Math.Abs(clientMovement - expectedTotalMovement), Is.LessThan(0.01f), $"Movement ({clientMovement:F4}) should match expected ({expectedTotalMovement:F4})"); // ========== 清理 ========== serverRuntime.Stop(); } private static float NormalizeDegrees(float degrees) { var normalized = degrees % 360f; if (normalized < 0f) normalized += 360f; return normalized; } private static float UnityYawToHeading(float unityYawDegrees) { return NormalizeDegrees(90f - unityYawDegrees); } private static float HeadingToUnityYaw(float headingDegrees) { return NormalizeDegrees(90f - headingDegrees); } } // ========== 测试辅助类 ========== /// /// 用于 PlayMode 测试的 FakeTransport /// public class FakeTestTransport : ITransport { private readonly List _receivedMessages = new(); public List BroadcastMessages { get; } = new(); public event Action OnReceive; public void EmitReceive(byte[] data, IPEndPoint sender) { _receivedMessages.Add(data); OnReceive?.Invoke(data, sender); } public void Send(byte[] data) { } public void SendTo(byte[] data, IPEndPoint target) { } public void SendToAll(byte[] data) { BroadcastMessages.Add(data); OnReceive?.Invoke(data, new IPEndPoint(IPAddress.Loopback, 0)); } public Task StartAsync() => Task.CompletedTask; public void Stop() { } } }