优化输入回放逻辑 + 调整项目结构
This commit is contained in:
parent
75289b5690
commit
098a1dd68c
|
|
@ -1,23 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using Network.Defines;
|
using Network.Defines;
|
||||||
|
|
||||||
namespace Network.NetworkApplication
|
namespace Network.NetworkApplication
|
||||||
{
|
{
|
||||||
public readonly struct PredictedMoveStep
|
|
||||||
{
|
|
||||||
public PredictedMoveStep(MoveInput input, float simulatedDurationSeconds)
|
|
||||||
{
|
|
||||||
Input = input ?? throw new ArgumentNullException(nameof(input));
|
|
||||||
SimulatedDurationSeconds = simulatedDurationSeconds < 0f ? 0f : simulatedDurationSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MoveInput Input { get; }
|
|
||||||
|
|
||||||
public float SimulatedDurationSeconds { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ClientPredictionBuffer
|
public sealed class ClientPredictionBuffer
|
||||||
{
|
{
|
||||||
private readonly List<PredictedMoveStep> pendingInputs = new();
|
private readonly List<PredictedMoveStep> pendingInputs = new();
|
||||||
|
|
@ -64,33 +50,38 @@ namespace Network.NetworkApplication
|
||||||
pendingInputs.Add(new PredictedMoveStep(input, 0f));
|
pendingInputs.Add(new PredictedMoveStep(input, 0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AccumulateLatest(float simulatedDurationSeconds)
|
public bool TryGetNextUnsimulatedInput(out PredictedMoveStep predictedMoveStep)
|
||||||
{
|
{
|
||||||
if (pendingInputs.Count == 0 || simulatedDurationSeconds <= 0f)
|
for (var i = 0; i < pendingInputs.Count; i++)
|
||||||
|
{
|
||||||
|
if (pendingInputs[i].SimulatedDurationSeconds <= 0f)
|
||||||
|
{
|
||||||
|
predictedMoveStep = pendingInputs[i];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
predictedMoveStep = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkInputSimulated(long tick, float simulatedDurationSeconds)
|
||||||
|
{
|
||||||
|
if (simulatedDurationSeconds <= 0f)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var latest = pendingInputs[^1];
|
for (var i = 0; i < pendingInputs.Count; i++)
|
||||||
pendingInputs[^1] =
|
{
|
||||||
new PredictedMoveStep(latest.Input, latest.SimulatedDurationSeconds + simulatedDurationSeconds);
|
if (pendingInputs[i].Input.Tick != tick)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
pendingInputs[i] = new PredictedMoveStep(pendingInputs[i].Input, simulatedDurationSeconds);
|
||||||
/// Accumulate pending input duration using the actual elapsed wall-clock time
|
|
||||||
/// since the last authoritative state, not the fixed simulation cadence.
|
|
||||||
/// This synchronizes accumulation with the server's 20Hz authoritative cadence.
|
|
||||||
/// </summary>
|
|
||||||
public void AccumulateWithElapsedTime(float elapsedSinceLastState)
|
|
||||||
{
|
|
||||||
if (pendingInputs.Count == 0 || elapsedSinceLastState <= 0f || !float.IsFinite(elapsedSinceLastState))
|
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var latest = pendingInputs[^1];
|
|
||||||
pendingInputs[^1] =
|
|
||||||
new PredictedMoveStep(latest.Input, latest.SimulatedDurationSeconds + elapsedSinceLastState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryApplyAuthoritativeState(PlayerState state, float currentTime,
|
public bool TryApplyAuthoritativeState(PlayerState state, float currentTime,
|
||||||
|
|
@ -110,7 +101,7 @@ namespace Network.NetworkApplication
|
||||||
LastAuthoritativeTick = state.Tick;
|
LastAuthoritativeTick = state.Tick;
|
||||||
LastAcknowledgedMoveTick = state.AcknowledgedMoveTick;
|
LastAcknowledgedMoveTick = state.AcknowledgedMoveTick;
|
||||||
pendingInputs.RemoveAll(input => input.Input.Tick <= state.AcknowledgedMoveTick);
|
pendingInputs.RemoveAll(input => input.Input.Tick <= state.AcknowledgedMoveTick);
|
||||||
replayInputs = pendingInputs.ToArray();
|
replayInputs = pendingInputs.FindAll(input => input.SimulatedDurationSeconds > 0f);
|
||||||
|
|
||||||
// Reset the elapsed-time tracker so the next accumulation period
|
// Reset the elapsed-time tracker so the next accumulation period
|
||||||
// starts from this authoritative state's arrival time.
|
// starts from this authoritative state's arrival time.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System;
|
||||||
|
using Network.Defines;
|
||||||
|
|
||||||
|
namespace Network.NetworkApplication
|
||||||
|
{
|
||||||
|
public readonly struct PredictedMoveStep
|
||||||
|
{
|
||||||
|
public PredictedMoveStep(MoveInput input, float simulatedDurationSeconds)
|
||||||
|
{
|
||||||
|
Input = input ?? throw new ArgumentNullException(nameof(input));
|
||||||
|
SimulatedDurationSeconds = simulatedDurationSeconds < 0f ? 0f : simulatedDurationSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MoveInput Input { get; }
|
||||||
|
|
||||||
|
public float SimulatedDurationSeconds { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 20280892ecfd4b25bff064d07668accc
|
||||||
|
timeCreated: 1775619625
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 309fec203b04d2b4cb87f9c2873c0449
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 输入源接口,用于解耦输入捕获
|
||||||
|
/// </summary>
|
||||||
|
public interface IInputSource
|
||||||
|
{
|
||||||
|
Vector3 GetPlanarInput();
|
||||||
|
bool ConsumeShootInput();
|
||||||
|
Vector3 GetAimDirection();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e7d6671c80a243619f1f3dc34ca92d15
|
||||||
|
timeCreated: 1775619222
|
||||||
|
|
@ -1,129 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Network.Defines;
|
using Network.Defines;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Vector3 = UnityEngine.Vector3;
|
using Vector3 = UnityEngine.Vector3;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 输入源接口,用于解耦输入捕获
|
|
||||||
/// </summary>
|
|
||||||
public interface IInputSource
|
|
||||||
{
|
|
||||||
Vector3 GetPlanarInput();
|
|
||||||
bool ConsumeShootInput();
|
|
||||||
Vector3 GetAimDirection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 真实的 Unity 输入源
|
|
||||||
/// </summary>
|
|
||||||
public class UnityInputSource : IInputSource
|
|
||||||
{
|
|
||||||
private readonly Transform _cameraTransform;
|
|
||||||
|
|
||||||
public UnityInputSource(Transform cameraTransform)
|
|
||||||
{
|
|
||||||
_cameraTransform = cameraTransform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector3 GetPlanarInput()
|
|
||||||
{
|
|
||||||
return new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ConsumeShootInput()
|
|
||||||
{
|
|
||||||
return Input.GetMouseButtonDown(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector3 GetAimDirection()
|
|
||||||
{
|
|
||||||
if (_cameraTransform != null)
|
|
||||||
{
|
|
||||||
return _cameraTransform.forward;
|
|
||||||
}
|
|
||||||
return Vector3.forward;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 模拟输入源(测试用),提供预设的输入序列
|
|
||||||
/// </summary>
|
|
||||||
public class SimulatedInputSource : IInputSource
|
|
||||||
{
|
|
||||||
private readonly (float turn, float throttle)[] _inputSequence;
|
|
||||||
private int _index;
|
|
||||||
private Vector3 _lastAimDirection = Vector3.forward;
|
|
||||||
private bool _shootTriggered;
|
|
||||||
|
|
||||||
public SimulatedInputSource((float turn, float throttle)[] sequence)
|
|
||||||
{
|
|
||||||
_inputSequence = sequence;
|
|
||||||
_index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector3 GetPlanarInput()
|
|
||||||
{
|
|
||||||
if (_index >= _inputSequence.Length)
|
|
||||||
{
|
|
||||||
return Vector3.zero;
|
|
||||||
}
|
|
||||||
var (turn, throttle) = _inputSequence[_index];
|
|
||||||
return new Vector3(turn, 0f, throttle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ConsumeShootInput()
|
|
||||||
{
|
|
||||||
if (_shootTriggered)
|
|
||||||
{
|
|
||||||
_shootTriggered = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector3 GetAimDirection()
|
|
||||||
{
|
|
||||||
return _lastAimDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 推进到下一个输入
|
|
||||||
/// </summary>
|
|
||||||
public void Advance()
|
|
||||||
{
|
|
||||||
if (_index < _inputSequence.Length)
|
|
||||||
{
|
|
||||||
_index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 是否还有更多输入
|
|
||||||
/// </summary>
|
|
||||||
public bool HasMore => _index < _inputSequence.Length;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 设置射击触发(下次 ConsumeShootInput 返回 true)
|
|
||||||
/// </summary>
|
|
||||||
public void SetShootTriggered()
|
|
||||||
{
|
|
||||||
_shootTriggered = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 设置瞄准方向
|
|
||||||
/// </summary>
|
|
||||||
public void SetAimDirection(Vector3 direction)
|
|
||||||
{
|
|
||||||
_lastAimDirection = direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前输入索引
|
|
||||||
/// </summary>
|
|
||||||
public int CurrentIndex => _index;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 输入组件,负责从 IInputSource 获取输入、发送 MoveInput 到服务器、管理 tick
|
/// 输入组件,负责从 IInputSource 获取输入、发送 MoveInput 到服务器、管理 tick
|
||||||
|
|
@ -142,8 +21,8 @@ public class InputComponent : MonoBehaviour
|
||||||
private bool _wasMovingLastFrame;
|
private bool _wasMovingLastFrame;
|
||||||
private long _tick;
|
private long _tick;
|
||||||
|
|
||||||
public event System.Action<MoveInput> OnMoveInputCreated;
|
public event Action<MoveInput> OnMoveInputCreated;
|
||||||
public event System.Action<ShootInput> OnShootInputCreated;
|
public event Action<ShootInput> OnShootInputCreated;
|
||||||
|
|
||||||
public long CurrentTick => _tick;
|
public long CurrentTick => _tick;
|
||||||
|
|
||||||
|
|
@ -191,7 +70,7 @@ public class InputComponent : MonoBehaviour
|
||||||
_currentInput = _inputSource?.GetPlanarInput() ?? Vector3.zero;
|
_currentInput = _inputSource?.GetPlanarInput() ?? Vector3.zero;
|
||||||
|
|
||||||
// 检测移动状态变化
|
// 检测移动状态变化
|
||||||
var hasMovement = ClientGameplayInputFlow.HasPlanarInput(_currentInput);
|
bool hasMovement = ClientGameplayInputFlow.HasPlanarInput(_currentInput);
|
||||||
if (hasMovement)
|
if (hasMovement)
|
||||||
{
|
{
|
||||||
_stopMessagePending = false;
|
_stopMessagePending = false;
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟输入源(测试用),提供预设的输入序列
|
||||||
|
/// </summary>
|
||||||
|
public class SimulatedInputSource : IInputSource
|
||||||
|
{
|
||||||
|
private readonly (float turn, float throttle)[] _inputSequence;
|
||||||
|
private int _index;
|
||||||
|
private Vector3 _lastAimDirection = Vector3.forward;
|
||||||
|
private bool _shootTriggered;
|
||||||
|
|
||||||
|
public SimulatedInputSource((float turn, float throttle)[] sequence)
|
||||||
|
{
|
||||||
|
_inputSequence = sequence;
|
||||||
|
_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 GetPlanarInput()
|
||||||
|
{
|
||||||
|
if (_index >= _inputSequence.Length)
|
||||||
|
{
|
||||||
|
return Vector3.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (turn, throttle) = _inputSequence[_index];
|
||||||
|
return new Vector3(turn, 0f, throttle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ConsumeShootInput()
|
||||||
|
{
|
||||||
|
if (_shootTriggered)
|
||||||
|
{
|
||||||
|
_shootTriggered = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 GetAimDirection()
|
||||||
|
{
|
||||||
|
return _lastAimDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 推进到下一个输入
|
||||||
|
/// </summary>
|
||||||
|
public void Advance()
|
||||||
|
{
|
||||||
|
if (_index < _inputSequence.Length)
|
||||||
|
{
|
||||||
|
_index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否还有更多输入
|
||||||
|
/// </summary>
|
||||||
|
public bool HasMore => _index < _inputSequence.Length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置射击触发(下次 ConsumeShootInput 返回 true)
|
||||||
|
/// </summary>
|
||||||
|
public void SetShootTriggered()
|
||||||
|
{
|
||||||
|
_shootTriggered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置瞄准方向
|
||||||
|
/// </summary>
|
||||||
|
public void SetAimDirection(Vector3 direction)
|
||||||
|
{
|
||||||
|
_lastAimDirection = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前输入索引
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentIndex => _index;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 53e3773e493842e8861ac7522a6227a9
|
||||||
|
timeCreated: 1775619270
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 真实的 Unity 输入源
|
||||||
|
/// </summary>
|
||||||
|
public class UnityInputSource : IInputSource
|
||||||
|
{
|
||||||
|
private readonly Transform _cameraTransform;
|
||||||
|
|
||||||
|
public UnityInputSource(Transform cameraTransform)
|
||||||
|
{
|
||||||
|
_cameraTransform = cameraTransform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 GetPlanarInput()
|
||||||
|
{
|
||||||
|
return new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ConsumeShootInput()
|
||||||
|
{
|
||||||
|
return Input.GetMouseButtonDown(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 GetAimDirection()
|
||||||
|
{
|
||||||
|
if (_cameraTransform != null)
|
||||||
|
{
|
||||||
|
return _cameraTransform.forward;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Vector3.forward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5d7f0a25d2b54decbf2c2386e5c0ebd2
|
||||||
|
timeCreated: 1775619246
|
||||||
|
|
@ -3,23 +3,37 @@ using UnityEngine;
|
||||||
public class MovementComponent : MonoBehaviour
|
public class MovementComponent : MonoBehaviour
|
||||||
{
|
{
|
||||||
[SerializeField] private Rigidbody _rigid;
|
[SerializeField] private Rigidbody _rigid;
|
||||||
private const float InterpolationAlpha = 0.15f;
|
[SerializeField] private float _followMoveSpeed = 2f;
|
||||||
|
[SerializeField] private float _followTurnSpeedDegreesPerSecond = 180f;
|
||||||
|
[SerializeField] private float _correctionDecayMoveSpeed = 4f;
|
||||||
|
[SerializeField] private float _correctionDecayTurnSpeedDegreesPerSecond = 360f;
|
||||||
|
private const float RemoteInterpolationAlpha = 0.15f;
|
||||||
|
private const float UnexpectedTurnLogCooldownSeconds = 0.25f;
|
||||||
|
|
||||||
private bool _isControlled;
|
private bool _isControlled;
|
||||||
private Vector3 _currentPosition;
|
private Vector3 _currentPosition;
|
||||||
private Quaternion _currentRotation;
|
private Quaternion _currentRotation;
|
||||||
private Vector3 _targetPosition;
|
private Vector3 _targetPosition;
|
||||||
private Quaternion _targetRotation;
|
private Quaternion _targetRotation;
|
||||||
|
private Vector3 _correctionPositionOffset;
|
||||||
|
private Quaternion _correctionRotationOffset = Quaternion.identity;
|
||||||
|
private float _expectedTurnInput;
|
||||||
|
private float _lastUnexpectedTurnLogTime = float.NegativeInfinity;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
_rigid ??= GetComponent<Rigidbody>();
|
_rigid ??= GetComponent<Rigidbody>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Init(bool isControlled)
|
public void Init(bool isControlled, float followMoveSpeed = 2f, float followTurnSpeedDegreesPerSecond = 180f)
|
||||||
{
|
{
|
||||||
_rigid ??= GetComponent<Rigidbody>();
|
_rigid ??= GetComponent<Rigidbody>();
|
||||||
_isControlled = isControlled;
|
_isControlled = isControlled;
|
||||||
|
_followMoveSpeed = Mathf.Max(0f, followMoveSpeed);
|
||||||
|
_followTurnSpeedDegreesPerSecond = Mathf.Max(0f, followTurnSpeedDegreesPerSecond);
|
||||||
|
_correctionDecayMoveSpeed = Mathf.Max(_followMoveSpeed * 2f, _followMoveSpeed);
|
||||||
|
_correctionDecayTurnSpeedDegreesPerSecond =
|
||||||
|
Mathf.Max(_followTurnSpeedDegreesPerSecond * 2f, _followTurnSpeedDegreesPerSecond);
|
||||||
_rigid.interpolation = isControlled ? RigidbodyInterpolation.None : RigidbodyInterpolation.Interpolate;
|
_rigid.interpolation = isControlled ? RigidbodyInterpolation.None : RigidbodyInterpolation.Interpolate;
|
||||||
_rigid.isKinematic = !isControlled;
|
_rigid.isKinematic = !isControlled;
|
||||||
_rigid.velocity = Vector3.zero;
|
_rigid.velocity = Vector3.zero;
|
||||||
|
|
@ -29,16 +43,45 @@ public class MovementComponent : MonoBehaviour
|
||||||
_currentRotation = _rigid.rotation;
|
_currentRotation = _rigid.rotation;
|
||||||
_targetPosition = _rigid.position;
|
_targetPosition = _rigid.position;
|
||||||
_targetRotation = _rigid.rotation;
|
_targetRotation = _rigid.rotation;
|
||||||
|
_correctionPositionOffset = Vector3.zero;
|
||||||
|
_correctionRotationOffset = Quaternion.identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
_currentPosition = Vector3.Lerp(_currentPosition, _targetPosition, InterpolationAlpha);
|
var beforeRotation = _currentRotation;
|
||||||
_currentRotation = Quaternion.Slerp(_currentRotation, _targetRotation, InterpolationAlpha);
|
|
||||||
|
if (_isControlled)
|
||||||
|
{
|
||||||
|
_correctionPositionOffset = Vector3.MoveTowards(
|
||||||
|
_correctionPositionOffset,
|
||||||
|
Vector3.zero,
|
||||||
|
_correctionDecayMoveSpeed * Time.deltaTime);
|
||||||
|
_correctionRotationOffset = Quaternion.RotateTowards(
|
||||||
|
_correctionRotationOffset,
|
||||||
|
Quaternion.identity,
|
||||||
|
_correctionDecayTurnSpeedDegreesPerSecond * Time.deltaTime);
|
||||||
|
|
||||||
|
var desiredPosition = _targetPosition + _correctionPositionOffset;
|
||||||
|
var desiredRotation = _targetRotation * _correctionRotationOffset;
|
||||||
|
|
||||||
|
_currentPosition = Vector3.MoveTowards(_currentPosition, desiredPosition, _followMoveSpeed * Time.deltaTime);
|
||||||
|
_currentRotation = Quaternion.RotateTowards(
|
||||||
|
_currentRotation,
|
||||||
|
desiredRotation,
|
||||||
|
_followTurnSpeedDegreesPerSecond * Time.deltaTime);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_currentPosition = Vector3.Lerp(_currentPosition, _targetPosition, RemoteInterpolationAlpha);
|
||||||
|
_currentRotation = Quaternion.Slerp(_currentRotation, _targetRotation, RemoteInterpolationAlpha);
|
||||||
|
}
|
||||||
|
|
||||||
_rigid.position = _currentPosition;
|
_rigid.position = _currentPosition;
|
||||||
_rigid.rotation = _currentRotation;
|
_rigid.rotation = _currentRotation;
|
||||||
|
|
||||||
|
LogUnexpectedTurnIfNeeded(beforeRotation, _currentRotation);
|
||||||
|
|
||||||
if (_isControlled && MainUI.Instance != null)
|
if (_isControlled && MainUI.Instance != null)
|
||||||
{
|
{
|
||||||
MainUI.Instance.OnClientPosChanged(_currentPosition);
|
MainUI.Instance.OnClientPosChanged(_currentPosition);
|
||||||
|
|
@ -59,13 +102,68 @@ public class MovementComponent : MonoBehaviour
|
||||||
_targetRotation = rotation;
|
_targetRotation = rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetExpectedTurnInput(float expectedTurnInput)
|
||||||
|
{
|
||||||
|
_expectedTurnInput = expectedTurnInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BlendToPoseFromCurrent(Vector3 position, Quaternion rotation)
|
||||||
|
{
|
||||||
|
_targetPosition = position;
|
||||||
|
_targetRotation = rotation;
|
||||||
|
_correctionPositionOffset = _currentPosition - position;
|
||||||
|
_correctionRotationOffset = Quaternion.Inverse(rotation) * _currentRotation;
|
||||||
|
}
|
||||||
|
|
||||||
public void SnapToPose(Vector3 position, Quaternion rotation)
|
public void SnapToPose(Vector3 position, Quaternion rotation)
|
||||||
{
|
{
|
||||||
_currentPosition = position;
|
_currentPosition = position;
|
||||||
_currentRotation = rotation;
|
_currentRotation = rotation;
|
||||||
_targetPosition = position;
|
_targetPosition = position;
|
||||||
_targetRotation = rotation;
|
_targetRotation = rotation;
|
||||||
|
_correctionPositionOffset = Vector3.zero;
|
||||||
|
_correctionRotationOffset = Quaternion.identity;
|
||||||
_rigid.position = position;
|
_rigid.position = position;
|
||||||
_rigid.rotation = rotation;
|
_rigid.rotation = rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LogUnexpectedTurnIfNeeded(Quaternion beforeRotation, Quaternion afterRotation)
|
||||||
|
{
|
||||||
|
if (!_isControlled || Mathf.Abs(_expectedTurnInput) < 0.01f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var beforeError = Quaternion.Angle(beforeRotation, _targetRotation);
|
||||||
|
var afterError = Quaternion.Angle(afterRotation, _targetRotation);
|
||||||
|
if (beforeError < 0.1f && afterError < 0.1f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deltaYaw = Mathf.DeltaAngle(beforeRotation.eulerAngles.y, afterRotation.eulerAngles.y);
|
||||||
|
if (Mathf.Abs(deltaYaw) < 0.01f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterError <= beforeError + 0.05f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Time.time - _lastUnexpectedTurnLogTime < UnexpectedTurnLogCooldownSeconds)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastUnexpectedTurnLogTime = Time.time;
|
||||||
|
Debug.LogWarning(
|
||||||
|
$"[UnexpectedTurnAwayFromTarget] expectedTurn={_expectedTurnInput:F2} deltaYaw={deltaYaw:F2} " +
|
||||||
|
$"beforeYaw={beforeRotation.eulerAngles.y:F2} afterYaw={afterRotation.eulerAngles.y:F2} " +
|
||||||
|
$"beforeError={beforeError:F2} afterError={afterError:F2} " +
|
||||||
|
$"current=({_currentPosition.x:F3},{_currentPosition.y:F3},{_currentPosition.z:F3}) " +
|
||||||
|
$"target=({_targetPosition.x:F3},{_targetPosition.y:F3},{_targetPosition.z:F3}) " +
|
||||||
|
$"correctionRot={_correctionRotationOffset.eulerAngles.y:F2}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ using Vector3 = UnityEngine.Vector3;
|
||||||
public class MovementResolverComponent : MonoBehaviour
|
public class MovementResolverComponent : MonoBehaviour
|
||||||
{
|
{
|
||||||
private const float ServerSimulationStepSeconds = 0.05f;
|
private const float ServerSimulationStepSeconds = 0.05f;
|
||||||
private const float SnapThreshold = 0.5f;
|
[SerializeField] private float SnapThreshold = 0.5f;
|
||||||
|
private const float TurnSpeedDegreesPerSecond = 180f;
|
||||||
|
|
||||||
[SerializeField] private int _speed = 2;
|
[SerializeField] private int _speed = 2;
|
||||||
[SerializeField] private MovementComponent _movement;
|
[SerializeField] private MovementComponent _movement;
|
||||||
|
|
@ -54,7 +55,7 @@ public class MovementResolverComponent : MonoBehaviour
|
||||||
|
|
||||||
if (_movement != null)
|
if (_movement != null)
|
||||||
{
|
{
|
||||||
_movement.Init(isControlled);
|
_movement.Init(isControlled, _speed, TurnSpeedDegreesPerSecond);
|
||||||
_authoritativePosition = _movement.CurrentPosition;
|
_authoritativePosition = _movement.CurrentPosition;
|
||||||
_authoritativeRotation = _movement.CurrentRotation;
|
_authoritativeRotation = _movement.CurrentRotation;
|
||||||
_predictedPosition = _movement.CurrentPosition;
|
_predictedPosition = _movement.CurrentPosition;
|
||||||
|
|
@ -118,14 +119,14 @@ public class MovementResolverComponent : MonoBehaviour
|
||||||
_simulationAccumulator += Time.fixedDeltaTime;
|
_simulationAccumulator += Time.fixedDeltaTime;
|
||||||
while (_simulationAccumulator >= ServerSimulationStepSeconds)
|
while (_simulationAccumulator >= ServerSimulationStepSeconds)
|
||||||
{
|
{
|
||||||
var pendingCount = _predictionBuffer.PendingInputs.Count;
|
if (!_predictionBuffer.TryGetNextUnsimulatedInput(out var nextInput))
|
||||||
if (pendingCount == 0)
|
|
||||||
{
|
{
|
||||||
_simulationAccumulator = 0f;
|
_simulationAccumulator = 0f;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Simulate(GetLatestPredictedInput());
|
Simulate(nextInput.Input);
|
||||||
|
_predictionBuffer.MarkInputSimulated(nextInput.Input.Tick, ServerSimulationStepSeconds);
|
||||||
_simulationAccumulator -= ServerSimulationStepSeconds;
|
_simulationAccumulator -= ServerSimulationStepSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,12 +140,17 @@ public class MovementResolverComponent : MonoBehaviour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Simulate(Vector3 input)
|
private void Simulate(MoveInput moveInput)
|
||||||
{
|
{
|
||||||
TankMovementKinematics.ApplyStep(_speed, input.x, input.z, ServerSimulationStepSeconds,
|
var simulationTurnInput = ToSimulationTurnInput(moveInput.TurnInput);
|
||||||
|
TankMovementKinematics.ApplyStep(
|
||||||
|
_speed,
|
||||||
|
simulationTurnInput,
|
||||||
|
moveInput.ThrottleInput,
|
||||||
|
ServerSimulationStepSeconds,
|
||||||
ref _predictedPosition, ref _predictedRotation);
|
ref _predictedPosition, ref _predictedRotation);
|
||||||
|
_movement.SetExpectedTurnInput(simulationTurnInput);
|
||||||
_movement.SetTargetPose(_predictedPosition, _predictedRotation);
|
_movement.SetTargetPose(_predictedPosition, _predictedRotation);
|
||||||
_predictionBuffer.AccumulateLatest(ServerSimulationStepSeconds);
|
|
||||||
|
|
||||||
if (MainUI.Instance != null)
|
if (MainUI.Instance != null)
|
||||||
{
|
{
|
||||||
|
|
@ -186,13 +192,20 @@ public class MovementResolverComponent : MonoBehaviour
|
||||||
ReplayPendingInputs(replayInputs);
|
ReplayPendingInputs(replayInputs);
|
||||||
|
|
||||||
var error = Vector3.Distance(_movement.CurrentPosition, _predictedPosition);
|
var error = Vector3.Distance(_movement.CurrentPosition, _predictedPosition);
|
||||||
if (error > SnapThreshold)
|
var shouldSnap = error > SnapThreshold;
|
||||||
|
Debug.Log(
|
||||||
|
$"[Reconcile] tick={snapshot.SourceState.Tick} ack={snapshot.AcknowledgedMoveTick} " +
|
||||||
|
$"error={error:F3} threshold={SnapThreshold:F3} snap={shouldSnap} " +
|
||||||
|
$"current=({_movement.CurrentPosition.x:F3},{_movement.CurrentPosition.y:F3},{_movement.CurrentPosition.z:F3}) " +
|
||||||
|
$"predicted=({_predictedPosition.x:F3},{_predictedPosition.y:F3},{_predictedPosition.z:F3}) " +
|
||||||
|
$"authoritative=({_authoritativePosition.x:F3},{_authoritativePosition.y:F3},{_authoritativePosition.z:F3})");
|
||||||
|
if (shouldSnap)
|
||||||
{
|
{
|
||||||
_movement.SnapToPose(_predictedPosition, _predictedRotation);
|
_movement.SnapToPose(_predictedPosition, _predictedRotation);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_movement.SetTargetPose(_predictedPosition, _predictedRotation);
|
_movement.BlendToPoseFromCurrent(_predictedPosition, _predictedRotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
_simulationAccumulator = 0f;
|
_simulationAccumulator = 0f;
|
||||||
|
|
@ -218,35 +231,41 @@ public class MovementResolverComponent : MonoBehaviour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector3 GetLatestPredictedInput()
|
|
||||||
{
|
|
||||||
var pending = _predictionBuffer.PendingInputs;
|
|
||||||
if (pending.Count == 0)
|
|
||||||
{
|
|
||||||
return Vector3.zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
var latest = pending[^1];
|
|
||||||
return new Vector3(-latest.Input.TurnInput, 0f, latest.Input.ThrottleInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReplayPendingInputs(IReadOnlyList<PredictedMoveStep> replayInputs)
|
private void ReplayPendingInputs(IReadOnlyList<PredictedMoveStep> replayInputs)
|
||||||
{
|
{
|
||||||
|
var lastSimulationTurnInput = 0f;
|
||||||
foreach (var replayInput in replayInputs)
|
foreach (var replayInput in replayInputs)
|
||||||
{
|
{
|
||||||
var remaining = replayInput.SimulatedDurationSeconds;
|
var remaining = replayInput.SimulatedDurationSeconds;
|
||||||
while (remaining > 0f)
|
while (remaining > 0f)
|
||||||
{
|
{
|
||||||
var step = Mathf.Min(remaining, ServerSimulationStepSeconds);
|
var step = Mathf.Min(remaining, ServerSimulationStepSeconds);
|
||||||
|
var beforeYaw = _predictedRotation.eulerAngles.y;
|
||||||
|
var simulationTurnInput = ToSimulationTurnInput(replayInput.Input.TurnInput);
|
||||||
|
lastSimulationTurnInput = simulationTurnInput;
|
||||||
TankMovementKinematics.ApplyStep(
|
TankMovementKinematics.ApplyStep(
|
||||||
_speed,
|
_speed,
|
||||||
replayInput.Input.TurnInput,
|
simulationTurnInput,
|
||||||
replayInput.Input.ThrottleInput,
|
replayInput.Input.ThrottleInput,
|
||||||
step,
|
step,
|
||||||
ref _predictedPosition,
|
ref _predictedPosition,
|
||||||
ref _predictedRotation);
|
ref _predictedRotation);
|
||||||
|
var afterYaw = _predictedRotation.eulerAngles.y;
|
||||||
|
Debug.Log(
|
||||||
|
$"[ReplayStep] authTick={_lastAuthoritativeState?.SourceState?.Tick ?? 0} " +
|
||||||
|
$"inputTick={replayInput.Input.Tick} netTurn={replayInput.Input.TurnInput:F2} simTurn={simulationTurnInput:F2} " +
|
||||||
|
$"throttle={replayInput.Input.ThrottleInput:F2} step={step:F3} " +
|
||||||
|
$"yaw={beforeYaw:F2}->{afterYaw:F2} " +
|
||||||
|
$"predicted=({_predictedPosition.x:F3},{_predictedPosition.y:F3},{_predictedPosition.z:F3})");
|
||||||
remaining -= step;
|
remaining -= step;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_movement.SetExpectedTurnInput(lastSimulationTurnInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float ToSimulationTurnInput(float networkTurnInput)
|
||||||
|
{
|
||||||
|
return -networkTurnInput;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -223,14 +223,23 @@ namespace Tests.EditMode.Network
|
||||||
TurnInput = 0f,
|
TurnInput = 0f,
|
||||||
ThrottleInput = 1f
|
ThrottleInput = 1f
|
||||||
});
|
});
|
||||||
predictionBuffer.AccumulateLatest(0.1f);
|
predictionBuffer.Record(new MoveInput
|
||||||
|
{
|
||||||
|
PlayerId = "player-1",
|
||||||
|
Tick = 2,
|
||||||
|
TurnInput = 0f,
|
||||||
|
ThrottleInput = 1f
|
||||||
|
});
|
||||||
|
predictionBuffer.MarkInputSimulated(1, 0.05f);
|
||||||
|
predictionBuffer.MarkInputSimulated(2, 0.05f);
|
||||||
|
|
||||||
resolver.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
resolver.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
||||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 1, Vector3.zero, acknowledgedMoveTick: 0)));
|
GameplayFlowTestSupport.CreatePlayerState("player-1", 1, Vector3.zero, acknowledgedMoveTick: 0)));
|
||||||
|
|
||||||
Assert.That(GetPrivateVector3(resolver, "_predictedPosition").z, Is.EqualTo(1f).Within(0.0001f));
|
Assert.That(GetPrivateVector3(resolver, "_predictedPosition").z, Is.EqualTo(1f).Within(0.0001f));
|
||||||
Assert.That(predictionBuffer.PendingInputs.Count, Is.EqualTo(1));
|
Assert.That(predictionBuffer.PendingInputs.Count, Is.EqualTo(2));
|
||||||
Assert.That(predictionBuffer.PendingInputs[0].Input.Tick, Is.EqualTo(1));
|
Assert.That(predictionBuffer.PendingInputs[0].Input.Tick, Is.EqualTo(1));
|
||||||
|
Assert.That(predictionBuffer.PendingInputs[1].Input.Tick, Is.EqualTo(2));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ namespace Tests.EditMode.Network
|
||||||
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, ThrottleInput = 1f });
|
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, ThrottleInput = 1f });
|
||||||
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 11, ThrottleInput = 1f });
|
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 11, ThrottleInput = 1f });
|
||||||
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 12, ThrottleInput = 1f });
|
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 12, ThrottleInput = 1f });
|
||||||
|
buffer.MarkInputSimulated(12, 0.05f);
|
||||||
|
|
||||||
var accepted = buffer.TryApplyAuthoritativeState(
|
var accepted = buffer.TryApplyAuthoritativeState(
|
||||||
new PlayerState { PlayerId = "player-1", Tick = 11, AcknowledgedMoveTick = 11 },
|
new PlayerState { PlayerId = "player-1", Tick = 11, AcknowledgedMoveTick = 11 },
|
||||||
|
|
@ -100,10 +101,25 @@ namespace Tests.EditMode.Network
|
||||||
Assert.That(buffer.LastAcknowledgedMoveTick, Is.EqualTo(11));
|
Assert.That(buffer.LastAcknowledgedMoveTick, Is.EqualTo(11));
|
||||||
Assert.That(replayInputs.Count, Is.EqualTo(1));
|
Assert.That(replayInputs.Count, Is.EqualTo(1));
|
||||||
Assert.That(replayInputs[0].Input.Tick, Is.EqualTo(12));
|
Assert.That(replayInputs[0].Input.Tick, Is.EqualTo(12));
|
||||||
Assert.That(replayInputs[0].SimulatedDurationSeconds, Is.EqualTo(0f));
|
Assert.That(replayInputs[0].SimulatedDurationSeconds, Is.EqualTo(0.05f).Within(0.0001f));
|
||||||
Assert.That(buffer.PendingInputs.Count, Is.EqualTo(1));
|
Assert.That(buffer.PendingInputs.Count, Is.EqualTo(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ClientPredictionBuffer_TryGetNextUnsimulatedInput_UsesOldestPendingMoveInput()
|
||||||
|
{
|
||||||
|
var buffer = new ClientPredictionBuffer();
|
||||||
|
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, ThrottleInput = 1f });
|
||||||
|
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 11, ThrottleInput = -1f });
|
||||||
|
buffer.MarkInputSimulated(10, 0.05f);
|
||||||
|
|
||||||
|
var found = buffer.TryGetNextUnsimulatedInput(out var nextInput);
|
||||||
|
|
||||||
|
Assert.That(found, Is.True);
|
||||||
|
Assert.That(nextInput.Input.Tick, Is.EqualTo(11));
|
||||||
|
Assert.That(nextInput.SimulatedDurationSeconds, Is.EqualTo(0f));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void ClientPredictionBuffer_StaleAuthoritativeState_IsIgnored()
|
public void ClientPredictionBuffer_StaleAuthoritativeState_IsIgnored()
|
||||||
{
|
{
|
||||||
|
|
@ -802,10 +818,16 @@ namespace Tests.EditMode.Network
|
||||||
rotation = Quaternion.identity;
|
rotation = Quaternion.identity;
|
||||||
for (var i = 0; i < steps; i++)
|
for (var i = 0; i < steps; i++)
|
||||||
{
|
{
|
||||||
TankMovementKinematics.ApplyStep(10, turnInput, throttleInput, stepDuration, ref position, ref rotation);
|
TankMovementKinematics.ApplyStep(10, ToSimulationTurnInput(turnInput), throttleInput, stepDuration,
|
||||||
|
ref position, ref rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static float ToSimulationTurnInput(float networkTurnInput)
|
||||||
|
{
|
||||||
|
return -networkTurnInput;
|
||||||
|
}
|
||||||
|
|
||||||
private static void ResetMovementState(Rigidbody rigidbody, Vector3 position, Quaternion rotation)
|
private static void ResetMovementState(Rigidbody rigidbody, Vector3 position, Quaternion rotation)
|
||||||
{
|
{
|
||||||
rigidbody.position = position;
|
rigidbody.position = position;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue