//Reconciliation methods private void AddReconciliation(ReconciliationEntry entry) { reconciliationList.Add(entry); //Limit the list size if (reconciliationList.Count > maxReconciliationEntries) { reconciliationList.RemoveAt(0); } }
/* * SHARED */ //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>FIXED UPDATE private void FixedUpdate() { float speed = 0f; //If this is running at the local player (client with authoritative control or host client) //We run normal FPS controller (prediction) if(isLocalPlayer) { //This must be before move to check if move grounded the character m_PreviouslyGrounded = m_CharacterController.isGrounded; currentStamp = Network.time; //Store crouch input to send to server //We do this before reading input so that we can compare with the current crouch state bool sendCrouch = m_isCrouching; //Input from user or simulated if(inputSimulation) { SimInput(out speed); } else { GetInput(out speed); } //Store jump input to send to server //We need to store this here because player movement will clear m_Jump bool sendJump = m_Jump; // Store transform values //This is also used for the host to know if it moved to send change messages Vector3 prevPosition = transform.position; Quaternion prevRotation = transform.rotation; // Store collision values CollisionFlags lastFlag = m_CollisionFlags; //If we have predicion, we use the input here to move the character if(prediction) { //Move the player object PlayerMovement(speed); } //Client sound and camera ProgressStepCycle(speed); if(!m_PreviouslyGrounded && m_CharacterController.isGrounded) { StartCoroutine(m_JumpBob.DoBobCycle()); PlayLandingSound(); } //OWNER CLIENTS THAT ARE NOT THE HOST //CLIENTS THAT ARE NOT THE SERVER if(!isServer) { bool crouchChange = m_isCrouching != sendCrouch; bool moved = Vector3.Distance(prevPosition, transform.position) > 0 || m_Input[0] || m_Input[1] || m_Input[2] || m_Input[3]; if(moved || sendJump || crouchChange || rotationChanged) { //Store all inputs generated between msgs to send to server Inputs inputs = new Inputs(); inputs.yaw = transform.rotation.eulerAngles.y; inputs.pitch = m_firstPersonCharacter.rotation.eulerAngles.x; inputs.wasd = m_Input; inputs.move = moved; inputs.walk = m_IsWalking; inputs.rotate = rotationChanged; inputs.jump = sendJump; inputs.crouch = m_isCrouching; inputs.timeStamp = currentStamp; inputsList.Enqueue(inputs); debugMovement dePos = new debugMovement(); // DEBUG POSITION dePos.velocity = m_CharacterController.velocity; dePos.position = transform.position; debugClientPos.Enqueue(dePos); //If we moved, then we need to store reconciliation if(moved || sendJump || crouchChange) { // Create reconciliation entry ReconciliationEntry entry = new ReconciliationEntry(); entry.inputs = inputs; entry.lastFlags = lastFlag; entry.position = prevPosition; entry.rotationYaw = transform.rotation.eulerAngles.y; entry.grounded = m_PreviouslyGrounded; entry.prevCrouching = m_PreviouslyCrouching; AddReconciliation(entry); } //Clear the jump to send sendJump = false; //Clear rotation flag rotationChanged = false; } } //HOST CLIENT if (isServer) { if(Vector3.Distance(transform.position, prevPosition) > 0 || rotationChanged) { movementToSend = true; } } //Thread.Sleep(7); } /* * SERVER SIDE */ else { //If we are on the server, we process commands from the client instead, and generate update messages if(isServer) { //Thread.Sleep(7); //Store state Vector3 lastPosition = transform.position; Quaternion lastCharacterRotation = transform.rotation; Quaternion lastCameraRotation = m_firstPersonCharacter.rotation; //Create the struct to read possible input to calculate Inputs inputs; inputs.rotate = false; //If we have inputs, get them and simulate on the server if(inputsList.Count > 0) { while(inputsList.Count > 0) { inputs = inputsList.Dequeue(); m_IsWalking = inputs.walk; m_Input = inputs.wasd; m_isCrouching = inputs.crouch; m_Jump = inputs.jump; currentStamp = inputs.timeStamp; //If need to, apply rotation if(inputs.rotate) { transform.rotation = Quaternion.Euler(transform.rotation.x, inputs.yaw, transform.rotation.z); m_firstPersonCharacter.rotation = Quaternion.Euler(inputs.pitch, m_firstPersonCharacter.rotation.eulerAngles.y, m_firstPersonCharacter.rotation.eulerAngles.z); } //If need to, simulate movement if(inputs.move) { //Server-side method to the speed out of input from clients CalcSpeed(out speed); //Move the player object PlayerMovement(speed); } //Check if something changed and store //This prevents movement skipping if(Vector3.Distance(transform.position, lastPosition) > 0 || inputs.rotate) { movementToSend = true; } } } m_PreviouslyGrounded = m_CharacterController.isGrounded; } } }
//Reconciliation methods private void AddReconciliation(ReconciliationEntry entry) { reconciliationList.Add(entry); //Limit the list size if(reconciliationList.Count > maxReconciliationEntries) reconciliationList.RemoveAt(0); }
private void RpcClientReceivePosition(double inputStamp, Vector3 pos, Vector3 movementVector) { if (reconciliation && isLocalPlayer) // RECONCILIATION for owner players //Check if this stamp is in the list { if (reconciliationList.Count == 0) { //Nothing to reconciliate, apply server position //Debug.Log(transform.position.ToString() + " : " + pos.ToString()); NUNCA REMOVER transform.position = pos; } else { //Reconciliation starting //Debug.Log("Stamp received from server: "+inputStamp); //Get the oldest recorded input from the player ReconciliationEntry firstEntry = reconciliationList[0]; //Debug.Log("The local reconciliation lists starts at: " + firstEntry.inputs.timeStamp); //Debug.Log("The current position (local start position - before prediction) is: " + firstEntry.trans.position); //Debug.Log("The position the server sent is: " + pos); //Debug.Log("The current position (local end position - after prediction) is: " + transform.position); string debugError = ""; float serverCalculationError = 0f; Vector3 predicted = transform.position; //If the incoming position is too old, ignore if (inputStamp < firstEntry.inputs.timeStamp) { debugError += "Ignored! " + inputStamp + " first in list was " + firstEntry.inputs.timeStamp + "\n"; return; } int oldListSize = reconciliationList.Count; //Remove all older stamps reconciliationList.RemoveAll( entry => entry.inputs.timeStamp <= inputStamp ); //debugError += "Removed: " + (reconciliationList.Count - oldListSize) + ", reconciliation list size: " + reconciliationList.Count + ", old list size: " + oldListSize + "\n"; //Save current collision flags CollisionFlags cflags = m_CollisionFlags; //Save m_Jump bool prevJump = m_Jump; // Apply the received position transform.position = pos; //Apply 'de' received movement m_MoveDir = movementVector; float threshold = 0.0001f; // Reapply all the inputs that aren't processed by the server yet. int count = 0; if (reconciliationList.Count > 0) { //debugError += "The first position for reconciliation is: " + reconciliationList[0].position + "\n"; //Get the lastest collision flags m_CollisionFlags = reconciliationList[0].lastFlags; //We use the next stamp because we save state before move //Debug firstEntry = reconciliationList[0]; serverCalculationError = Vector3.Distance(firstEntry.position, pos); //Debug.Log("SStamp: "+inputStamp+" CStamp: "+ clientForServerStamp.inputs.timeStamp); float speed = 0f; foreach (ReconciliationEntry e in reconciliationList) { Inputs i = e.inputs; m_Input = i.wasd; m_IsWalking = i.walk; m_isCrouching = i.crouch; m_Jump = i.jump; m_PreviouslyCrouching = e.prevCrouching; CalcSpeed(out speed); PlayerMovement(speed, e.grounded, e.position, e.rotationYaw); debugError += "(" + (count++) + ")Intermediate rec position: " + transform.position + "\n"; } } debugError += "The final reconciliated position is: " + transform.position + "\n"; debugError += "The predicted position was: " + predicted + "\n"; //Check if the server calculated the position in a wrong way if (serverCalculationError > threshold) { Debug.Log("[Server position sim failure " + inputStamp + "] Error (distance): " + serverCalculationError); } //Check if predicted is different from renconciliated float recError = Vector3.Distance(predicted, transform.position); if (recError > threshold) { debugError += "Total error: " + recError + "\n"; debugError += "(Logging only errors above: " + threshold + ")"; if (serverCalculationError > threshold) { Debug.Log("[Reconciliation error due to server error] Log:\n" + debugError); } else { Debug.Log("[Reconciliation error] Log:\n" + debugError); } } //Restore collision flags m_CollisionFlags = cflags; //Restore jump state m_Jump = prevJump; } } else { //NO RECONCILIATION //When the position arrives from the server, since server is priority, //set the local pos to it transform.position = pos; } }
/* * SHARED */ //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>FIXED UPDATE private void FixedUpdate() { float speed = 0f; //If this is running at the local player (client with authoritative control or host client) //We run normal FPS controller (prediction) if (isLocalPlayer) { //This must be before move to check if move grounded the character m_PreviouslyGrounded = m_CharacterController.isGrounded; currentStamp = Network.time; //Store crouch input to send to server //We do this before reading input so that we can compare with the current crouch state bool sendCrouch = m_isCrouching; //Input from user or simulated if (inputSimulation) { SimInput(out speed); } else { GetInput(out speed); } //Store jump input to send to server //We need to store this here because player movement will clear m_Jump bool sendJump = m_Jump; // Store transform values //This is also used for the host to know if it moved to send change messages Vector3 prevPosition = transform.position; Quaternion prevRotation = transform.rotation; // Store collision values CollisionFlags lastFlag = m_CollisionFlags; //If we have predicion, we use the input here to move the character if (prediction) { //Move the player object PlayerMovement(speed); } //Client sound and camera ProgressStepCycle(speed); if (!m_PreviouslyGrounded && m_CharacterController.isGrounded) { StartCoroutine(m_JumpBob.DoBobCycle()); PlayLandingSound(); } //OWNER CLIENTS THAT ARE NOT THE HOST //CLIENTS THAT ARE NOT THE SERVER if (!isServer) { bool crouchChange = m_isCrouching != sendCrouch; bool moved = Vector3.Distance(prevPosition, transform.position) > 0 || m_Input[0] || m_Input[1] || m_Input[2] || m_Input[3]; if (moved || sendJump || crouchChange || rotationChanged) { //Store all inputs generated between msgs to send to server Inputs inputs = new Inputs(); inputs.yaw = transform.rotation.eulerAngles.y; inputs.pitch = m_firstPersonCharacter.rotation.eulerAngles.x; inputs.wasd = m_Input; inputs.move = moved; inputs.walk = m_IsWalking; inputs.rotate = rotationChanged; inputs.jump = sendJump; inputs.crouch = m_isCrouching; inputs.timeStamp = currentStamp; inputsList.Enqueue(inputs); debugMovement dePos = new debugMovement(); // DEBUG POSITION dePos.velocity = m_CharacterController.velocity; dePos.position = transform.position; debugClientPos.Enqueue(dePos); //If we moved, then we need to store reconciliation if (moved || sendJump || crouchChange) { // Create reconciliation entry ReconciliationEntry entry = new ReconciliationEntry(); entry.inputs = inputs; entry.lastFlags = lastFlag; entry.position = prevPosition; entry.rotationYaw = transform.rotation.eulerAngles.y; entry.grounded = m_PreviouslyGrounded; entry.prevCrouching = m_PreviouslyCrouching; AddReconciliation(entry); } //Clear the jump to send sendJump = false; //Clear rotation flag rotationChanged = false; } } //HOST CLIENT if (isServer) { if (Vector3.Distance(transform.position, prevPosition) > 0 || rotationChanged) { movementToSend = true; } } //Thread.Sleep(7); } /* * SERVER SIDE */ else //If we are on the server, we process commands from the client instead, and generate update messages { if (isServer) { //Thread.Sleep(7); //Store state Vector3 lastPosition = transform.position; Quaternion lastCharacterRotation = transform.rotation; Quaternion lastCameraRotation = m_firstPersonCharacter.rotation; //Create the struct to read possible input to calculate Inputs inputs; inputs.rotate = false; //If we have inputs, get them and simulate on the server if (inputsList.Count > 0) { while (inputsList.Count > 0) { inputs = inputsList.Dequeue(); m_IsWalking = inputs.walk; m_Input = inputs.wasd; m_isCrouching = inputs.crouch; m_Jump = inputs.jump; currentStamp = inputs.timeStamp; //If need to, apply rotation if (inputs.rotate) { transform.rotation = Quaternion.Euler(transform.rotation.x, inputs.yaw, transform.rotation.z); m_firstPersonCharacter.rotation = Quaternion.Euler(inputs.pitch, m_firstPersonCharacter.rotation.eulerAngles.y, m_firstPersonCharacter.rotation.eulerAngles.z); } //If need to, simulate movement if (inputs.move) { //Server-side method to the speed out of input from clients CalcSpeed(out speed); //Move the player object PlayerMovement(speed); } //Check if something changed and store //This prevents movement skipping if (Vector3.Distance(transform.position, lastPosition) > 0 || inputs.rotate) { movementToSend = true; } } } m_PreviouslyGrounded = m_CharacterController.isGrounded; } } }
/* * SHARED */ //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>FIXED UPDATE private void FixedUpdate() { float speed = 0f; //If this is running at the local player (client with authoritative control or host client) //We run normal FPS controller (prediction) if (isLocalPlayer) { long timestamp = System.DateTime.UtcNow.Ticks; //Store crouch input to send to server bool sendCrouch = m_isCrouching; //Input from user or simulated if (inputSimulation) { SimInput(out speed); } else { GetInput(out speed); } //This is for the host to know if it moved to send change messages Vector3 lastPosition = transform.position; //Store jump input to send to server bool sendJump = m_Jump; // Store transform values Vector3 prevPosition = transform.position; Quaternion prevRotation = transform.rotation; // Store collision values CollisionFlags lastFlag = m_CollisionFlags; //If we have predicion, we use the input here to move the character if (prediction || isServer) { //Move the player object MovePlayer(speed); } //Client sound and camera ProgressStepCycle(speed); //OWNER CLIENTS THAT ARE NOT THE HOST //CLIENTS THAT ARE NOT THE SERVER if (!isServer) { bool crouchChange = m_isCrouching != sendCrouch; bool moved = m_CharacterController.velocity.sqrMagnitude > 0 || m_Input[0] || m_Input[1] || m_Input[2] || m_Input[3]; if (moved || sendJump || crouchChange || rotationChanged) { //Debug.Log("W: " + m_Input[0] + " A: " + m_Input[1] + " S: " + m_Input[2] + " D: " + m_Input[3]); //Store all inputs generated between msgs to send to server Inputs inputs = new Inputs(); inputs.yaw = transform.rotation.eulerAngles.y; inputs.pitch = m_firstPersonCharacter.rotation.eulerAngles.x; inputs.wasd = m_Input; inputs.move = moved; inputs.walk = m_IsWalking; inputs.rotate = rotationChanged; inputs.jump = sendJump; inputs.crouch = m_isCrouching; inputs.timeStamp = timestamp; inputsList.Enqueue(inputs); // Create reconciliation entry ReconciliationEntry entry = new ReconciliationEntry(); entry.inputs = inputs; entry.lastFlags = lastFlag; entry.position = prevPosition; entry.rotation = prevRotation; AddReconciliation(entry); //Clear the jump to send sendJump = false; //Clear rotation flag rotationChanged = false; //Debug.Log("InLst sz is: "+ inputsList.Count+ " Moved is: "+moved); } //Only send input at the network send interval if (dataStep > GetNetworkSendInterval()) { dataStep = 0; //Debug.Log("Sending messages to server"); int toSend = inputsList.Count; //Send input to the server while (inputsList.Count > 0) { //Send the inputs done locally Inputs i = inputsList.Dequeue(); if (i.move && i.rotate) { //Debug.Log("Mov & Rot sent"); CmdProcessMovementAndRotation(i.timeStamp, i.wasd, i.walk, i.crouch, i.jump, i.pitch, i.yaw); } else if (i.move) { //Debug.Log("Mov sent"); CmdProcessMovement(i.timeStamp, i.wasd, i.walk, i.crouch, i.jump); } else if (i.rotate) { //Debug.Log("Rot sent"); CmdProcessRotation(i.timeStamp, i.pitch, i.yaw); } } /*if (toSend > 0) { Debug.Log(toSend + " messages sent to server"); }*/ //Clear the input list inputsList.Clear(); } dataStep += Time.fixedDeltaTime; } //This is for the host player to send its position to all other clients //HOST (THE CLIENT ON THE SERVER) CLIENT if (isServer) { if (dataStep > GetNetworkSendInterval()) { dataStep = 0; if (Vector3.Distance(transform.position, lastPosition) > 0 || rotationChanged) { //Send the current server pos to all clients RpcClientReceivePosition(timestamp, transform.position, m_MoveDir); Debug.Log("Sent host pos"); } } dataStep += Time.fixedDeltaTime; } } /* * SERVER SIDE */ else { //If we are on the server, we process commands from the client instead, and generate update messages if (isServer) { Inputs inputs; if (inputsList.Count == 0) { //Check if the message is late //If the message is too late and the list is empty //Stop server simulation of the player if (lastMassageTime > maxDelayBeforeServerSimStop) return; inputs = new Inputs(); inputs.walk = true; inputs.crouch = m_isCrouching; inputs.wasd = new bool[] { false, false, false, false }; inputs.jump = false; inputs.rotate = false; inputs.timeStamp = System.DateTime.UtcNow.Ticks; } else { inputs = inputsList.Dequeue(); //Debug.Log("Removing : "+inputs.timeStamp); } Vector3 lastPosition = transform.position; Quaternion lastCharacterRotation = transform.rotation; Quaternion lastCameraRotation = m_firstPersonCharacter.rotation; m_IsWalking = inputs.walk; m_Input = inputs.wasd; m_isCrouching = inputs.crouch; m_Jump = inputs.jump; if (inputs.rotate) { transform.rotation = Quaternion.Euler(transform.rotation.x, inputs.yaw, transform.rotation.z); m_firstPersonCharacter.rotation = Quaternion.Euler(inputs.pitch, m_firstPersonCharacter.rotation.eulerAngles.y, m_firstPersonCharacter.rotation.eulerAngles.z); } currentReconciliationStamp = inputs.timeStamp; CalcSpeed(out speed); //Server-side method to the speed out of input from clients //Move the player object MovePlayer(speed); if (dataStep > GetNetworkSendInterval()) { if (Vector3.Distance(transform.position, lastPosition) > 0 || Quaternion.Angle(transform.rotation, lastCharacterRotation) > 0 || Quaternion.Angle(m_firstPersonCharacter.rotation, lastCameraRotation) > 0) { RpcClientReceivePosition(currentReconciliationStamp, transform.position, m_MoveDir); //Debug.Log("Sent client pos "+dataStep + ", stamp: " + currentReconciliationStamp); } dataStep = 0; } dataStep += Time.fixedDeltaTime; } } }
//Reconciliation methods private void AddReconciliation(ReconciliationEntry entry) { reconciliationList.Add(entry); //Limit the list size if (reconciliationList.Count > maxReconciliationEntries) reconciliationList.RemoveAt(0); //Debug.Log("Current reconciliation list size: " + reconciliationList.Count); }