/// <summary> /// Unity's FixedUpdate method /// Handles prediction, server processing, reconciliation /// & FixedUpdate of state /// </summary> void FixedUpdate() { // Don't start until initialization is done, stop updating if the input is lost if (!_networkReady) { return; } if (!_initialized && !Initialize()) { return; } if ((networkObject.IsServer || _isLocalOwner) && _inputListener == null) { return; } #region Netcode Logic // Server Authority - snap the position on all clients to the server's position if (!networkObject.IsServer) { _rigidBody.position = networkObject.position; if (_isLocalOwner && networkObject.frame != 0 && _lastNetworkFrame <= networkObject.frame) { _lastNetworkFrame = networkObject.frame; Reconcile(); } } if (!_isLocalOwner && !networkObject.IsServer) { return; } // Local client prediction & server authoritative logic if (_inputListener.FramesToPlay.Count <= 0) { return; } _currentInput = _inputListener.FramesToPlay.Pop(); _lastLocalFrame = _currentInput.frameNumber; // Try to do a player update (if this fails, something's weird) try { PlayerUpdate(_currentInput); } catch (Exception e) { Debug.LogError("Malformed input frame."); Debug.LogError(e); } // Reconciliation only happens on the local client if (_isLocalOwner && !networkObject.IsServer) { _inputListener.FramesToReconcile.Add(_currentInput); } #endregion }
private void Update() { /* * Poll the input in Update * * I've heard some discussions about this vs polling in * FixedUpdate on the Forge Networking Discord * * From what I can tell it is an opinionated choice * and there's no benefit/problem one way or the other, * although theoretically you are one input behind in * Update since FixedUpdate runs before Update per: * https://docs.unity3d.com/Manual/ExecutionOrder.html */ _lastInputFrame = _inputFrame; _inputFrame = new InputFrame() { right = Input.GetKey(KeyCode.L) || Input.GetKey(KeyCode.RightArrow), down = Input.GetKey(KeyCode.K) || Input.GetKey(KeyCode.DownArrow), left = Input.GetKey(KeyCode.J) || Input.GetKey(KeyCode.LeftArrow), up = Input.GetKey(KeyCode.I) || Input.GetKey(KeyCode.UpArrow), horizontal = Input.GetAxisRaw("Horizontal"), vertical = Input.GetAxisRaw("Vertical") }; }
private void MoveTank(InputFrame input) { // Create a vector in the direction the tank is facing with a magnitude based on the input, speed and the time between frames. Vector3 movement = transform.forward * input.vertical * 5 * Time.deltaTime; // Apply this movement to the rigidbody's position. _rigidBody.MovePosition(_rigidBody.position + movement); }
/// <summary> /// Move the player's simulation (rigid body) /// </summary> /// <param name="input"></param> private void Move(InputFrame input) { // Move the player, clamping the movement so diagonals aren't faster Vector2 translation = Vector2.ClampMagnitude(new Vector2(input.horizontal, input.vertical) * Speed * Time.fixedDeltaTime, Speed); _rigidBody.position += translation; _rigidBody.velocity = translation; }
/// <summary> /// Send input state to the server for processing /// </summary> /// <param name="args"></param> public override void SyncInputs(RpcArgs args) { if (networkObject.IsServer) { var bytes = args.GetNext <Byte[]>(); InputFrame newest = (InputFrame)ByteArray.Deserialize(bytes); FramesToPlay.Add(newest); } }
private void PlayerUpdate(InputFrame input) { // Set the velocity to zero, move the player based on the next input, then detect & resolve collisions _rigidBody.velocity = Vector2.zero; if (input != null && input.HasInput) { Move(input); } PhysicsCollisions(); }
private void TurnTank(InputFrame input) { // Determine the number of degrees to be turned based on the input, speed and time between frames. float turn = input.horizontal * 40 * Time.deltaTime; // Make this into a rotation in the y axis. Quaternion turnRotation = Quaternion.Euler(0f, turn, 0f); // Apply this rotation to the rigidbody's rotation. _rigidBody.MoveRotation(_rigidBody.rotation * turnRotation); }
/// <summary> /// Player update composed of movement and collision processing /// </summary> /// <param name="input"></param> private void PlayerUpdate(InputFrame input) { // Set the velocity to zero, move the player based on the next input, then detect & resolve collisions _rigidBody.velocity = Vector3.zero; // DKE Changed Vector2 to Vector3 if (input != null && input.HasInput) { //Debug.Log("PlayerUpdate has input"); //Move(input); // DKE: removed MoveTank(input); // DKE: Replaced Move by MoveTank + TurnTank TurnTank(input); // DKE: added PhysicsCollisions(); } }
// RPC for receiving InputFrames from the clients public override void SyncInputs(RpcArgs args) { if (networkObject.IsServer) { var bytes = args.GetNext <Byte[]>(); if (sendSingleInputs) { InputFrame nextInputFrame = (InputFrame)ByteArrayUtilities.ByteArrayToObject(bytes); FramesToPlay.Add(nextInputFrame); } else { List <InputFrame> networkInputFrames = (List <InputFrame>)ByteArrayUtilities.ByteArrayToObject(bytes); FramesToPlay.AddRange(networkInputFrames); } } }
/// <summary> /// Reconcile inputs that haven't yet been /// authoritatively processed by the server /// </summary> private void Reconcile() { // Remove any inputs up to and including the last input processed by the server _inputListener.FramesToReconcile.RemoveAll(f => f.frameNumber < networkObject.frame); // Replay them all back to the last input processed by client prediction if (_inputListener.FramesToReconcile.Count > 0) { for (Int32 i = 0; i < _inputListener.FramesToReconcile.Count; ++i) { _currentInput = _inputListener.FramesToReconcile[i]; PlayerUpdate(_currentInput); } } // The error vector measures the difference between the predicted & server updated sim position (this one) // and the view position (the position of the MonoBehavior holding your renderer/view) _errorVector = _rigidBody.position - (Vector2)View.transform.position; _errorTimer = 0.0f; }
/// <summary> /// Polling of inputs is handled in Update since they /// reset every frame making weird things happen in FixedUpdate /// </summary> private void Update() { if (!_networkReady) { return; } if (!networkObject.IsServer && networkObject.IsOwner) { _inputFrame = new InputFrame { right = Input.GetKey(KeyCode.L) || Input.GetKey(KeyCode.RightArrow), down = Input.GetKey(KeyCode.K) || Input.GetKey(KeyCode.DownArrow), left = Input.GetKey(KeyCode.J) || Input.GetKey(KeyCode.LeftArrow), up = Input.GetKey(KeyCode.I) || Input.GetKey(KeyCode.UpArrow), horizontal = Input.GetAxisRaw("Horizontal"), vertical = Input.GetAxisRaw("Vertical") }; } }
void FixedUpdate() { // Check if this client is the local owner _isLocalOwner = networkObject.MyPlayerId == networkObject.ownerNetId; #region Setup // Initial setup - only do this once if ((_isLocalOwner || networkObject.IsServer) && !_setup) { // Interpolation on the predicted client and server does weird things // Only interpolate on non-owner clients networkObject.positionInterpolation.Enabled = false; _setup = true; } // Get the input listener if it doesn't exist and this isn't a remote client if (_inputListener == null) { _inputListener = FindObjectsOfType <InputListener>().FirstOrDefault(x => x.networkObject.Owner.NetworkId == networkObject.ownerNetId); } #endregion #region Netcode Logic // Server Authority - snap the position on all clients to the server's position if (!networkObject.IsServer) { _rigidBody.position = networkObject.position; } // Client owner Reconciliation & Prediction if (_isLocalOwner) { if (_inputListener != null) { // Reconciliation - only do this if the server update is current or new if (networkObject.frame != 0 && _lastNetworkFrame <= networkObject.frame) { _lastNetworkFrame = networkObject.frame; Reconcile(); } // Prediction if (_inputListener.FramesToPlay.Count > 0) { InputFrame input = _inputListener.FramesToPlay[0]; _lastLocalFrame = input.frameNumber; PlayerUpdate(input); _inputListener.FramesToPlay.RemoveAt(0); } } } // Server Processing else if (networkObject.IsServer) { // Reset the current input - we don't want to re-use it if there no inputs in the queue _currentInput = null; if (_inputListener != null) { // Process all available inputs each frame while (_inputListener.FramesToPlay.Count > 0) { _currentInput = _inputListener.FramesToPlay[0]; _lastLocalFrame = _currentInput.frameNumber; _inputListener.FramesToPlay.RemoveAt(0); // Try-catch is a good idea to handle weird serialization/deserialization errors try { PlayerUpdate(_currentInput); } catch (Exception e) { Debug.LogError(e + " (Serverside input processing - Player.cs line 104)"); } } } } #endregion }
private void Move(InputFrame input) { _rigidBody.velocity = new Vector2(input.horizontal, input.vertical) * Speed * Time.fixedDeltaTime; _rigidBody.position += _rigidBody.velocity; }