Easing (Lerp) is just... awful. With that out of the way, here's my problem. I'm (pathetically) trying to make a first-person character controller, and am having unbelievable amounts of trouble actually getting anything even remotely related to the camera working. Part of the problem is I want this dynamic, otherwise, I'd just use animations and forget the concept of math ever existed. I'm trying to get the camera to bob down and then back up when the player lands after jumping (or falling), which is proving to be substantially more difficult than I ever thought possible for such a simple thing. I'm trying to replicate exponential ease-out, to very little success. This is probably the wrong approach to this but I'm not even able to achieve the wrong thing after several hours of staring at strange websites and trying to get anything at all working. The coroutine supposedly meant to achieve the easing is at 238, and the call for it is at 163 (not even sure this is the best way to check if the player lands either). I'm completely out of ideas and have absolutely no idea where to go from here. Please excuse my trash code, I'm not a programmer. Code (CSharp): using System; using System.Collections; using System.Collections.Generic; using System.Security.Principal; using System.Threading; using UnityEngine; [RequireComponent(typeof(CharacterController))] [AddComponentMenu("Arcane/Player/Character Controller")] public class ArcaneCharacterController : MonoBehaviour { //Public Reference Access public static ArcaneCharacterController arcaneCharacter; //Variables and defaults public float playerLookSensitivity = 8f; public float playerWalkingSpeed = 5f; public float playerRunningSpeed = 10f; public float playerJumpStrength = 4f; public float vertRotLimit = 80f; float gravityModifier = 1.0f; //Backend float forwardMovt; float sidewaysMovt; float verticalVelocity; bool airControl; bool hasJumped; float vertRot = 0; //CharacterController Reference CharacterController cc; //Camera head bob float bobSpeedWalk = 0.18f; float bobSpeedRun = 0.32f; float bobHeight = 0.2f; [HideInInspector] public float bobMid = 0.7f; bool isHeadBobbing = true; float bobTimer = 0f; void Awake() { //If player already exists, destroy. If not, set reference to this and set player to DDOL. if (arcaneCharacter != null) { Destroy(gameObject); } else { arcaneCharacter = this; DontDestroyOnLoad(gameObject); } cc = GetComponent<CharacterController>(); Cursor.visible = false; Cursor.lockState = CursorLockMode.Locked; } void Update() { #region Movement //Set variables to preferences (obtained through Arcane_GameManager static reference) playerLookSensitivity = float.Parse(Arcane_GameManager.manager.GetPref("playerLookSpeed")); playerWalkingSpeed = float.Parse(Arcane_GameManager.manager.GetPref("playerWalkingSpeed")); playerRunningSpeed = float.Parse(Arcane_GameManager.manager.GetPref("playerRunningSpeed")); playerJumpStrength = float.Parse(Arcane_GameManager.manager.GetPref("playerJumpHeight")); vertRotLimit = float.Parse(Arcane_GameManager.manager.GetPref("playerVertLimit")); gravityModifier = float.Parse(Arcane_GameManager.manager.GetPref("playerGravMod")); airControl = bool.Parse(Arcane_GameManager.manager.GetPref("playerAirCtrl")); //Horizontal Look float horiRot = Input.GetAxis("Mouse X") * (playerLookSensitivity / 4); transform.Rotate(0, horiRot, 0); //Vertical Look vertRot -= Input.GetAxis("Mouse Y") * (playerLookSensitivity / 4); //Clamp (Limit) view angles to +vertRotLimit and -vertRotLimit vertRot = Mathf.Clamp(vertRot, -vertRotLimit, vertRotLimit); //Set camera rotation Camera.main.transform.localRotation = Quaternion.Euler(vertRot, 0, 0); //Look With Keys //Horizontal if (Input.GetKey(Arcane_GameManager.manager.GetCont("contRotLeft"))) { transform.Rotate(0, -1.25f, 0); } if (Input.GetKey(Arcane_GameManager.manager.GetCont("contRotRight"))) { transform.Rotate(0, 1.25f, 0); } //Vertical if (Input.GetKey(Arcane_GameManager.manager.GetCont("contLookUp"))) { vertRot += -1f; } if (Input.GetKey(Arcane_GameManager.manager.GetCont("contLookDown"))) { vertRot += 1f; } //Movement if (!airControl && cc.isGrounded) { forwardMovt = CustomAxis(0f, Arcane_GameManager.manager.GetCont("contForward"), Arcane_GameManager.manager.GetCont("contBackward")) * playerWalkingSpeed; sidewaysMovt = CustomAxis(0f, Arcane_GameManager.manager.GetCont("contRight"), Arcane_GameManager.manager.GetCont("contLeft")) * playerWalkingSpeed; //Sprint toggle if (Input.GetKeyDown(Arcane_GameManager.manager.GetCont("contSprintToggle"))) { Arcane_GameManager.manager.sprintToggle = !Arcane_GameManager.manager.sprintToggle; } //If Running if (Input.GetKey(Arcane_GameManager.manager.GetCont("contSprintHold")) || Arcane_GameManager.manager.sprintToggle) { forwardMovt = Input.GetAxis("Vertical") * playerRunningSpeed; sidewaysMovt = Input.GetAxis("Horizontal") * playerRunningSpeed; } } else if (airControl) { forwardMovt = CustomAxis(0f, Arcane_GameManager.manager.GetCont("contForward"), Arcane_GameManager.manager.GetCont("contBackward")) * playerWalkingSpeed; sidewaysMovt = CustomAxis(0f, Arcane_GameManager.manager.GetCont("contRight"), Arcane_GameManager.manager.GetCont("contLeft")) * playerWalkingSpeed; //Sprint toggle if (Input.GetKeyDown(Arcane_GameManager.manager.GetCont("contSprintToggle"))) { Arcane_GameManager.manager.sprintToggle = !Arcane_GameManager.manager.sprintToggle; } //If Running if (Input.GetKey(Arcane_GameManager.manager.GetCont("contSprintHold")) || Arcane_GameManager.manager.sprintToggle) { forwardMovt = Input.GetAxis("Vertical") * playerRunningSpeed; sidewaysMovt = Input.GetAxis("Horizontal") * playerRunningSpeed; } } //Add Gravity verticalVelocity += (Physics.gravity.y * gravityModifier) * Time.deltaTime; //Jump if (Input.GetKey(Arcane_GameManager.manager.GetCont("contJump")) && cc.isGrounded) { verticalVelocity = playerJumpStrength; } //Player Lands if(Input.GetKey(Arcane_GameManager.manager.GetCont("contJump")) && !cc.isGrounded) { hasJumped = true; } //Head bob if(hasJumped && cc.isGrounded) { StartCoroutine(LandHeadBob(Camera.main.transform.localPosition, bobHeight, 1.0f)); hasJumped = false; } //Create movement vector Vector3 playerMovement = new Vector3(sidewaysMovt, verticalVelocity, forwardMovt); //Apply movement vector to CharacterController cc.Move(transform.rotation * playerMovement * Time.deltaTime); #endregion #region Camera Bob bobSpeedWalk = float.Parse(Arcane_GameManager.manager.GetPref("playerHBobWlkSpd")); bobSpeedRun = float.Parse(Arcane_GameManager.manager.GetPref("playerHBobRunSpd")); bobHeight = float.Parse(Arcane_GameManager.manager.GetPref("playerHBobHgt")); bobMid = float.Parse(Arcane_GameManager.manager.GetPref("playerHeight")) - (float)(Math.Truncate(float.Parse(Arcane_GameManager.manager.GetPref("playerHeight")))); float waveslice = 0f; Vector3 posConvert = Camera.main.transform.localPosition; if (Mathf.Abs(sidewaysMovt) == 0 && Mathf.Abs(forwardMovt) == 0) { bobTimer = 0f; } else { waveslice = Mathf.Sin(bobTimer); //If Running if (Input.GetKey(Arcane_GameManager.manager.GetCont("contSprintHold")) || Arcane_GameManager.manager.sprintToggle) { bobTimer = bobTimer + bobSpeedRun; } else { //Walking bobTimer = bobTimer + bobSpeedWalk; } if (bobTimer > Mathf.PI * 2) { bobTimer = bobTimer - (Mathf.PI * 2); } } if (waveslice != 0) { float translateChange = waveslice * bobHeight; float totalAxes = Mathf.Abs(sidewaysMovt) + Mathf.Abs(forwardMovt); totalAxes = Mathf.Clamp(totalAxes, 0.0f, 1.0f); translateChange = totalAxes * translateChange; if (isHeadBobbing) { posConvert.y = bobMid + translateChange; } else if (!isHeadBobbing) { posConvert.x = translateChange; } } else { if (isHeadBobbing) { posConvert.y = bobMid; } else if (!isHeadBobbing) { posConvert.x = 0; } } Camera.main.transform.localPosition = posConvert; #endregion } IEnumerator LandHeadBob(Vector3 start, float y_target, float duration) { float elapsed_time = 0; Vector3 pos = start; float y_start = pos.y; while(elapsed_time < duration) { pos.y = EaseOutExpo(y_start, y_target, elapsed_time / duration); start = pos; yield return null; elapsed_time += Time.deltaTime; } } public static float EaseOutExpo(float start, float end, float value) { end -= start; return end * (-Mathf.Pow(2, -10 * value) + 1) + start; } //Smooth movement output float CustomAxis(float value, KeyCode positive, KeyCode negative) { float value2 = value; if (Input.GetKey(positive) && !Input.GetKey(negative)) { value2 = Mathf.SmoothStep(value, 1f, 1f); } if (Input.GetKey(negative) && !Input.GetKey(positive)) { value2 = Mathf.SmoothStep(value, -1f, 1f); } if (!Input.GetKey(positive) && !Input.GetKey(negative)) { value2 = Mathf.SmoothStep(value, 0f, 1f); } return value2; } public void SetPlayerHeight(float totalHeight) { //Set CharacterController Height cc.height = (float)(Math.Truncate(totalHeight) + 1); //Set Camera Height Vector3 camPos = new Vector3(0, (cc.height - totalHeight), 0); Camera.main.transform.localPosition = camPos; } }
Camera stuff is tricky. Have you looked into Cinemachine? It might do exactly what you want out of the box. Otherwise you need to accumulate your own offset to the camera based on the acceleration of your character, which is the first derivative of the speed, i.e., you take the speed THIS frame, compare it to the speed LAST frame, and that is the instantaneous acceleration you experienced, such as slamming into the ground. Then you use that offset in some kind of accumulator that slowly fades itself back to zero (Vector3.Lerp works for this) so that large inputs (such as landing) will transiently deflect it, then it will restore itself. I implemented such a system in my Jetpack Kurt Space Flight game, with a fair amount of magic numbers and fiddling. Here is the source code for the function that accumulates and deflects the camera from sudden changes to velocity. Code (csharp): Vector3 previousVelocity; Vector3 AccumulatedDifferential; Vector3 AccumulatedDeflection; // this is added to offset the camera from its desired neutral point public void MyUpdateCameraPhysics( Rigidbody rb) { Vector3 currentVelocity = rb.velocity; Vector3 acceleration = previousVelocity - currentVelocity; const float AccelerationScale = 200.0f; const float MaxAcceleration = 0.2f; const float Rebound = 5.0f; const float Damping = 10.0f; acceleration /= AccelerationScale; if (acceleration.magnitude > MaxAcceleration) { acceleration = acceleration.normalized * MaxAcceleration; } AccumulatedDifferential += acceleration; AccumulatedDeflection += AccumulatedDifferential; AccumulatedDifferential -= AccumulatedDeflection * Rebound * Time.deltaTime; AccumulatedDeflection -= AccumulatedDeflection * Damping * Time.deltaTime; previousVelocity = currentVelocity; } All of that is deep within the camera infrastructure, which also drives an airframe shaking effect when you add huge amounts of power, and also allows gaze-around control, etc. You can see it in action here: