RUDPFramework/Assets/Scripts/MovementComponent.cs

400 lines
15 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.Collections.Generic;
using Network.Defines;
using Network.NetworkApplication;
using UnityEngine;
using Vector3 = UnityEngine.Vector3;
public class MovementComponent : MonoBehaviour
{
[SerializeField] private int _speed = 2;
[SerializeField] private Rigidbody _rigid;
[SerializeField] private InputComponent _inputComponent;
private Player _master;
private const float TurnSpeedDegreesPerSecond = 180f;
// 测试时设为 false可接收服务器状态日志但不应用校正
[SerializeField] private bool _applyServerCorrection = true;
private const float UnityYawOffsetDegrees = 90f;
// Server authoritative movement cadence used for replay substepping.
// This matches ServerAuthoritativeMovementConfiguration.SimulationInterval (50ms).
private const float kServerSimulationStepSeconds = 0.05f;
private bool _isControlled = false;
private Vector3 _serverPosition;
private bool _hasServerState = false;
private ClientAuthoritativePlayerStateSnapshot _lastAuthoritativeState;
private ControlledPlayerVisualCorrectionState _activeVisualCorrection;
public long Tick { get; private set; } = 0;
private long _startTickOffset = 0;
private long _currentTickOffset = 0;
private float _simulationAccumulator = 0f;
private readonly ClientPredictionBuffer _predictionBuffer = new ClientPredictionBuffer();
private readonly RemotePlayerSnapshotInterpolator _remoteSnapshotInterpolator = new();
[SerializeField] private float _lerpRate = 0.1f;
private Vector3 _lastAimDirection = Vector3.forward;
public void Init(bool isControlled, Player master, ClientMovementBootstrap bootstrap)
{
Init(isControlled, master, bootstrap.AuthoritativeMoveSpeed, bootstrap.ServerTick);
}
public void Init(bool isControlled, Player master, int speed = 0, long serverTick = 0)
{
_master = master;
_isControlled = isControlled;
_speed = speed;
_startTickOffset = serverTick;
_rigid.interpolation = isControlled ? RigidbodyInterpolation.None : RigidbodyInterpolation.Interpolate;
_rigid.isKinematic = !isControlled;
_rigid.velocity = Vector3.zero;
_rigid.angularVelocity = Vector3.zero;
// 设置 InputComponent 的 playerId
if (_inputComponent != null)
{
_inputComponent.InjectPlayerId(master.PlayerId);
}
if (serverTick != 0 && _isControlled && MainUI.Instance != null)
MainUI.Instance.OnStartTickOffsetChanged(serverTick);
}
private void Update()
{
if (_isControlled)
{
if (_inputComponent != null)
{
MainUI.Instance.OnClientTickChanged(_inputComponent.CurrentTick);
}
}
}
private void Start()
{
// 订阅 InputComponent 的事件来记录预测输入
if (_inputComponent != null)
{
_inputComponent.OnMoveInputCreated += HandleMoveInputCreated;
}
}
private void OnDestroy()
{
if (_inputComponent != null)
{
_inputComponent.OnMoveInputCreated -= HandleMoveInputCreated;
}
}
private void HandleMoveInputCreated(MoveInput moveInput)
{
// 记录到预测缓冲区,用于后续的服务器状态校正和回放
_predictionBuffer.Record(moveInput);
}
/// <summary>
/// 测试用:设置是否应用服务器状态校正(默认 true
/// 设为 false 时只打印服务器状态日志,不影响本地位置
/// </summary>
public void SetApplyServerCorrection(bool apply)
{
_applyServerCorrection = apply;
}
private void FixedUpdate()
{
if (_isControlled)
{
if (_hasServerState)
{
if (MainUI.Instance != null)
{
MainUI.Instance.OnServerPosChanged(_serverPosition);
}
Reconcile(_lastAuthoritativeState);
_hasServerState = false;
}
// 累积时间,按服务端 50ms 步长进行模拟
_simulationAccumulator += Time.fixedDeltaTime;
while (_simulationAccumulator >= kServerSimulationStepSeconds)
{
var pendingCount = _predictionBuffer.PendingInputs.Count;
if (pendingCount == 0)
{
// 没有待处理的输入,清零累积时间,跳出循环
_simulationAccumulator = 0f;
break;
}
Debug.Log(
$"[SimulateLoop] frame={Time.frameCount} accum={_simulationAccumulator:F4} pendingCount={pendingCount}");
// 使用最近发送的 MoveInput来自 predictionBuffer而非实时输入
// 确保客户端与服务端的输入时序一致
Simulate(GetLatestPredictedInput());
_simulationAccumulator -= kServerSimulationStepSeconds;
}
// 注意:模拟时间现在在 Simulate() 内部通过 AccumulateLatest 累加
}
else
{
var sample = _remoteSnapshotInterpolator.Sample(Time.time);
if (sample.HasValue)
{
_rigid.MovePosition(sample.Position);
_rigid.MoveRotation(sample.Rotation);
_rigid.velocity = sample.Velocity;
}
}
}
private void Reconcile(ClientAuthoritativePlayerStateSnapshot snapshot)
{
_serverPosition = snapshot.Position;
if (!_predictionBuffer.TryApplyAuthoritativeState(snapshot.SourceState, Time.time, out var replayInputs))
{
return;
}
var predictedPosition = _rigid.position;
var predictedRotation = _rigid.rotation;
var correction = ControlledPlayerCorrection.Resolve(
predictedPosition,
predictedRotation,
snapshot.Position,
snapshot.RotationQuaternion,
new ControlledPlayerCorrectionSettings(kServerSimulationStepSeconds, _speed, TurnSpeedDegreesPerSecond,
snapDistanceMultiplier: 5f),
_activeVisualCorrection);
_activeVisualCorrection = correction.NextState;
_rigid.position = correction.Position;
_rigid.rotation = correction.Rotation;
_rigid.velocity = correction.UsedHardSnap ? snapshot.Velocity : Vector3.zero;
_rigid.angularVelocity = Vector3.zero;
// 位置已被校正到服务器位置,不需要再 ReplayPendingInputs
// 因为 pendingInputs 中的 SimulatedDurationSeconds 是累积的模拟时间,
// 如果用来 replay 会导致多余的移动。清空 pendingInputs 让客户端从校正位置重新开始
if (replayInputs.Count > 0)
{
_predictionBuffer.ClearPendingInputs();
}
// 清零 accumulator 防止 FixedUpdate 中再次 Simulate 导致重复移动
_simulationAccumulator = 0f;
if (MainUI.Instance != null)
{
MainUI.Instance.OnCorrectionMagnitudeChanged?.Invoke(
predictedPosition,
snapshot.Position,
correction.PositionError,
correction.RotationErrorDegrees);
MainUI.Instance.OnAcknowledgedMoveTickChanged?.Invoke(_predictionBuffer.LastAcknowledgedMoveTick ?? 0);
}
}
private Vector3 ResolveAimDirection()
{
var planarForward = Vector3.ProjectOnPlane(_rigid.transform.forward, Vector3.up);
if (ClientGameplayInputFlow.HasPlanarInput(planarForward))
{
_lastAimDirection = planarForward;
return planarForward;
}
return ClientGameplayInputFlow.HasPlanarInput(_lastAimDirection)
? _lastAimDirection
: ResolveHeadingForward(UnityYawToHeading(_rigid.rotation.eulerAngles.y));
}
private void Simulate(Vector3 input)
{
Debug.Log(
$"[Simulate] frame={Time.frameCount} input=({input.x:F2},{input.z:F2}) accum={_simulationAccumulator:F4}");
ApplyTankMovement(input.x, input.z, kServerSimulationStepSeconds);
//ApplyTankMovement(-input.x, input.z, kServerSimulationStepSeconds);
// 每次 Simulate 后累加模拟时间(用于 Reconcile 时的重放)
_predictionBuffer.AccumulateLatest(kServerSimulationStepSeconds);
if (_isControlled)
{
if (MainUI.Instance != null)
{
MainUI.Instance.OnClientPosChanged(_rigid.position);
}
// 打印客户端当前状态,用于与服务端状态对比
Debug.Log($"[ClientState] Tick={Tick} " +
$"Pos=({_rigid.position.x:F3}, {_rigid.position.y:F3}, {_rigid.position.z:F3}) " +
$"Rot={_rigid.rotation.eulerAngles.y:F2}");
}
}
public void OnAuthoritativeState(ClientAuthoritativePlayerStateSnapshot snapshot)
{
if (_isControlled)
{
_lastAuthoritativeState = snapshot;
// 打印服务端状态,用于与客户端计算结果对比
Debug.Log($"[ServerState] Tick={snapshot.SourceState.Tick} " +
$"Pos=({snapshot.SourceState.Position.X:F3}, {snapshot.SourceState.Position.Y:F3}, {snapshot.SourceState.Position.Z:F3}) " +
$"Rot={snapshot.SourceState.Rotation:F2} " +
$"Vel=({snapshot.SourceState.Velocity.X:F3}, {snapshot.SourceState.Velocity.Y:F3}, {snapshot.SourceState.Velocity.Z:F3}) " +
$"AckTick={snapshot.AcknowledgedMoveTick}");
// 清理已确认的旧输入,确保客户端使用正确的(已确认的)输入
var pendingBefore = _predictionBuffer.PendingInputs.Count;
_predictionBuffer.PruneAcknowledgedInputs(snapshot.AcknowledgedMoveTick);
var pendingAfter = _predictionBuffer.PendingInputs.Count;
Debug.Log(
$"[Prune] AckTick={snapshot.AcknowledgedMoveTick} removed {pendingBefore - pendingAfter}/{pendingBefore} inputs, remaining={pendingAfter}");
// 收到服务器状态后,必须清空 pendingInputs
// 因为 pendingInputs 中的 SimulatedDurationSeconds 是累积的模拟时间,
// 如果不清理,客户端会继续用这些输入移动(测试模式下位置不被服务器校正)
_predictionBuffer.ClearPendingInputs();
_simulationAccumulator = 0f;
// 只有开启校正时才设置 _hasServerState否则只打印日志不应用
if (_applyServerCorrection)
{
_hasServerState = true;
}
}
else
{
_lastAuthoritativeState = snapshot;
_remoteSnapshotInterpolator.TryAddSnapshot(snapshot, Time.time);
}
}
public void SetServerTick(long serverTick)
{
_currentTickOffset = serverTick - Tick - _startTickOffset;
if (_isControlled)
{
if (MainUI.Instance != null)
{
MainUI.Instance.OnServerTickChanged(serverTick);
}
}
}
/// <summary>
/// 获取最近发送的 MoveInput用于与服务器输入时序对齐。
/// 如果没有记录的输入,返回零向量(停止状态)。
/// </summary>
private Vector3 GetLatestPredictedInput()
{
var pending = _predictionBuffer.PendingInputs;
if (pending.Count == 0)
{
Debug.Log("[MoveInput] No pending inputs, using zero (stop)");
return Vector3.zero;
}
var latest = pending[^1];
Debug.Log(
$"[MoveInput] Using tick={latest.Input.Tick} TurnInput={latest.Input.TurnInput} ThrottleInput={latest.Input.ThrottleInput} ({pending.Count} pending)");
// MoveInput 的 TurnInput/ThrottleInput 转回 Unity 的 x/z 格式
// 注意 TurnInput 在 MoveInput 里是正数=右,正数=-input.x=左(需要取反)
// ThrottleInput 在 MoveInput 里正数=前进,正数=input.z=前
return new Vector3(-latest.Input.TurnInput, 0f, latest.Input.ThrottleInput);
}
private void ReplayPendingInputs(IReadOnlyList<PredictedMoveStep> replayInputs)
{
foreach (var replayInput in replayInputs)
{
var remaining = replayInput.SimulatedDurationSeconds;
while (remaining > 0f)
{
// Use the server's fixed cadence (50ms) as the substep size to ensure
// replay trajectory matches live FixedUpdate prediction exactly.
var step = Mathf.Min(remaining, kServerSimulationStepSeconds);
ApplyTankMovement(
replayInput.Input.TurnInput,
replayInput.Input.ThrottleInput,
step);
remaining -= step;
}
}
if (_isControlled)
{
if (MainUI.Instance != null)
{
MainUI.Instance.OnClientPosChanged(_rigid.position);
}
}
}
private void ApplyTankMovement(float turnInput, float throttleInput, float deltaTime)
{
if (deltaTime <= 0f)
{
_rigid.velocity = Vector3.zero;
return;
}
var clampedTurnInput = Mathf.Clamp(turnInput, -1f, 1f);
var clampedThrottleInput = Mathf.Clamp(throttleInput, -1f, 1f);
var heading = NormalizeDegrees(UnityYawToHeading(_rigid.rotation.eulerAngles.y) +
(clampedTurnInput * TurnSpeedDegreesPerSecond * deltaTime));
_rigid.rotation = Quaternion.Euler(0f, HeadingToUnityYaw(heading), 0f);
var forward = ResolveHeadingForward(heading);
var velocity = forward * (clampedThrottleInput * _speed);
Vector3 targetPos = _rigid.position + velocity * deltaTime;
_rigid.MovePosition(targetPos);
// _rigid.velocity = velocity;
// _rigid.position += velocity * deltaTime;
// 调试日志:打印每步计算细节
Debug.Log($"[MoveStep] _speed={_speed} deltaTime={deltaTime:F4} throttle={clampedThrottleInput} " +
$"heading={heading:F2} velocity=({velocity.x:F3}, {velocity.y:F3}, {velocity.z:F3}) " +
$"pos=({_rigid.position.x:F3}, {_rigid.position.y:F3}, {_rigid.position.z:F3})");
}
private static Vector3 ResolveHeadingForward(float headingDegrees)
{
var rotationRadians = headingDegrees * Mathf.Deg2Rad;
return new Vector3(Mathf.Cos(rotationRadians), 0f, Mathf.Sin(rotationRadians));
}
private static float HeadingToUnityYaw(float headingDegrees)
{
return NormalizeDegrees(UnityYawOffsetDegrees - headingDegrees);
}
private static float UnityYawToHeading(float unityYawDegrees)
{
return NormalizeDegrees(UnityYawOffsetDegrees - unityYawDegrees);
}
private static float NormalizeDegrees(float degrees)
{
var normalized = degrees % 360f;
if (normalized < 0f)
{
normalized += 360f;
}
return normalized;
}
}