process TODO.md step4
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using Network.Defines;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
public sealed class ClientAuthoritativePlayerState
|
||||
{
|
||||
public ClientAuthoritativePlayerStateSnapshot Current { get; private set; }
|
||||
|
||||
public bool TryAccept(PlayerState state, out ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(state));
|
||||
}
|
||||
|
||||
if (Current != null && state.Tick <= Current.Tick)
|
||||
{
|
||||
snapshot = Current;
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot = new ClientAuthoritativePlayerStateSnapshot(state);
|
||||
Current = snapshot;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ClientAuthoritativePlayerStateSnapshot
|
||||
{
|
||||
public ClientAuthoritativePlayerStateSnapshot(PlayerState state)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(state));
|
||||
}
|
||||
|
||||
SourceState = state.Clone();
|
||||
PlayerId = SourceState.PlayerId ?? string.Empty;
|
||||
Tick = SourceState.Tick;
|
||||
Position = SourceState.Position != null ? SourceState.Position.ToVector3() : Vector3.zero;
|
||||
Velocity = SourceState.Velocity != null ? SourceState.Velocity.ToVector3() : Vector3.zero;
|
||||
Rotation = SourceState.Rotation;
|
||||
Hp = SourceState.Hp;
|
||||
}
|
||||
|
||||
public PlayerState SourceState { get; }
|
||||
|
||||
public string PlayerId { get; }
|
||||
|
||||
public long Tick { get; }
|
||||
|
||||
public Vector3 Position { get; }
|
||||
|
||||
public Vector3 Velocity { get; }
|
||||
|
||||
public float Rotation { get; }
|
||||
|
||||
public int Hp { get; }
|
||||
|
||||
public Quaternion RotationQuaternion => Quaternion.Euler(0f, Rotation, 0f);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 973b463b11350f445801c3673c585625
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -63,16 +63,14 @@ public class MovementComponent : MonoBehaviour
|
||||
|
||||
private Vector3 _serverPosition;
|
||||
private bool _hasServerState = false;
|
||||
private PlayerState _lastServerState;
|
||||
private ClientAuthoritativePlayerStateSnapshot _lastAuthoritativeState;
|
||||
|
||||
public long Tick { get; private set; } = 0;
|
||||
private long _startTickOffset = 0;
|
||||
private long _currentTickOffset = 0;
|
||||
private readonly ClientPredictionBuffer _predictionBuffer = new ClientPredictionBuffer();
|
||||
|
||||
private Vector3 _serverPos;
|
||||
private Vector3 _currentPos;
|
||||
private float _lerpTime;
|
||||
private readonly RemotePlayerSnapshotInterpolator _remoteSnapshotInterpolator = new();
|
||||
[SerializeField] private float _lerpRate = 0.1f;
|
||||
private Vector3 _cachedMoveInput;
|
||||
private Vector3 _lastAimDirection = Vector3.forward;
|
||||
@@ -88,7 +86,7 @@ public class MovementComponent : MonoBehaviour
|
||||
_rigid.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
_rigid.isKinematic = !isControlled;
|
||||
_rigid.velocity = Vector3.zero;
|
||||
if (serverTick != 0 && _isControlled) MainUI.Instance.OnStartTickOffsetChanged(serverTick);
|
||||
if (serverTick != 0 && _isControlled && MainUI.Instance != null) MainUI.Instance.OnStartTickOffsetChanged(serverTick);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -138,8 +136,12 @@ public class MovementComponent : MonoBehaviour
|
||||
{
|
||||
if (_hasServerState)
|
||||
{
|
||||
MainUI.Instance.OnServerPosChanged(_serverPosition);
|
||||
Reconcile(_lastServerState);
|
||||
if (MainUI.Instance != null)
|
||||
{
|
||||
MainUI.Instance.OnServerPosChanged(_serverPosition);
|
||||
}
|
||||
|
||||
Reconcile(_lastAuthoritativeState);
|
||||
_hasServerState = false;
|
||||
}
|
||||
|
||||
@@ -147,21 +149,27 @@ public class MovementComponent : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
_lerpTime += Time.fixedDeltaTime / 0.05f;
|
||||
_rigid.MovePosition(Vector3.Lerp(_currentPos, _serverPos, _lerpTime));
|
||||
var sample = _remoteSnapshotInterpolator.Sample(Time.time);
|
||||
if (sample.HasValue)
|
||||
{
|
||||
_rigid.MovePosition(sample.Position);
|
||||
_rigid.MoveRotation(sample.Rotation);
|
||||
_rigid.velocity = sample.Velocity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Reconcile(PlayerState state)
|
||||
private void Reconcile(ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
if (!_predictionBuffer.TryApplyAuthoritativeState(state, out var replayInputs))
|
||||
if (!_predictionBuffer.TryApplyAuthoritativeState(snapshot.SourceState, out var replayInputs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_serverPosition = state.Position.ToVector3();
|
||||
_serverPosition = snapshot.Position;
|
||||
_rigid.position = Vector3.Lerp(_rigid.position, _serverPosition, _lerpRate);
|
||||
_rigid.velocity = Vector3.zero;
|
||||
_rigid.rotation = Quaternion.Slerp(_rigid.rotation, snapshot.RotationQuaternion, _lerpRate);
|
||||
_rigid.velocity = snapshot.Velocity;
|
||||
ReplayPendingInputs(replayInputs);
|
||||
}
|
||||
|
||||
@@ -197,34 +205,24 @@ public class MovementComponent : MonoBehaviour
|
||||
_rigid.velocity = _speed * input;
|
||||
if (_isControlled)
|
||||
{
|
||||
MainUI.Instance.OnClientPosChanged(_rigid.position);
|
||||
if (MainUI.Instance != null)
|
||||
{
|
||||
MainUI.Instance.OnClientPosChanged(_rigid.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnServerState(PlayerState state)
|
||||
public void OnAuthoritativeState(ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
if (_isControlled)
|
||||
{
|
||||
if (_predictionBuffer.LastAuthoritativeTick.HasValue &&
|
||||
state.Tick <= _predictionBuffer.LastAuthoritativeTick.Value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastServerState = state;
|
||||
_lastAuthoritativeState = snapshot;
|
||||
_hasServerState = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_lastServerState != null && state.Tick < _lastServerState.Tick)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastServerState = state;
|
||||
_serverPos = state.Position.ToVector3();
|
||||
_currentPos = _rigid.position;
|
||||
_lerpTime = 0f;
|
||||
_lastAuthoritativeState = snapshot;
|
||||
_remoteSnapshotInterpolator.TryAddSnapshot(snapshot, Time.time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +231,10 @@ public class MovementComponent : MonoBehaviour
|
||||
_currentTickOffset = serverTick - Tick - _startTickOffset;
|
||||
if (_isControlled)
|
||||
{
|
||||
MainUI.Instance.OnServerTickChanged(serverTick);
|
||||
if (MainUI.Instance != null)
|
||||
{
|
||||
MainUI.Instance.OnServerTickChanged(serverTick);
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentTickOffset < 0)
|
||||
@@ -255,7 +256,10 @@ public class MovementComponent : MonoBehaviour
|
||||
|
||||
if (_isControlled)
|
||||
{
|
||||
MainUI.Instance.OnClientPosChanged(_rigid.position);
|
||||
if (MainUI.Instance != null)
|
||||
{
|
||||
MainUI.Instance.OnClientPosChanged(_rigid.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ public class Player : MonoBehaviour
|
||||
{
|
||||
//[SerializeField] private float _moveSpeed = 10f;
|
||||
public string PlayerId { get; private set; } = "1001";
|
||||
public ClientAuthoritativePlayerStateSnapshot AuthoritativeState => _authoritativeState.Current;
|
||||
[SerializeField] private MeshRenderer _meshRenderer;
|
||||
[SerializeField] private Material[] _materials;
|
||||
[SerializeField] private Camera _camera;
|
||||
[SerializeField] private MovementComponent _movement;
|
||||
[SerializeField] private PlayerUI _playerUI;
|
||||
[SerializeField] private bool _isControlled;
|
||||
private readonly ClientAuthoritativePlayerState _authoritativeState = new();
|
||||
|
||||
public void LocalInit(string playerId, int speed, long serverTick)
|
||||
{
|
||||
@@ -49,8 +51,13 @@ public class Player : MonoBehaviour
|
||||
|
||||
public void SyncPosition(PlayerState movement)
|
||||
{
|
||||
if (this._movement == null) return;
|
||||
_movement.OnServerState(movement);
|
||||
if (!_authoritativeState.TryAccept(movement, out var snapshot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_playerUI?.SyncAuthoritativeState(snapshot);
|
||||
_movement?.OnAuthoritativeState(snapshot);
|
||||
}
|
||||
|
||||
public void SyncTick(long serverTick)
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
public sealed class RemotePlayerSnapshotInterpolator
|
||||
{
|
||||
// Keep remote rendering roughly two movement send intervals behind receive time so the client
|
||||
// usually has both an older and newer authoritative sample to interpolate between.
|
||||
public const float DefaultInterpolationDelaySeconds = 0.1f;
|
||||
public const int DefaultMaxBufferedSnapshots = 6;
|
||||
|
||||
private readonly List<BufferedSnapshot> _snapshots = new();
|
||||
private readonly float _interpolationDelaySeconds;
|
||||
private readonly int _maxBufferedSnapshots;
|
||||
|
||||
public RemotePlayerSnapshotInterpolator(
|
||||
float interpolationDelaySeconds = DefaultInterpolationDelaySeconds,
|
||||
int maxBufferedSnapshots = DefaultMaxBufferedSnapshots)
|
||||
{
|
||||
if (interpolationDelaySeconds < 0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(interpolationDelaySeconds));
|
||||
}
|
||||
|
||||
if (maxBufferedSnapshots < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxBufferedSnapshots));
|
||||
}
|
||||
|
||||
_interpolationDelaySeconds = interpolationDelaySeconds;
|
||||
_maxBufferedSnapshots = maxBufferedSnapshots;
|
||||
}
|
||||
|
||||
public int BufferedSnapshotCount => _snapshots.Count;
|
||||
|
||||
public long LatestBufferedTick => _snapshots.Count == 0 ? -1 : _snapshots[^1].Snapshot.Tick;
|
||||
|
||||
public bool TryAddSnapshot(ClientAuthoritativePlayerStateSnapshot snapshot, float receivedAtSeconds)
|
||||
{
|
||||
if (snapshot == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(snapshot));
|
||||
}
|
||||
|
||||
if (_snapshots.Count > 0 && snapshot.Tick <= _snapshots[^1].Snapshot.Tick)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_snapshots.Add(new BufferedSnapshot(snapshot, receivedAtSeconds));
|
||||
TrimOverflow();
|
||||
return true;
|
||||
}
|
||||
|
||||
public RemotePlayerInterpolationSample Sample(float nowSeconds)
|
||||
{
|
||||
if (_snapshots.Count == 0)
|
||||
{
|
||||
return RemotePlayerInterpolationSample.None;
|
||||
}
|
||||
|
||||
// Sample against a fixed delayed render timestamp. If the delay cannot be bracketed by two
|
||||
// buffered authoritative samples, clamp to the newest accepted snapshot instead of predicting.
|
||||
var renderTime = nowSeconds - _interpolationDelaySeconds;
|
||||
TrimConsumedSamples(renderTime);
|
||||
|
||||
if (_snapshots.Count >= 2)
|
||||
{
|
||||
var from = _snapshots[0];
|
||||
var to = _snapshots[1];
|
||||
if (from.ReceivedAtSeconds <= renderTime && renderTime <= to.ReceivedAtSeconds)
|
||||
{
|
||||
var duration = to.ReceivedAtSeconds - from.ReceivedAtSeconds;
|
||||
var t = duration <= Mathf.Epsilon ? 1f : Mathf.Clamp01((renderTime - from.ReceivedAtSeconds) / duration);
|
||||
return RemotePlayerInterpolationSample.Interpolated(
|
||||
Vector3.Lerp(from.Snapshot.Position, to.Snapshot.Position, t),
|
||||
Quaternion.Slerp(from.Snapshot.RotationQuaternion, to.Snapshot.RotationQuaternion, t),
|
||||
Vector3.Lerp(from.Snapshot.Velocity, to.Snapshot.Velocity, t),
|
||||
to.Snapshot,
|
||||
from.Snapshot,
|
||||
t);
|
||||
}
|
||||
}
|
||||
|
||||
return RemotePlayerInterpolationSample.Latest(_snapshots[^1].Snapshot);
|
||||
}
|
||||
|
||||
private void TrimConsumedSamples(float renderTime)
|
||||
{
|
||||
while (_snapshots.Count >= 2 && _snapshots[1].ReceivedAtSeconds <= renderTime)
|
||||
{
|
||||
_snapshots.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void TrimOverflow()
|
||||
{
|
||||
while (_snapshots.Count > _maxBufferedSnapshots)
|
||||
{
|
||||
_snapshots.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct BufferedSnapshot
|
||||
{
|
||||
public BufferedSnapshot(ClientAuthoritativePlayerStateSnapshot snapshot, float receivedAtSeconds)
|
||||
{
|
||||
Snapshot = snapshot;
|
||||
ReceivedAtSeconds = receivedAtSeconds;
|
||||
}
|
||||
|
||||
public ClientAuthoritativePlayerStateSnapshot Snapshot { get; }
|
||||
|
||||
public float ReceivedAtSeconds { get; }
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct RemotePlayerInterpolationSample
|
||||
{
|
||||
private RemotePlayerInterpolationSample(
|
||||
bool hasValue,
|
||||
bool usedInterpolation,
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
Vector3 velocity,
|
||||
ClientAuthoritativePlayerStateSnapshot latestSnapshot,
|
||||
ClientAuthoritativePlayerStateSnapshot fromSnapshot,
|
||||
float alpha)
|
||||
{
|
||||
HasValue = hasValue;
|
||||
UsedInterpolation = usedInterpolation;
|
||||
Position = position;
|
||||
Rotation = rotation;
|
||||
Velocity = velocity;
|
||||
LatestSnapshot = latestSnapshot;
|
||||
FromSnapshot = fromSnapshot;
|
||||
Alpha = alpha;
|
||||
}
|
||||
|
||||
public static RemotePlayerInterpolationSample None { get; } =
|
||||
new(false, false, Vector3.zero, Quaternion.identity, Vector3.zero, null, null, 0f);
|
||||
|
||||
public bool HasValue { get; }
|
||||
|
||||
public bool UsedInterpolation { get; }
|
||||
|
||||
public Vector3 Position { get; }
|
||||
|
||||
public Quaternion Rotation { get; }
|
||||
|
||||
public Vector3 Velocity { get; }
|
||||
|
||||
public ClientAuthoritativePlayerStateSnapshot LatestSnapshot { get; }
|
||||
|
||||
public ClientAuthoritativePlayerStateSnapshot FromSnapshot { get; }
|
||||
|
||||
public float Alpha { get; }
|
||||
|
||||
public static RemotePlayerInterpolationSample Latest(ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
return new RemotePlayerInterpolationSample(
|
||||
true,
|
||||
false,
|
||||
snapshot.Position,
|
||||
snapshot.RotationQuaternion,
|
||||
snapshot.Velocity,
|
||||
snapshot,
|
||||
snapshot,
|
||||
1f);
|
||||
}
|
||||
|
||||
public static RemotePlayerInterpolationSample Interpolated(
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
Vector3 velocity,
|
||||
ClientAuthoritativePlayerStateSnapshot latestSnapshot,
|
||||
ClientAuthoritativePlayerStateSnapshot fromSnapshot,
|
||||
float alpha)
|
||||
{
|
||||
return new RemotePlayerInterpolationSample(
|
||||
true,
|
||||
true,
|
||||
position,
|
||||
rotation,
|
||||
velocity,
|
||||
latestSnapshot,
|
||||
fromSnapshot,
|
||||
alpha);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c88aa7375c5dd94882fe09f5208af9b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -14,7 +14,12 @@ public class PlayerUI : MonoBehaviour
|
||||
_canvas = this.transform.GetComponent<Canvas>();
|
||||
_mainCamera = Camera.main;
|
||||
this._master = master;
|
||||
this._text.text = _master.PlayerId;
|
||||
RefreshText();
|
||||
}
|
||||
|
||||
public void SyncAuthoritativeState(ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
RefreshText(snapshot);
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
@@ -41,4 +46,20 @@ public class PlayerUI : MonoBehaviour
|
||||
{
|
||||
_isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshText(ClientAuthoritativePlayerStateSnapshot snapshot = null)
|
||||
{
|
||||
if (_text == null || _master == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (snapshot == null)
|
||||
{
|
||||
_text.text = _master.PlayerId;
|
||||
return;
|
||||
}
|
||||
|
||||
_text.text = $"{_master.PlayerId}\nHP:{snapshot.Hp} Tick:{snapshot.Tick}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user