RUDPClient/Assets/Tests/EditMode/Network/MovementAlgorithmConsistenc...

278 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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=0throttle=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;
}
}
}