补充输入重放
输入重放是指当收到一个服务器状态包时以那个包为初始状态,然后重新计算所有未确认的输入来得到当前帧的状态,然后再对当前角色的位置进行插值移动。服务器状态包只能给出前几帧的权威数据,而当前帧的状态则需要通过输入回放来得到
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Network.Defines;
|
||||
using UnityEngine;
|
||||
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>
|
||||
/// 输入组件,负责从 IInputSource 获取输入、发送 MoveInput 到服务器、管理 tick
|
||||
/// Update() 是唯一调用 SendMoveInput / SendShootInput 的地方
|
||||
/// </summary>
|
||||
public class InputComponent : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private string _playerId = "player";
|
||||
[SerializeField] private float _sendInterval = 0.05f; // 50ms 发送间隔
|
||||
|
||||
private IInputSource _inputSource;
|
||||
private Vector3 _currentInput;
|
||||
private Vector3 _lastAimDirection = Vector3.forward;
|
||||
private float _lastSendTime;
|
||||
private bool _stopMessagePending;
|
||||
private bool _wasMovingLastFrame;
|
||||
private long _tick;
|
||||
|
||||
public event System.Action<MoveInput> OnMoveInputCreated;
|
||||
public event System.Action<ShootInput> OnShootInputCreated;
|
||||
|
||||
public long CurrentTick => _tick;
|
||||
|
||||
/// <summary>
|
||||
/// 设置玩家 ID(由 MovementComponent.Init 调用)
|
||||
/// </summary>
|
||||
public void InjectPlayerId(string playerId)
|
||||
{
|
||||
_playerId = playerId;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
var camera = Camera.main;
|
||||
_inputSource = new UnityInputSource(camera?.transform);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置自定义输入源(用于替换默认的 UnityInputSource)
|
||||
/// </summary>
|
||||
public void SetInputSource(IInputSource source)
|
||||
{
|
||||
_inputSource = source ?? throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前输入源(用于测试)
|
||||
/// </summary>
|
||||
public IInputSource GetInputSource()
|
||||
{
|
||||
return _inputSource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置 tick(用于测试)
|
||||
/// </summary>
|
||||
public void ResetTick(long tick = 0)
|
||||
{
|
||||
_tick = tick;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 从输入源获取输入
|
||||
_currentInput = _inputSource?.GetPlanarInput() ?? Vector3.zero;
|
||||
|
||||
// 检测移动状态变化
|
||||
var hasMovement = ClientGameplayInputFlow.HasPlanarInput(_currentInput);
|
||||
if (hasMovement)
|
||||
{
|
||||
_stopMessagePending = false;
|
||||
}
|
||||
else if (_wasMovingLastFrame)
|
||||
{
|
||||
_stopMessagePending = true;
|
||||
}
|
||||
|
||||
_wasMovingLastFrame = hasMovement;
|
||||
|
||||
// 处理射击输入
|
||||
var shootInput = GetShootInput();
|
||||
if (shootInput != null && NetworkManager.Instance != null)
|
||||
{
|
||||
NetworkManager.Instance.SendShootInput(shootInput);
|
||||
OnShootInputCreated?.Invoke(shootInput);
|
||||
}
|
||||
|
||||
// 定期发送移动输入
|
||||
if (Time.time - _lastSendTime > _sendInterval)
|
||||
{
|
||||
SendMoveInput();
|
||||
}
|
||||
}
|
||||
|
||||
private void SendMoveInput()
|
||||
{
|
||||
if (!ClientGameplayInputFlow.TryCreateMoveInput(_playerId, _tick, _currentInput,
|
||||
_stopMessagePending, out var moveInput))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (NetworkManager.Instance != null)
|
||||
{
|
||||
NetworkManager.Instance.SendMoveInput(moveInput);
|
||||
}
|
||||
|
||||
OnMoveInputCreated?.Invoke(moveInput);
|
||||
_stopMessagePending = false;
|
||||
_lastSendTime = Time.time;
|
||||
_tick++;
|
||||
}
|
||||
|
||||
private ShootInput GetShootInput()
|
||||
{
|
||||
var shootTriggered = _inputSource?.ConsumeShootInput() ?? false;
|
||||
var aimDirection = _inputSource?.GetAimDirection() ?? Vector3.forward;
|
||||
|
||||
if (!shootTriggered)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var planarForward = Vector3.ProjectOnPlane(aimDirection, Vector3.up);
|
||||
if (ClientGameplayInputFlow.HasPlanarInput(planarForward))
|
||||
{
|
||||
_lastAimDirection = planarForward;
|
||||
}
|
||||
|
||||
return ClientGameplayInputFlow.CreateShootInput(_playerId, _tick, _lastAimDirection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76493c52bd37a4a4db167d10a4dd6369
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,71 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class MovementComponent : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Rigidbody _rigid;
|
||||
private const float InterpolationAlpha = 0.15f;
|
||||
|
||||
private bool _isControlled;
|
||||
private Vector3 _currentPosition;
|
||||
private Quaternion _currentRotation;
|
||||
private Vector3 _targetPosition;
|
||||
private Quaternion _targetRotation;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_rigid ??= GetComponent<Rigidbody>();
|
||||
}
|
||||
|
||||
public void Init(bool isControlled)
|
||||
{
|
||||
_rigid ??= GetComponent<Rigidbody>();
|
||||
_isControlled = isControlled;
|
||||
_rigid.interpolation = isControlled ? RigidbodyInterpolation.None : RigidbodyInterpolation.Interpolate;
|
||||
_rigid.isKinematic = !isControlled;
|
||||
_rigid.velocity = Vector3.zero;
|
||||
_rigid.angularVelocity = Vector3.zero;
|
||||
|
||||
_currentPosition = _rigid.position;
|
||||
_currentRotation = _rigid.rotation;
|
||||
_targetPosition = _rigid.position;
|
||||
_targetRotation = _rigid.rotation;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
_currentPosition = Vector3.Lerp(_currentPosition, _targetPosition, InterpolationAlpha);
|
||||
_currentRotation = Quaternion.Slerp(_currentRotation, _targetRotation, InterpolationAlpha);
|
||||
|
||||
_rigid.position = _currentPosition;
|
||||
_rigid.rotation = _currentRotation;
|
||||
|
||||
if (_isControlled && MainUI.Instance != null)
|
||||
{
|
||||
MainUI.Instance.OnClientPosChanged(_currentPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public Vector3 CurrentPosition => _currentPosition;
|
||||
|
||||
public Quaternion CurrentRotation => _currentRotation;
|
||||
|
||||
public Vector3 TargetPosition => _targetPosition;
|
||||
|
||||
public Quaternion TargetRotation => _targetRotation;
|
||||
|
||||
public void SetTargetPose(Vector3 position, Quaternion rotation)
|
||||
{
|
||||
_targetPosition = position;
|
||||
_targetRotation = rotation;
|
||||
}
|
||||
|
||||
public void SnapToPose(Vector3 position, Quaternion rotation)
|
||||
{
|
||||
_currentPosition = position;
|
||||
_currentRotation = rotation;
|
||||
_targetPosition = position;
|
||||
_targetRotation = rotation;
|
||||
_rigid.position = position;
|
||||
_rigid.rotation = rotation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db8117151f564304bae153aa55c0a960
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,252 @@
|
||||
using System.Collections.Generic;
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
public class MovementResolverComponent : MonoBehaviour
|
||||
{
|
||||
private const float ServerSimulationStepSeconds = 0.05f;
|
||||
private const float SnapThreshold = 0.5f;
|
||||
|
||||
[SerializeField] private int _speed = 2;
|
||||
[SerializeField] private MovementComponent _movement;
|
||||
[SerializeField] private InputComponent _inputComponent;
|
||||
[SerializeField] private bool _applyServerCorrection = true;
|
||||
|
||||
private Player _master;
|
||||
private bool _isControlled;
|
||||
private Vector3 _serverPosition;
|
||||
private ClientAuthoritativePlayerStateSnapshot _lastAuthoritativeState;
|
||||
|
||||
private Vector3 _authoritativePosition;
|
||||
private Quaternion _authoritativeRotation;
|
||||
private Vector3 _predictedPosition;
|
||||
private Quaternion _predictedRotation;
|
||||
|
||||
public long Tick { get; private set; }
|
||||
private long _startTickOffset;
|
||||
private long _currentTickOffset;
|
||||
private float _simulationAccumulator;
|
||||
private readonly ClientPredictionBuffer _predictionBuffer = new ClientPredictionBuffer();
|
||||
private readonly RemotePlayerSnapshotInterpolator _remoteSnapshotInterpolator = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_movement ??= GetComponent<MovementComponent>();
|
||||
_inputComponent ??= GetComponent<InputComponent>();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_movement ??= GetComponent<MovementComponent>();
|
||||
_inputComponent ??= GetComponent<InputComponent>();
|
||||
|
||||
_master = master;
|
||||
_isControlled = isControlled;
|
||||
_speed = speed;
|
||||
_startTickOffset = serverTick;
|
||||
|
||||
if (_movement != null)
|
||||
{
|
||||
_movement.Init(isControlled);
|
||||
_authoritativePosition = _movement.CurrentPosition;
|
||||
_authoritativeRotation = _movement.CurrentRotation;
|
||||
_predictedPosition = _movement.CurrentPosition;
|
||||
_predictedRotation = _movement.CurrentRotation;
|
||||
}
|
||||
|
||||
if (_inputComponent != null && master != null)
|
||||
{
|
||||
_inputComponent.InjectPlayerId(master.PlayerId);
|
||||
}
|
||||
|
||||
if (serverTick != 0 && _isControlled && MainUI.Instance != null)
|
||||
{
|
||||
MainUI.Instance.OnStartTickOffsetChanged(serverTick);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_isControlled && _inputComponent != null && MainUI.Instance != null)
|
||||
{
|
||||
MainUI.Instance.OnClientTickChanged(_inputComponent.CurrentTick);
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_inputComponent != null)
|
||||
{
|
||||
_inputComponent.OnMoveInputCreated += HandleMoveInputCreated;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_inputComponent != null)
|
||||
{
|
||||
_inputComponent.OnMoveInputCreated -= HandleMoveInputCreated;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleMoveInputCreated(MoveInput moveInput)
|
||||
{
|
||||
_predictionBuffer.Record(moveInput);
|
||||
}
|
||||
|
||||
public void SetApplyServerCorrection(bool apply)
|
||||
{
|
||||
_applyServerCorrection = apply;
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (_movement == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isControlled)
|
||||
{
|
||||
_simulationAccumulator += Time.fixedDeltaTime;
|
||||
while (_simulationAccumulator >= ServerSimulationStepSeconds)
|
||||
{
|
||||
var pendingCount = _predictionBuffer.PendingInputs.Count;
|
||||
if (pendingCount == 0)
|
||||
{
|
||||
_simulationAccumulator = 0f;
|
||||
break;
|
||||
}
|
||||
|
||||
Simulate(GetLatestPredictedInput());
|
||||
_simulationAccumulator -= ServerSimulationStepSeconds;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var sample = _remoteSnapshotInterpolator.Sample(Time.time);
|
||||
if (sample.HasValue)
|
||||
{
|
||||
_movement.SnapToPose(sample.Position, sample.Rotation);
|
||||
}
|
||||
}
|
||||
|
||||
private void Simulate(Vector3 input)
|
||||
{
|
||||
TankMovementKinematics.ApplyStep(_speed, input.x, input.z, ServerSimulationStepSeconds,
|
||||
ref _predictedPosition, ref _predictedRotation);
|
||||
_movement.SetTargetPose(_predictedPosition, _predictedRotation);
|
||||
_predictionBuffer.AccumulateLatest(ServerSimulationStepSeconds);
|
||||
|
||||
if (MainUI.Instance != null)
|
||||
{
|
||||
MainUI.Instance.OnClientPosChanged(_movement.CurrentPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAuthoritativeState(ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
if (_isControlled)
|
||||
{
|
||||
if (!_applyServerCorrection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastAuthoritativeState = snapshot;
|
||||
Reconcile(snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
_lastAuthoritativeState = snapshot;
|
||||
_remoteSnapshotInterpolator.TryAddSnapshot(snapshot, Time.time);
|
||||
}
|
||||
|
||||
private void Reconcile(ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
_serverPosition = snapshot.Position;
|
||||
if (!_predictionBuffer.TryApplyAuthoritativeState(snapshot.SourceState, Time.time, out var replayInputs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_authoritativePosition = snapshot.Position;
|
||||
_authoritativeRotation = snapshot.RotationQuaternion;
|
||||
_predictedPosition = _authoritativePosition;
|
||||
_predictedRotation = _authoritativeRotation;
|
||||
|
||||
ReplayPendingInputs(replayInputs);
|
||||
|
||||
var error = Vector3.Distance(_movement.CurrentPosition, _predictedPosition);
|
||||
if (error > SnapThreshold)
|
||||
{
|
||||
_movement.SnapToPose(_predictedPosition, _predictedRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
_movement.SetTargetPose(_predictedPosition, _predictedRotation);
|
||||
}
|
||||
|
||||
_simulationAccumulator = 0f;
|
||||
|
||||
if (MainUI.Instance != null)
|
||||
{
|
||||
MainUI.Instance.OnServerPosChanged(_serverPosition);
|
||||
MainUI.Instance.OnCorrectionMagnitudeChanged?.Invoke(
|
||||
_predictedPosition,
|
||||
snapshot.Position,
|
||||
error,
|
||||
Quaternion.Angle(_predictedRotation, snapshot.RotationQuaternion));
|
||||
MainUI.Instance.OnAcknowledgedMoveTickChanged?.Invoke(_predictionBuffer.LastAcknowledgedMoveTick ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetServerTick(long serverTick)
|
||||
{
|
||||
_currentTickOffset = serverTick - Tick - _startTickOffset;
|
||||
if (_isControlled && MainUI.Instance != null)
|
||||
{
|
||||
MainUI.Instance.OnServerTickChanged(serverTick);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
foreach (var replayInput in replayInputs)
|
||||
{
|
||||
var remaining = replayInput.SimulatedDurationSeconds;
|
||||
while (remaining > 0f)
|
||||
{
|
||||
var step = Mathf.Min(remaining, ServerSimulationStepSeconds);
|
||||
TankMovementKinematics.ApplyStep(
|
||||
_speed,
|
||||
replayInput.Input.TurnInput,
|
||||
replayInput.Input.ThrottleInput,
|
||||
step,
|
||||
ref _predictedPosition,
|
||||
ref _predictedRotation);
|
||||
remaining -= step;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fcfebc0989c4bf04cacf1c633d49a8bb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,96 @@
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using UnityEngine;
|
||||
|
||||
public class Player : MonoBehaviour
|
||||
{
|
||||
//[SerializeField] private float _moveSpeed = 10f;
|
||||
public string PlayerId { get; private set; } = "1001";
|
||||
public ClientAuthoritativePlayerStateSnapshot AuthoritativeState => _authoritativeState.Current;
|
||||
public ClientCombatPresentationSnapshot CombatPresentation => _authoritativeState.CombatPresentation;
|
||||
[SerializeField] private MeshRenderer _meshRenderer;
|
||||
[SerializeField] private Material[] _materials;
|
||||
[SerializeField] private Camera _camera;
|
||||
[SerializeField] private MovementComponent _movement;
|
||||
[SerializeField] private MovementResolverComponent _movementResolver;
|
||||
[SerializeField] private PlayerUI _playerUI;
|
||||
[SerializeField] private bool _isControlled;
|
||||
private readonly ClientAuthoritativePlayerState _authoritativeState = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_movement ??= GetComponent<MovementComponent>();
|
||||
if (_movement == null)
|
||||
{
|
||||
_movement = gameObject.AddComponent<MovementComponent>();
|
||||
}
|
||||
|
||||
_movementResolver ??= GetComponent<MovementResolverComponent>();
|
||||
if (_movementResolver == null)
|
||||
{
|
||||
_movementResolver = gameObject.AddComponent<MovementResolverComponent>();
|
||||
}
|
||||
}
|
||||
|
||||
public void LocalInit(string playerId, ClientMovementBootstrap bootstrap)
|
||||
{
|
||||
this.PlayerId = playerId;
|
||||
this._isControlled = true;
|
||||
|
||||
int idx = Random.Range(0, _materials.Length);
|
||||
_meshRenderer.material = _materials[idx];
|
||||
|
||||
_playerUI.Init(this);
|
||||
_movementResolver.Init(true, this, bootstrap);
|
||||
}
|
||||
|
||||
public void RemoteInit(string playerId, UnityEngine.Vector3 pos)
|
||||
{
|
||||
this.PlayerId = playerId;
|
||||
this._isControlled = false;
|
||||
|
||||
int idx = Random.Range(0, _materials.Length);
|
||||
_meshRenderer.material = _materials[idx];
|
||||
|
||||
Destroy(_camera.gameObject);
|
||||
this.transform.position = pos;
|
||||
|
||||
_playerUI.Init(this);
|
||||
_movementResolver.Init(false, this);
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(this.PlayerId) && _isControlled)
|
||||
{
|
||||
NetworkManager.Instance.SendLogoutRequest(this.PlayerId);
|
||||
}
|
||||
}
|
||||
|
||||
public void SyncPosition(PlayerState movement)
|
||||
{
|
||||
if (!_authoritativeState.TryAccept(movement, out var snapshot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_playerUI?.SyncAuthoritativeState(snapshot, CombatPresentation);
|
||||
_movementResolver?.OnAuthoritativeState(snapshot);
|
||||
}
|
||||
|
||||
public bool ApplyCombatEvent(CombatEvent combatEvent)
|
||||
{
|
||||
if (!_authoritativeState.TryApplyCombatEvent(combatEvent, PlayerId, out var snapshot, out var combatSnapshot))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_playerUI?.SyncAuthoritativeState(snapshot, combatSnapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SyncTick(long serverTick)
|
||||
{
|
||||
_movementResolver.SetServerTick(serverTick);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef97bb6040fda094b8aeab1bff8f13f8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
using UnityEngine;
|
||||
|
||||
public static class TankMovementKinematics
|
||||
{
|
||||
private const float TurnSpeedDegreesPerSecond = 180f;
|
||||
private const float UnityYawOffsetDegrees = 90f;
|
||||
|
||||
public static void ApplyStep(int speed, float turnInput, float throttleInput, float deltaTime,
|
||||
ref Vector3 position, ref Quaternion rotation)
|
||||
{
|
||||
if (deltaTime <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var clampedTurnInput = Mathf.Clamp(turnInput, -1f, 1f);
|
||||
var clampedThrottleInput = Mathf.Clamp(throttleInput, -1f, 1f);
|
||||
var heading = NormalizeDegrees(UnityYawToHeading(rotation.eulerAngles.y) +
|
||||
(clampedTurnInput * TurnSpeedDegreesPerSecond * deltaTime));
|
||||
rotation = Quaternion.Euler(0f, HeadingToUnityYaw(heading), 0f);
|
||||
|
||||
var forward = ResolveHeadingForward(heading);
|
||||
var velocity = forward * (clampedThrottleInput * speed);
|
||||
position += velocity * deltaTime;
|
||||
}
|
||||
|
||||
public static Vector3 ResolveHeadingForward(float headingDegrees)
|
||||
{
|
||||
var rotationRadians = headingDegrees * Mathf.Deg2Rad;
|
||||
return new Vector3(Mathf.Cos(rotationRadians), 0f, Mathf.Sin(rotationRadians));
|
||||
}
|
||||
|
||||
public static float HeadingToUnityYaw(float headingDegrees)
|
||||
{
|
||||
return NormalizeDegrees(UnityYawOffsetDegrees - headingDegrees);
|
||||
}
|
||||
|
||||
public static float UnityYawToHeading(float unityYawDegrees)
|
||||
{
|
||||
return NormalizeDegrees(UnityYawOffsetDegrees - unityYawDegrees);
|
||||
}
|
||||
|
||||
public static float NormalizeDegrees(float degrees)
|
||||
{
|
||||
var normalized = degrees % 360f;
|
||||
if (normalized < 0f)
|
||||
{
|
||||
normalized += 360f;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22558181f2430ca4b80f5787ec62c68d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user