704 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
		
		
			
		
	
	
			704 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
|  | using Fusion; | ||
|  | using Fusion.Addons.KCC; | ||
|  | using Fusion.Plugin; | ||
|  | 
 | ||
|  | namespace TPSBR | ||
|  | { | ||
|  | 	using System; | ||
|  | 	using UnityEngine; | ||
|  | 	using UnityEngine.InputSystem; | ||
|  | 	using TPSBR.UI; | ||
|  | 
 | ||
|  | 	[DefaultExecutionOrder(-10)] | ||
|  | 	public sealed partial class AgentInput : ContextBehaviour, IBeforeTick, IAfterAllTicks | ||
|  | 	{ | ||
|  | 		// PUBLIC MEMBERS | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Holds input for fixed update. | ||
|  | 		/// </summary> | ||
|  | 		public GameplayInput FixedInput { get { CheckFixedAccess(false); return _fixedInput; } } | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Holds input for current frame render update. | ||
|  | 		/// </summary> | ||
|  | 		public GameplayInput RenderInput { get { CheckRenderAccess(false); return _renderInput; } } | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Holds accumulated inputs from all render frames since last fixed update. Used when Fusion input poll is triggered. | ||
|  | 		/// </summary> | ||
|  | 		public GameplayInput AccumulatedInput { get { CheckRenderAccess(false); return _accumulatedInput; } } | ||
|  | 
 | ||
|  | 		public bool          IsCyclingGrenades => Time.time < _grenadesCyclingStartTime + _grenadesCycleDuration; | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// These actions won't be accumulated and polled by Fusion if they are triggered in the same frame as the simulation. | ||
|  | 		/// They are accumulated after Fusion simulation and before Render(), effectively defering actions to first fixed simulation in following frames. | ||
|  | 		/// This makes fixed and render-predicted movement much more consistent (less prediction correction) at the cost of slight delay. | ||
|  | 		/// </summary> | ||
|  | 		[NonSerialized] | ||
|  | 		public EGameplayInputAction[] DeferredInputActions = new EGameplayInputAction[] { EGameplayInputAction.Attack, EGameplayInputAction.Jump, EGameplayInputAction.ToggleJetpack }; | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// These actions trigger sending interpolation data required for render-accurate lag compensation queries. | ||
|  | 		/// Like DeferredInputActions, these actions won't be accumulated and polled by Fusion if they are triggered in the same frame as the simulation. | ||
|  | 		/// They are accumulated after Fusion simulation and before Render(), effectively defering actions to first fixed simulation in following frames. | ||
|  | 		/// </summary> | ||
|  | 		[NonSerialized] | ||
|  | 		public EGameplayInputAction[] InterpolationDataActions = new EGameplayInputAction[] { EGameplayInputAction.Attack }; | ||
|  | 
 | ||
|  | 		// PRIVATE MEMBERS | ||
|  | 
 | ||
|  | 		[SerializeField] | ||
|  | 		private float _grenadesCycleDuration = 2f; | ||
|  | 		[SerializeField][Range(0.0f, 0.1f)][Tooltip("Look rotation delta for a render frame is calculated as average from all frames within responsivity time.")] | ||
|  | 		private float _lookResponsivity = 0.0f; | ||
|  | 		[SerializeField][Range(0.0f, 1.0f)][Tooltip("How long the last known input is repeated before using default.")] | ||
|  | 		private float _maxRepeatTime = 0.25f; | ||
|  | 		[SerializeField][Tooltip("Outputs missing inputs to console.")] | ||
|  | 		private bool  _logMissingInputs; | ||
|  | 
 | ||
|  | 		// We need to store current input to compare against previous input (to track actions activation/deactivation). It is also reused if the input for current tick is not available. | ||
|  | 		// This is not needed on proxies and will be replicated to input authority only. | ||
|  | 		[Networked] | ||
|  | 		private GameplayInput _fixedInput { get; set; } | ||
|  | 
 | ||
|  | 		private Agent             _agent; | ||
|  | 		private GameplayInput     _renderInput; | ||
|  | 		private GameplayInput     _accumulatedInput; | ||
|  | 		private GameplayInput     _previousFixedInput; | ||
|  | 		private GameplayInput     _previousRenderInput; | ||
|  | 		private GameplayInput     _deferActionsInput; | ||
|  | 		private bool              _useDeferActionsInput; | ||
|  | 		private bool              _updateInterpolationData; | ||
|  | 		private Vector2           _partialMoveDirection; | ||
|  | 		private float             _partialMoveDirectionSize; | ||
|  | 		private Vector2           _accumulatedMoveDirection; | ||
|  | 		private float             _accumulatedMoveDirectionSize; | ||
|  | 		private SmoothVector2     _smoothLookRotationDelta = new SmoothVector2(256); | ||
|  | 		private float             _repeatTime; | ||
|  | 		private float             _lastRenderAlpha; | ||
|  | 		private float             _inputPollDeltaTime; | ||
|  | 		private int               _lastInputPollFrame; | ||
|  | 		private int               _processInputFrame; | ||
|  | 		private int               _missingInputsInRow; | ||
|  | 		private int               _missingInputsTotal; | ||
|  | 		private int               _logMissingInputFromTick; | ||
|  | 		private float             _grenadesCyclingStartTime; | ||
|  | 		private UIMobileInputView _mobileInputView; | ||
|  | 
 | ||
|  | 		// PUBLIC METHODS | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Check if an action is active in current input. FUN/Render input is resolved automatically. | ||
|  | 		/// </summary> | ||
|  | 		public bool HasActive(EGameplayInputAction action) | ||
|  | 		{ | ||
|  | 			if (Runner.Stage != default) | ||
|  | 			{ | ||
|  | 				CheckFixedAccess(false); | ||
|  | 				return action.IsActive(_fixedInput); | ||
|  | 			} | ||
|  | 			else | ||
|  | 			{ | ||
|  | 				CheckRenderAccess(false); | ||
|  | 				return action.IsActive(_renderInput); | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Check if an action was activated in current input. | ||
|  | 		/// In FUN this method compares current fixed input agains previous fixed input. | ||
|  | 		/// In Render this method compares current render input against previous render input OR current fixed input (first Render call after FUN). | ||
|  | 		/// </summary> | ||
|  | 		public bool WasActivated(EGameplayInputAction action) | ||
|  | 		{ | ||
|  | 			if (Runner.Stage != default) | ||
|  | 			{ | ||
|  | 				CheckFixedAccess(false); | ||
|  | 				return action.WasActivated(_fixedInput, _previousFixedInput); | ||
|  | 			} | ||
|  | 			else | ||
|  | 			{ | ||
|  | 				CheckRenderAccess(false); | ||
|  | 				return action.WasActivated(_renderInput, _previousRenderInput); | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Check if an action was activated in custom input. | ||
|  | 		/// In FUN this method compares custom input agains previous fixed input. | ||
|  | 		/// In Render this method compares custom input against previous render input OR current fixed input (first Render call after FUN). | ||
|  | 		/// </summary> | ||
|  | 		public bool WasActivated(EGameplayInputAction action, GameplayInput customInput) | ||
|  | 		{ | ||
|  | 			if (Runner.Stage != default) | ||
|  | 			{ | ||
|  | 				CheckFixedAccess(false); | ||
|  | 				return action.WasActivated(customInput, _previousFixedInput); | ||
|  | 			} | ||
|  | 			else | ||
|  | 			{ | ||
|  | 				CheckRenderAccess(false); | ||
|  | 				return action.WasActivated(customInput, _previousRenderInput); | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Check if an action was deactivated in current input. | ||
|  | 		/// In FUN this method compares current fixed input agains previous fixed input. | ||
|  | 		/// In Render this method compares current render input against previous render input OR current fixed input (first Render call after FUN). | ||
|  | 		/// </summary> | ||
|  | 		public bool WasDeactivated(EGameplayInputAction action) | ||
|  | 		{ | ||
|  | 			if (Runner.Stage != default) | ||
|  | 			{ | ||
|  | 				CheckFixedAccess(false); | ||
|  | 				return action.WasDeactivated(_fixedInput, _previousFixedInput); | ||
|  | 			} | ||
|  | 			else | ||
|  | 			{ | ||
|  | 				CheckRenderAccess(false); | ||
|  | 				return action.WasDeactivated(_renderInput, _previousRenderInput); | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Check if an action was deactivated in custom input. | ||
|  | 		/// In FUN this method compares custom input agains previous fixed input. | ||
|  | 		/// In Render this method compares custom input against previous render input OR current fixed input (first Render call after FUN). | ||
|  | 		/// </summary> | ||
|  | 		public bool WasDeactivated(EGameplayInputAction action, GameplayInput customInput) | ||
|  | 		{ | ||
|  | 			if (Runner.Stage != default) | ||
|  | 			{ | ||
|  | 				CheckFixedAccess(false); | ||
|  | 				return action.WasDeactivated(customInput, _previousFixedInput); | ||
|  | 			} | ||
|  | 			else | ||
|  | 			{ | ||
|  | 				CheckRenderAccess(false); | ||
|  | 				return action.WasDeactivated(customInput, _previousRenderInput); | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Updates fixed input. Use after manipulating with fixed input outside. | ||
|  | 		/// </summary> | ||
|  | 		/// <param name="fixedInput">Input used in fixed update.</param> | ||
|  | 		/// <param name="setPreviousInputs">Updates base fixed input and base render input.</param> | ||
|  | 		public void SetFixedInput(GameplayInput fixedInput, bool setPreviousInputs) | ||
|  | 		{ | ||
|  | 			CheckFixedAccess(true); | ||
|  | 
 | ||
|  | 			_fixedInput = fixedInput; | ||
|  | 
 | ||
|  | 			if (setPreviousInputs == true) | ||
|  | 			{ | ||
|  | 				_previousFixedInput  = fixedInput; | ||
|  | 				_previousRenderInput = fixedInput; | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Updates render input. Use after manipulating with render input outside. | ||
|  | 		/// </summary> | ||
|  | 		/// <param name="renderInput">Input used in render update.</param> | ||
|  | 		/// <param name="setPreviousInput">Updates base render input.</param> | ||
|  | 		public void SetRenderInput(GameplayInput renderInput, bool setPreviousInput) | ||
|  | 		{ | ||
|  | 			CheckRenderAccess(false); | ||
|  | 
 | ||
|  | 			_renderInput = renderInput; | ||
|  | 
 | ||
|  | 			if (setPreviousInput == true) | ||
|  | 			{ | ||
|  | 				_previousRenderInput = renderInput; | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// NetworkBehaviour INTERFACE | ||
|  | 
 | ||
|  | 		public override void Spawned() | ||
|  | 		{ | ||
|  | 			ReplicateToAll(false); | ||
|  | 			ReplicateTo(Object.InputAuthority, true); | ||
|  | 
 | ||
|  | 			SetDefaults(); | ||
|  | 
 | ||
|  | 			// Wait few seconds before the connection is stable to start tracking missing inputs. | ||
|  | 			_logMissingInputFromTick = Runner.Tick + TickRate.Resolve(Runner.Config.Simulation.TickRateSelection).Client * 5; | ||
|  | 
 | ||
|  | 			if (_agent.HasInputAuthority == false) | ||
|  | 				return; | ||
|  | 
 | ||
|  | 			// Register local player input polling. | ||
|  | 			NetworkEvents networkEvents = Runner.GetComponent<NetworkEvents>(); | ||
|  | 			networkEvents.OnInput.RemoveListener(OnInput); | ||
|  | 			networkEvents.OnInput.AddListener(OnInput); | ||
|  | 
 | ||
|  | 			// Hide cursor | ||
|  | 			Context.Input.RequestCursorVisibility(false, ECursorStateSource.Agent); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		public override void Despawned(NetworkRunner runner, bool hasState) | ||
|  | 		{ | ||
|  | 			if (runner != null) | ||
|  | 			{ | ||
|  | 				// Unregister local player input polling. | ||
|  | 				NetworkEvents networkEvents = runner.GetComponent<NetworkEvents>(); | ||
|  | 				networkEvents.OnInput.RemoveListener(OnInput); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			SetDefaults(); | ||
|  | 
 | ||
|  | 			_mobileInputView = default; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		public override void FixedUpdateNetwork() | ||
|  | 		{ | ||
|  | 			_agent.EarlyFixedUpdateNetwork(); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		public override void Render() | ||
|  | 		{ | ||
|  | 			// If the following flag is set true, it means that input was polled from OnInput callback, but Actions are deferred to match this Render() and processed next FixedUpdateNetwork(). | ||
|  | 			// Because alpha values are not set at that time, we need to explicitly update them from Render(). | ||
|  | 			if (_updateInterpolationData == true) | ||
|  | 			{ | ||
|  | 				_updateInterpolationData = false; | ||
|  | 
 | ||
|  | 				// Get alpha of the Render() call. Later in FUN we can identify when exactly the action was triggered (render-accurate processing). | ||
|  | 				_renderInput.LocalAlpha = Runner.LocalAlpha; | ||
|  | 
 | ||
|  | 				// Store interpolation data. This is used for render-accurate lag-compensated casts. | ||
|  | 				Runner.GetInterpolationData(out _renderInput.InterpolationFromTick, out _renderInput.InterpolationToTick, out _renderInput.InterpolationAlpha); | ||
|  | 
 | ||
|  | 				// This is first render after input polls, we can safely override the accumulated input. | ||
|  | 				_accumulatedInput.LocalAlpha            = _renderInput.LocalAlpha; | ||
|  | 				_accumulatedInput.InterpolationAlpha    = _renderInput.InterpolationAlpha; | ||
|  | 				_accumulatedInput.InterpolationFromTick = _renderInput.InterpolationFromTick; | ||
|  | 				_accumulatedInput.InterpolationToTick   = _renderInput.InterpolationToTick; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			ProcessFrameInput(false); | ||
|  | 
 | ||
|  | 			_lastRenderAlpha = Runner.LocalAlpha; | ||
|  | 
 | ||
|  | 			_agent.EarlyRender(); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// IBeforeTick INTERFACE | ||
|  | 
 | ||
|  | 		void IBeforeTick.BeforeTick() | ||
|  | 		{ | ||
|  | 			if (Object == null) | ||
|  | 				return; | ||
|  | 
 | ||
|  | 			if (Context == null || Context.GameplayMode == null || Context.GameplayMode.State != GameplayMode.EState.Active) | ||
|  | 			{ | ||
|  | 				_fixedInput          = default; | ||
|  | 				_renderInput         = default; | ||
|  | 				_accumulatedInput    = default; | ||
|  | 				_previousFixedInput  = default; | ||
|  | 				_previousRenderInput = default; | ||
|  | 				return; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// Store previous fixed input as a base. This will be compared agaisnt new fixed input. | ||
|  | 			_previousFixedInput = _fixedInput; | ||
|  | 
 | ||
|  | 			if (Object.InputAuthority == PlayerRef.None) | ||
|  | 				return; | ||
|  | 
 | ||
|  | 			// If this fails, fallback (last known) input will be used as current. | ||
|  | 			if (Runner.TryGetInputForPlayer(Object.InputAuthority, out GameplayInput input) == true) | ||
|  | 			{ | ||
|  | 				// New input received, we can store it. | ||
|  | 				_fixedInput = input; | ||
|  | 
 | ||
|  | 				if (Runner.Stage == SimulationStages.Forward) | ||
|  | 				{ | ||
|  | 					// Reset statistics. | ||
|  | 					_missingInputsInRow = 0; | ||
|  | 
 | ||
|  | 					// Reset threshold for repeating inputs. | ||
|  | 					_repeatTime = 0.0f; | ||
|  | 				} | ||
|  | 			} | ||
|  | 			else | ||
|  | 			{ | ||
|  | 				if (Runner.Stage == SimulationStages.Forward) | ||
|  | 				{ | ||
|  | 					// Update statistics. | ||
|  | 					++_missingInputsInRow; | ||
|  | 					++_missingInputsTotal; | ||
|  | 
 | ||
|  | 					// Update threshold for repeating inputs. | ||
|  | 					_repeatTime += Runner.DeltaTime; | ||
|  | 
 | ||
|  | 					if (_logMissingInputs == true && Runner.Tick >= _logMissingInputFromTick) | ||
|  | 					{ | ||
|  | 						Debug.LogWarning($"Missing input for {Object.InputAuthority} {Runner.Tick}. In Row: {_missingInputsInRow} Total: {_missingInputsTotal} Repeating Last Known Input: {_repeatTime <= _maxRepeatTime}", gameObject); | ||
|  | 					} | ||
|  | 				} | ||
|  | 
 | ||
|  | 				if (_repeatTime > _maxRepeatTime) | ||
|  | 				{ | ||
|  | 					_fixedInput = default; | ||
|  | 				} | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// IAfterAllTicks INTERFACE | ||
|  | 
 | ||
|  | 		void IAfterAllTicks.AfterAllTicks(bool resimulation, int tickCount) | ||
|  | 		{ | ||
|  | 			if (resimulation == true) | ||
|  | 				return; | ||
|  | 
 | ||
|  | 			// All OnInput callbacks were executed, we can reset the temporary flag for polling defer actions input. | ||
|  | 			_useDeferActionsInput = default; | ||
|  | 
 | ||
|  | 			// Input consumed in OnInput callback is always tick-aligned, but the input for this frame is aligned with engine/render time. | ||
|  | 			// At this point the accumulated input was consumed up to latest tick time, remains only partial input from latest tick time to render time. | ||
|  | 			// The remaining input is stored in render input and accumulated input should be equal. | ||
|  | 			_accumulatedInput = _renderInput; | ||
|  | 
 | ||
|  | 			// The current fixed input will be used as a base for first Render() after FixedUpdateNetwork(). | ||
|  | 			// This is used to detect changes like NetworkButtons press. | ||
|  | 			_previousRenderInput = _fixedInput; | ||
|  | 
 | ||
|  | 			if (_inputPollDeltaTime > 0.0f) | ||
|  | 			{ | ||
|  | 				// The partial move direction contains input since last engine frame. | ||
|  | 				// We need to scale it so it equals to FUN => Render delta time instead of Render => Render. | ||
|  | 				float remainingRenderInputRatio = _inputPollDeltaTime / Time.unscaledDeltaTime; | ||
|  | 
 | ||
|  | 				_partialMoveDirection     *= remainingRenderInputRatio; | ||
|  | 				_partialMoveDirectionSize *= remainingRenderInputRatio; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// Resetting accumulated move direction to values from current frame. | ||
|  | 			// Because input for current frame was already processed from OnInput callback, we need to reset accumulation to these values, not zero. | ||
|  | 			_accumulatedMoveDirection     = _partialMoveDirection; | ||
|  | 			_accumulatedMoveDirectionSize = _partialMoveDirectionSize; | ||
|  | 
 | ||
|  | 			// Now we can reset last frame render input to defaults. | ||
|  | 			_partialMoveDirection     = default; | ||
|  | 			_partialMoveDirectionSize = default; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// MonoBehaviour INTERFACE | ||
|  | 
 | ||
|  | 		private void Awake() | ||
|  | 		{ | ||
|  | 			_agent = GetComponent<Agent>(); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// PARTIAL METHODS | ||
|  | 
 | ||
|  | 		partial void ProcessStandaloneInput(bool isInputPoll); | ||
|  | 		partial void ProcessMobileInput(bool isInputPoll); | ||
|  | 		partial void ProcessGamepadInput(bool isInputPoll); | ||
|  | 
 | ||
|  | 		// PRIVATE METHODS | ||
|  | 
 | ||
|  | 		private void OnInput(NetworkRunner runner, NetworkInput networkInput) | ||
|  | 		{ | ||
|  | 			int currentFrame = Time.frameCount; | ||
|  | 
 | ||
|  | 			bool isFirstPoll = _lastInputPollFrame != currentFrame; | ||
|  | 			if (isFirstPoll == true) | ||
|  | 			{ | ||
|  | 				_lastInputPollFrame = currentFrame; | ||
|  | 				_inputPollDeltaTime = Time.unscaledDeltaTime; | ||
|  | 
 | ||
|  | 				if (IsFrameInputProcessed() == false) | ||
|  | 				{ | ||
|  | 					_deferActionsInput = _accumulatedInput; | ||
|  | 
 | ||
|  | 					ProcessFrameInput(true); | ||
|  | 				} | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if (_agent.HasInputAuthority == false || Context.HasInput == false) | ||
|  | 			{ | ||
|  | 				_accumulatedInput = default; | ||
|  | 				_renderInput      = default; | ||
|  | 				return; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			GameplayInput pollInput = _accumulatedInput; | ||
|  | 
 | ||
|  | 			if (_inputPollDeltaTime > 0.0001f) | ||
|  | 			{ | ||
|  | 				// At this moment the poll input has render input already accumulated. | ||
|  | 				// This "reverts" the poll input to a state before last render input accumulation. | ||
|  | 				pollInput.LookRotationDelta -= _renderInput.LookRotationDelta; | ||
|  | 
 | ||
|  | 				// In the first input poll (within single Unity frame) we want to accumulate only "missing" part to align timing with fixed tick (last Runner.LocalAlpha => 1.0). | ||
|  | 				// All subsequent input polls return remaining input which is not yet consumed, but again within alignment limits of fixed ticks (0.0 => 1.0 = current => next). | ||
|  | 				float baseRenderAlpha = isFirstPoll == true ? _lastRenderAlpha : 0.0f; | ||
|  | 
 | ||
|  | 				// Here we calculate delta time between last render time (or last input poll simulation time) and time of the pending simulation tick. | ||
|  | 				float pendingTickAlignedDeltaTime = (1.0f - baseRenderAlpha) * Runner.DeltaTime; | ||
|  | 
 | ||
|  | 				// The full render input look rotation delta is not aligned with ticks, we need to remove delta which is ahead of fixed tick time. | ||
|  | 				Vector2 pendingTickAlignedLookRotationDelta = _renderInput.LookRotationDelta * Mathf.Clamp01(pendingTickAlignedDeltaTime / _inputPollDeltaTime); | ||
|  | 
 | ||
|  | 				// Accumulate look rotation delta up to aligned tick time. | ||
|  | 				pollInput.LookRotationDelta += pendingTickAlignedLookRotationDelta; | ||
|  | 
 | ||
|  | 				// Consume same look rotation delta from render input. | ||
|  | 				_renderInput.LookRotationDelta -= pendingTickAlignedLookRotationDelta; | ||
|  | 
 | ||
|  | 				// Decrease remaining input poll delta time by the partial delta time consumed by accumulation. | ||
|  | 				_inputPollDeltaTime = Mathf.Max(0.0f, _inputPollDeltaTime - pendingTickAlignedDeltaTime); | ||
|  | 
 | ||
|  | 				// Accumulated input is now consumed and should equal to remaining render input (after tick-alignment). | ||
|  | 				// This will be fully/partially consumed by following OnInput call(s) or next frame. | ||
|  | 				_accumulatedInput.LookRotationDelta = _renderInput.LookRotationDelta; | ||
|  | 			} | ||
|  | 			else | ||
|  | 			{ | ||
|  | 				// Input poll delta time is too small, we consume whole input. | ||
|  | 				_accumulatedInput.LookRotationDelta = default; | ||
|  | 				_renderInput.LookRotationDelta      = default; | ||
|  | 				_inputPollDeltaTime                 = default; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if (_useDeferActionsInput == true) | ||
|  | 			{ | ||
|  | 				// An action was triggered but it should be processed by the first fixed simulation tick after Render(). | ||
|  | 				// Instead of polling the accumulated input, we replace actions by accumulated input before the action was triggered. | ||
|  | 				pollInput.Actions               = _deferActionsInput.Actions; | ||
|  | 				pollInput.LocalAlpha            = _deferActionsInput.LocalAlpha; | ||
|  | 				pollInput.InterpolationAlpha    = _deferActionsInput.InterpolationAlpha; | ||
|  | 				pollInput.InterpolationFromTick = _deferActionsInput.InterpolationFromTick; | ||
|  | 				pollInput.InterpolationToTick   = _deferActionsInput.InterpolationToTick; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			networkInput.Set(pollInput); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		private bool IsFrameInputProcessed() => _processInputFrame == Time.frameCount; | ||
|  | 
 | ||
|  | 		private void ProcessFrameInput(bool isInputPoll) | ||
|  | 		{ | ||
|  | 			// Collect input from devices. | ||
|  | 			// Can be executed multiple times between FixedUpdateNetwork() calls because of faster rendering speed. | ||
|  | 			// However the input is processed only once per frame. | ||
|  | 
 | ||
|  | 			int currentFrame = Time.frameCount; | ||
|  | 			if (currentFrame == _processInputFrame) | ||
|  | 				return; | ||
|  | 
 | ||
|  | 			_processInputFrame = currentFrame; | ||
|  | 
 | ||
|  | 			// Store last render input as a base to current render input. | ||
|  | 			_previousRenderInput = _renderInput; | ||
|  | 
 | ||
|  | 			// Reset input for current frame to default. | ||
|  | 			_renderInput = default; | ||
|  | 
 | ||
|  | 			// Only input authority is tracking render input. | ||
|  | 			if (HasInputAuthority == false) | ||
|  | 				return; | ||
|  | 			if (_agent.HasInputAuthority == false || Context.HasInput == false) | ||
|  | 				return; | ||
|  | 			if ((Context.Input.IsCursorVisible == true && Context.Settings.SimulateMobileInput == false) || Context.GameplayMode.State != GameplayMode.EState.Active) | ||
|  | 				return; | ||
|  | 
 | ||
|  | 			// Storing the accumulated input for reference. | ||
|  | 			GameplayInput previousAccumulatedInput = _accumulatedInput; | ||
|  | 
 | ||
|  | 			if ((Application.isMobilePlatform == false || Application.isEditor == true) && Context.Settings.SimulateMobileInput == false) | ||
|  | 			{ | ||
|  | 				ProcessStandaloneInput(isInputPoll); | ||
|  | 			} | ||
|  | 			else | ||
|  | 			{ | ||
|  | 				ProcessMobileInput(isInputPoll); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			ProcessGamepadInput(isInputPoll); | ||
|  | 
 | ||
|  | 			AccumulateRenderInput(); | ||
|  | 
 | ||
|  | 			if (isInputPoll == true) | ||
|  | 			{ | ||
|  | 				// Check actions that were triggered in this frame and should be deferred - and processed by the first fixed simulation tick after Render(). | ||
|  | 
 | ||
|  | 				for (int i = 0; i < InterpolationDataActions.Length; ++i) | ||
|  | 				{ | ||
|  | 					if (InterpolationDataActions[i].WasActivated(_renderInput, previousAccumulatedInput) == true) | ||
|  | 					{ | ||
|  | 						// Actions that require interpolation data are always deferred. | ||
|  | 						_useDeferActionsInput = true; | ||
|  | 
 | ||
|  | 						// We cannot set alpha value because it is not calculated yet. Postponing to Render(). | ||
|  | 						_updateInterpolationData = true; | ||
|  | 
 | ||
|  | 						break; | ||
|  | 					} | ||
|  | 				} | ||
|  | 
 | ||
|  | 				if (_useDeferActionsInput == false) | ||
|  | 				{ | ||
|  | 					for (int i = 0; i < DeferredInputActions.Length; ++i) | ||
|  | 					{ | ||
|  | 						if (DeferredInputActions[i].WasActivated(_renderInput, previousAccumulatedInput) == true) | ||
|  | 						{ | ||
|  | 							_useDeferActionsInput = true; | ||
|  | 							break; | ||
|  | 						} | ||
|  | 					} | ||
|  | 				} | ||
|  | 			} | ||
|  | 			else | ||
|  | 			{ | ||
|  | 				// Actions were triggered from Render() in this frame. | ||
|  | 				// Interpolation data is correctly calculated and can be directly written to input. | ||
|  | 
 | ||
|  | 				for (int i = 0; i < InterpolationDataActions.Length; ++i) | ||
|  | 				{ | ||
|  | 					if (InterpolationDataActions[i].WasActivated(_renderInput, previousAccumulatedInput) == true) | ||
|  | 					{ | ||
|  | 						_renderInput.LocalAlpha = Runner.LocalAlpha; | ||
|  | 						Runner.GetInterpolationData(out _renderInput.InterpolationFromTick, out _renderInput.InterpolationToTick, out _renderInput.InterpolationAlpha); | ||
|  | 
 | ||
|  | 						_accumulatedInput.LocalAlpha            = _renderInput.LocalAlpha; | ||
|  | 						_accumulatedInput.InterpolationAlpha    = _renderInput.InterpolationAlpha; | ||
|  | 						_accumulatedInput.InterpolationFromTick = _renderInput.InterpolationFromTick; | ||
|  | 						_accumulatedInput.InterpolationToTick   = _renderInput.InterpolationToTick; | ||
|  | 
 | ||
|  | 						break; | ||
|  | 					} | ||
|  | 				} | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		private void AccumulateRenderInput() | ||
|  | 		{ | ||
|  | 			// We don't accumulate render move direction directly, instead we accumulate the value multiplied by delta time, the result is then divided by total time accumulated. | ||
|  | 			// This approach correctly reflects full throttle in last frame with very fast rendering and is more consistent with fixed simulation. | ||
|  | 
 | ||
|  | 			_partialMoveDirectionSize = Time.unscaledDeltaTime; | ||
|  | 			_partialMoveDirection     = _renderInput.MoveDirection * _partialMoveDirectionSize; | ||
|  | 
 | ||
|  | 			// In other words: | ||
|  | 			// Move direction accumulation is a special case. Let's say simulation runs 30Hz (33.333ms delta time) and render runs 300Hz (3.333ms delta time). | ||
|  | 			// If the player hits a key to run forward in last frame before fixed tick, the KCC will move in render by (velocity * 0.003333f). | ||
|  | 			// Treating this input the same way for next fixed tick results in KCC moving by (velocity * 0.03333f) - 10x more. | ||
|  | 			// Following accumulation proportionally scales move direction so it reflects frames in which input was active. | ||
|  | 			// This way the next fixed tick will correspond more accurately to what happened in predicted render. | ||
|  | 
 | ||
|  | 			_accumulatedMoveDirectionSize += _partialMoveDirectionSize; | ||
|  | 			_accumulatedMoveDirection     += _partialMoveDirection; | ||
|  | 
 | ||
|  | 			// Accumulate input for the OnInput() call, the result represents sum of inputs for all render frames since last fixed tick. | ||
|  | 			_accumulatedInput.Actions            = new NetworkButtons(_accumulatedInput.Actions.Bits | _renderInput.Actions.Bits); | ||
|  | 			_accumulatedInput.MoveDirection      = _accumulatedMoveDirection / _accumulatedMoveDirectionSize; | ||
|  | 			_accumulatedInput.LookRotationDelta += _renderInput.LookRotationDelta; | ||
|  | 
 | ||
|  | 			if (_renderInput.Weapon != default) | ||
|  | 			{ | ||
|  | 				_accumulatedInput.Weapon = _renderInput.Weapon; | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		private byte GetWeaponInput(Keyboard keyboard) | ||
|  | 		{ | ||
|  | 			if (keyboard.qKey.wasPressedThisFrame == true) | ||
|  | 				return (byte)(_agent.Weapons.PreviousWeaponSlot + 1); // Fast switch | ||
|  | 
 | ||
|  | 			int weaponSlot = -1; | ||
|  | 
 | ||
|  | 			if (keyboard.digit1Key.wasPressedThisFrame == true) { weaponSlot = 0; } | ||
|  | 			if (keyboard.digit2Key.wasPressedThisFrame == true) { weaponSlot = 1; } | ||
|  | 			if (keyboard.digit3Key.wasPressedThisFrame == true) { weaponSlot = 2; } | ||
|  | 			if (keyboard.digit4Key.wasPressedThisFrame == true) { weaponSlot = 3; } | ||
|  | 			if (keyboard.digit5Key.wasPressedThisFrame == true) { weaponSlot = 4; } | ||
|  | 
 | ||
|  | 			if (weaponSlot < 0 && keyboard.gKey.wasPressedThisFrame == true) | ||
|  | 			{ | ||
|  | 				weaponSlot = 3; // Cycle grenades | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if (weaponSlot < 0) | ||
|  | 				return 0; | ||
|  | 
 | ||
|  | 			if (weaponSlot <= 2) | ||
|  | 				return (byte)(weaponSlot + 1); // Standard weapon switch | ||
|  | 
 | ||
|  | 			// Grenades (grenades are under slot 5, 6, 7 - but we cycle them with 4 numped key) | ||
|  | 			if (weaponSlot == 3) | ||
|  | 			{ | ||
|  | 				int pendingWeapon = _agent.Weapons.PendingWeaponSlot; | ||
|  | 				int grenadesStart = IsCyclingGrenades == true && pendingWeapon < 7 ? Mathf.Max(pendingWeapon, 4) : 4; | ||
|  | 
 | ||
|  | 				int grenadeToSwitch = _agent.Weapons.GetNextWeaponSlot(grenadesStart, 4); | ||
|  | 
 | ||
|  | 				_grenadesCyclingStartTime = Time.time; | ||
|  | 
 | ||
|  | 				if (grenadeToSwitch > 0 && grenadeToSwitch != pendingWeapon) | ||
|  | 				{ | ||
|  | 					return (byte)(grenadeToSwitch + 1); | ||
|  | 				} | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return 0; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		private void SetDefaults() | ||
|  | 		{ | ||
|  | 			_fixedInput              = default; | ||
|  | 			_renderInput             = default; | ||
|  | 			_accumulatedInput        = default; | ||
|  | 			_previousFixedInput      = default; | ||
|  | 			_previousRenderInput     = default; | ||
|  | 			_deferActionsInput       = default; | ||
|  | 			_useDeferActionsInput    = default; | ||
|  | 			_updateInterpolationData = default; | ||
|  | 			_repeatTime              = default; | ||
|  | 			_lastRenderAlpha         = default; | ||
|  | 			_inputPollDeltaTime      = default; | ||
|  | 			_lastInputPollFrame      = default; | ||
|  | 			_processInputFrame       = default; | ||
|  | 			_missingInputsTotal      = default; | ||
|  | 			_missingInputsInRow      = default; | ||
|  | 
 | ||
|  | 			_smoothLookRotationDelta.ClearValues(); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		[System.Diagnostics.Conditional("UNITY_EDITOR")] | ||
|  | 		private void CheckFixedAccess(bool checkStage) | ||
|  | 		{ | ||
|  | 			if (checkStage == true && Runner.Stage == default) | ||
|  | 			{ | ||
|  | 				throw new InvalidOperationException("This call should be executed from FixedUpdateNetwork!"); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if (Runner.Stage != default && IsProxy == true) | ||
|  | 			{ | ||
|  | 				throw new InvalidOperationException("Fixed input is available only on State & Input authority!"); | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		[System.Diagnostics.Conditional("UNITY_EDITOR")] | ||
|  | 		private void CheckRenderAccess(bool checkStage) | ||
|  | 		{ | ||
|  | 			if (checkStage == true && Runner.Stage != default) | ||
|  | 			{ | ||
|  | 				throw new InvalidOperationException("This call should be executed outside of FixedUpdateNetwork!"); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if (Runner.Stage == default && HasInputAuthority == false) | ||
|  | 			{ | ||
|  | 				throw new InvalidOperationException("Render and accumulated inputs are available only on Input authority!"); | ||
|  | 			} | ||
|  | 		} | ||
|  | 	} | ||
|  | } |