// CharacterController movement is physics based and requires FixedUpdate. // (using Update causes strange movement speeds in builds otherwise) void FixedUpdate() { // only control movement for local player if (isLocalPlayer) { // get input and desired direction based on camera and ground Vector2 inputDir = player.IsMovementAllowed() ? GetInputDirection() : Vector2.zero; Vector3 desiredDir = GetDesiredDirection(inputDir); Debug.DrawLine(transform.position, transform.position + desiredDir, Color.cyan); // update state machine if (state == MoveState.IDLE) { state = UpdateIDLE(inputDir, desiredDir); } else if (state == MoveState.WALKING) { state = UpdateWALKING(inputDir, desiredDir); } else { Debug.LogError("Unhandled Movement State: " + state); } // move depending on latest moveDir changes //Debug.Log(name + " step speed=" + moveDir.magnitude + " in state=" + state); controller.Move(moveDir * Time.fixedDeltaTime); // note: returns CollisionFlags if needed velocity = controller.velocity; // for animations and fall damage // broadcast to server CmdFixedMove(new Move(route, state, transform.position, transform.rotation.eulerAngles.y)); } // server/other clients need to do some caching and scaling too else { // apply next pending move if we have at least 'minMoveBuffer' // moves pending to make sure that we also still have one to apply // in the next FixedUpdate call. // => it's better to apply 1,1,1,1 move instead of 2,0,2,1,0 moves. if (pendingMoves.Count > 0 && pendingMoves.Count >= minMoveBuffer) { // too many pending moves? // (only the server has authority to reset!) if (isServer && pendingMoves.Count >= maxMoveBuffer) { // force reset Warp(transform.position); } // more than combine threshold? // (both server and client can combine moves if needed) else if (pendingMoves.Count >= 2 && pendingMoves.Count >= combineMovesAfter) { Vector3 previousPosition = transform.position; MoveState previousState = state; // combine the next two moves to minimize overall delay. // => if we are always behind 5 moves then we might as well // accelerate a bit to always be behind 4, 3, or 2 moves // to minimize movement delay. // // note: calling controller.Move() multiple times in one // FixedUpdate doesn't work well and isn't // recommended. adding the two vectors together works // better. // // note: we COULD warp to first.position and then move to // second.position to avoid the double-move which // always comes with the risk of getting out of sync // because A,B went behind a wall while A+B went into // the wall. // // BUT if we do warp then we would ALWAYS risk wall // hack cheats since the server would sometimes set // the position without checking physics via .Move. // // INSTEAD we risk move(A+B) walkning into a wall and // reset if rubberbanding gets too far off. at least // this method can be made cheat safe. Move first = pendingMoves.Dequeue(); Move second = pendingMoves.Dequeue(); Vector3 move = second.position - transform.position; // calculate the delta before each move. using .position is 100% accurate and never gets out of sync. // check if allowed (only check on server) if (!isServer || IsValidMove(move)) { state = second.state; // multiple moves. use velocity between the two moves, not between position and second move. // -> and divide by fixedDeltaTime because velocity is // direction / second velocity = (second.position - first.position) / Time.fixedDeltaTime; transform.rotation = Quaternion.Euler(0, second.yRotation, 0); controller.Move(move); ++combinedMoves; // debug information only // safety checks on server if (isServer) { // check speed AFTER actually moving with the TRUE // velocity. this works better than checking before // moving because now we know the true velocity // after collisions. if (!WasValidSpeed(velocity, previousState, state, true)) { Warp(previousPosition); } // rubberbanding check if server RubberbandCheck(second.position); } } // force reset to last allowed position if not allowed else { Warp(transform.position); Debug.LogWarning(name + " combined move: " + move + " rejected and force reset to: " + transform.position + "@" + DateTime.UtcNow); } } // less than combine threshold? else { Vector3 previousPosition = transform.position; MoveState previousState = state; // apply one move Move next = pendingMoves.Dequeue(); Vector3 move = next.position - transform.position; // calculate the delta before each move. using .position is 100% accurate and never gets out of sync. // check if allowed (only check on server) if (!isServer || IsValidMove(move)) { state = next.state; transform.rotation = Quaternion.Euler(0, next.yRotation, 0); controller.Move(move); velocity = controller.velocity; // only one move, so controller velocity is true. // safety checks on server if (isServer) { // check speed AFTER actually moving with the TRUE // velocity. this works better than checking before // moving because now we know the true velocity // after collisions. if (!WasValidSpeed(velocity, previousState, state, false)) { Warp(previousPosition); } // rubberbanding check if server RubberbandCheck(next.position); } } // force reset to last allowed position if not allowed else { Warp(transform.position); Debug.LogWarning(name + " single move: " + move + " rejected and force reset to: " + transform.position + "@" + DateTime.UtcNow); } } } //else Debug.Log("none pending. client should send faster..."); } // set last state after everything else is done. lastState = state; }
void FixedUpdate() { if (isLocalPlayer) { Vector2 inputDir = GetInputDirection(); Vector3 desiredDir = GetDesiredDirection(inputDir); Debug.DrawLine(transform.position, transform.position + desiredDir, Color.cyan); if (state == MoveState.IDLE) { state = UpdateIDLE(inputDir, desiredDir); } else if (state == MoveState.WALKING) { state = UpdateWALKINGandRUNNING(inputDir, desiredDir); } else if (state == MoveState.RUNNING) { state = UpdateWALKINGandRUNNING(inputDir, desiredDir); } else if (state == MoveState.CROUCHING) { state = UpdateCROUCHING(inputDir, desiredDir); } else if (state == MoveState.CRAWLING) { state = UpdateCRAWLING(inputDir, desiredDir); } else if (state == MoveState.AIRBORNE) { state = UpdateAIRBORNE(inputDir, desiredDir); } else if (state == MoveState.CLIMBING) { state = UpdateCLIMBING(inputDir, desiredDir); } else if (state == MoveState.SWIMMING) { state = UpdateSWIMMING(inputDir, desiredDir); } else if (state == MoveState.DEAD) { state = UpdateDEAD(inputDir, desiredDir); } else { Debug.LogError("Unhandled Movement State: " + state); } if (!controller.isGrounded) { lastFall = controller.velocity; } controller.Move(moveDir * Time.fixedDeltaTime); velocity = controller.velocity; byte rotationByte = FloatBytePacker.ScaleFloatToByte(transform.rotation.eulerAngles.y, 0, 360, byte.MinValue, byte.MaxValue); CmdFixedMove(new Move(route, state, transform.position, rotationByte)); float runCycle = Mathf.Repeat(animator.GetCurrentAnimatorStateInfo(0).normalizedTime + runCycleLegOffset, 1); jumpLeg = (runCycle < 0.5f ? 1 : -1); jumpKeyPressed = false; crawlKeyPressed = false; crouchKeyPressed = false; } else { if (lastState != state) { AdjustControllerCollider(); } if (!controller.isGrounded) { lastFall = velocity; } if (pendingMoves.Count > 0 && pendingMoves.Count >= minMoveBuffer) { if (isServer && pendingMoves.Count >= maxMoveBuffer) { Warp(transform.position); } else if (pendingMoves.Count >= 2 && pendingMoves.Count >= combineMovesAfter) { Move first = pendingMoves.Dequeue(); Move second = pendingMoves.Dequeue(); state = second.state; Vector3 move = second.position - transform.position; velocity = second.position - first.position; float yRotation = FloatBytePacker.ScaleByteToFloat(second.yRotation, byte.MinValue, byte.MaxValue, 0, 360); transform.rotation = Quaternion.Euler(0, yRotation, 0); controller.Move(move); ++combinedMoves; if (isServer) { RubberbandCheck(second.position); } } else { Move next = pendingMoves.Dequeue(); state = next.state; Vector3 move = next.position - transform.position; float yRotation = FloatBytePacker.ScaleByteToFloat(next.yRotation, byte.MinValue, byte.MaxValue, 0, 360); transform.rotation = Quaternion.Euler(0, yRotation, 0); controller.Move(move); velocity = controller.velocity; if (isServer) { RubberbandCheck(next.position); } } } } if (isServer) { if (lastState == MoveState.AIRBORNE && state != MoveState.AIRBORNE) { ApplyFallDamage(); } } lastState = state; }