Experimental Smooth Crouch

Discuss coding questions, pull requests, and implementation details.
User avatar
MeteoricDragon
Posts: 141
Joined: Mon Feb 12, 2018 8:23 pm

Re: Experimental Smooth Crouch

Post by MeteoricDragon »

Interkarma wrote: Thu Apr 19, 2018 10:49 pm The camera is a child of the controller object, so moving controller will also move camera. But you should be able to transform camera's localPosition like ifkopifko's suggestion then perform snap crouch. Here's the breakdown, but not sure how well this work in practice as other scripts (like headbob) might also be controlling the camera so need to make allowances for that.
  • When player initiates crouch, break this motion into two stages.
  • During first stage, just lerp camera to desired end position. Basically camera eye height is lerped down into the capsule towards player's waist.
  • In the second stage (after smooth camera movement) perform same snap crouch as now and reset camera position back to normal eye height.
  • Reverse process when standing up.
The end result should be a smooth camera movement followed by a snap crouch and camera reset in same frame. With a bit of tuning, it should look pretty good. But keep in mind I haven't actually tried this method, and I'm a bit worried about other scripts controlling camera.

Another small improvement would be to perform a ray check up from top of player's head before standing back up to ensure they have head room. This will prevent them from standing up under low bits of geometry and poking their heads through the geometry. This is more of a general improvement to crouch though, rather than anything to do with smooth crouch.
Correct, I'm trying to do that.
The only problem is that in the second stage, when you set the controller position and height, the camera will follow, you'll reset it back to normal, and then I think that when you reset the camera, the effects of both are still felt by jerking the camera about. That's why I wonder if it'd be a good idea to disable the connection between the two, then re-enable it once the camera is in the right position again.

User avatar
Uncanny_Valley
Posts: 221
Joined: Mon Mar 23, 2015 5:47 pm

Re: Experimental Smooth Crouch

Post by Uncanny_Valley »

I do find the sudden crouching to be a bit weird, thank you MeteoricDragon for working on this! :D

When it comes to the implementation. As long as you reset the camera back to it's original position during the same frame (the same update loop) as you snap the player body in position, there is no possible reason why the camera should jerk into place. If it does, it's more likely that it doesn't move into the correct position in the first step (when you lerp it's movement). Ho do you currently check whether the camera has moved into position, before snapping the player into position?

User avatar
MeteoricDragon
Posts: 141
Joined: Mon Feb 12, 2018 8:23 pm

Re: Experimental Smooth Crouch

Post by MeteoricDragon »

Uncanny_Valley wrote: Fri Apr 20, 2018 8:16 pm I do find the sudden crouching to be a bit weird, thank you MeteoricDragon for working on this! :D

When it comes to the implementation. As long as you reset the camera back to it's original position during the same frame (the same update loop) as you snap the player body in position, there is no possible reason why the camera should jerk into place. If it does, it's more likely that it doesn't move into the correct position in the first step (when you lerp it's movement). Ho do you currently check whether the camera has moved into position, before snapping the player into position?
I'm trying to figure out the right math to use to get the Camera's local position correct for the Lerp.

Code: Select all

    public class Croucher : MonoBehaviour
    {
        private CrouchToggleAction toggleAction;
        public CrouchToggleAction ToggleAction
        {
            get { return toggleAction; }
            set { toggleAction = value; }
        }
        public float toggleActionSpeed;
        private PlayerMotor playerMotor;
        private CharacterController controller;
        private HeadBobber headBobber;
        private Camera mainCamera;
        private float crouchHeight;
        private float standHeight;
        private float eyeHeight;
        private float camCrouchLevel;
        private float camStandLevel;
        private float camNormalLevel;
        private float crouchTimer;
        private float standTimer;
        private float timerSpeed = 0.5f;
        private float yPosMod;
        private const float timerMax = 0.3f;

        private void Start()
        {
            playerMotor = GetComponent<PlayerMotor>();
            controller = GetComponent<CharacterController>();
            headBobber = GetComponent<HeadBobber>();
            mainCamera = GameManager.Instance.MainCamera;
            standHeight = playerMotor.standingHeight;
            crouchHeight = playerMotor.crouchingHeight;
            eyeHeight = playerMotor.eyeHeight;
            camNormalLevel = (standHeight / 2f) - eyeHeight;  // trying to figure out the right values to use.
            camStandLevel = (standHeight - crouchHeight) / 2f;
            camCrouchLevel = (standHeight - crouchHeight) / 2f;

            toggleAction = CrouchToggleAction.DoNothing;       
        }

        // perform whatever action CrouchToggleAction is set to.
        private void Update()
        {
            if (toggleAction == CrouchToggleAction.DoNothing)
                return;

            bool bFinished = false;

            if (toggleAction != CrouchToggleAction.DoNothing)
            {
                if (toggleAction == CrouchToggleAction.DoCrouching)
                {
                    crouchTimer += Time.deltaTime * timerSpeed;
                    float t = (crouchTimer / timerMax);
                    yPosMod = Mathf.Lerp(camNormalLevel, camCrouchLevel, t);
                }
                else if (toggleAction == CrouchToggleAction.DoStanding)
                {
                    standTimer += Time.deltaTime * timerSpeed;
                    float t = (standTimer / timerMax);
                    yPosMod = Mathf.Lerp(camNormalLevel, camStandLevel, t);
                }

                UpdateCameraPosition();

                bFinished = (crouchTimer >= timerMax || standTimer >= timerMax);
            }

            if (bFinished)
            {
                if (toggleAction == CrouchToggleAction.DoCrouching)
                    yPosMod = camCrouchLevel;
                else
                    yPosMod = camNormalLevel;

                UpdateCameraPosition();
                DoSnapToggleAction();
                crouchTimer = 0;
                standTimer = 0;
                toggleAction = CrouchToggleAction.DoNothing;
            }
        }

        private void UpdateCameraPosition()
        {
            Vector3 camPos = mainCamera.transform.localPosition;
            headBobber.restPos.y = yPosMod; 
            mainCamera.transform.localPosition = new Vector3(camPos.x, yPosMod, camPos.z);
        } 

        private void DoSnapToggleAction()
        {
            if (playerMotor.IsCrouching)
            {
                controller.height = crouchHeight;
                Vector3 pos = controller.transform.position;
                pos.y -= (standHeight - crouchHeight) / 2.0f;
                controller.transform.position = pos;
            }
            else if (!playerMotor.IsCrouching)
            {
                controller.height = standHeight;
                Vector3 pos = controller.transform.position;
                pos.y += (standHeight - crouchHeight) / 2.0f;
                controller.transform.position = pos;
            }
        }

User avatar
Uncanny_Valley
Posts: 221
Joined: Mon Mar 23, 2015 5:47 pm

Re: Experimental Smooth Crouch

Post by Uncanny_Valley »

I believe the following code should work. I haven't tested it and I'm making a few assumptions. I also added a few suggestions on improvements and tips, feel free to ignore any or all of them :)

The one thing I know that will break this, is if it possible to crouch(or uncrouch) before the CroucheToggleAction has reset to DoNothing. It is possible to code this so that player can cancel-out from a crouching "animation", but would require a bit of rewriting.

Code: Select all

 
   //Added the RequireComponent attribute to make sure that following components are indeed on this GameObject, since they are require to make this code work
   [RequireComponent(typeof(PlayerMotor))]
   [RequireComponent(typeof(CharacterController))]
   [RequireComponent(typeof(HeadBobber))]
    public class Croucher : MonoBehaviour
    {
        private CrouchToggleAction toggleAction;
        public CrouchToggleAction ToggleAction
        {
            get { return toggleAction; }
            set { toggleAction = value; }
        }
        //public float toggleActionSpeed; Not used
        private PlayerMotor playerMotor;
        private CharacterController controller;
        private HeadBobber headBobber;
        private Camera mainCamera;
        private float crouchHeight;
        private float standHeight;
        //private float eyeHeight; not used
        private float camCrouchLevel;
        private float camStandLevel;
        private float camNormalLevel;
        private float crouchTimer;
        ///private float standTimer; There is no need to have 2 seperate timers for standing and crouching, since the two will never be used at the same time
        //private float timerSpeed = 0.5f; Not really needed. Can also be easier to read the code since timerMax will equal the crouching time "animation" (or very close to it)
        //private float yPosMod; I made this redundat, see code below (makes code easier to read and follow in my opinion, and it saves a tiny little bit of memory)
        private const float timerMax = 0.3f;

        private void Start()
        {
            playerMotor = GetComponent<PlayerMotor>();
            controller = GetComponent<CharacterController>();
            headBobber = GetComponent<HeadBobber>();
            mainCamera = GameManager.Instance.MainCamera;
            standHeight = playerMotor.standingHeight;
            crouchHeight = playerMotor.crouchingHeight;
            //eyeHeight = playerMotor.eyeHeight; Not used
            //camNormalLevel = (standHeight / 2f) - eyeHeight;  // trying to figure out the right values to use.
            //camStandLevel = (standHeight - crouchHeight) / 2f;
            //camCrouchLevel = (standHeight - crouchHeight) / 2f;

	    camStandHeight = mainCamera.transform.localPosition.y; //With the assumption that the camera begins at correct standing position height
	    camCrouchHeight = camStandHeight - (standHeight - crouchHeight); //Assuming that we want the camera to lower the same amount as the character

            toggleAction = CrouchToggleAction.DoNothing;    //May be unnecessary if toggleAction default value is DoNothing
        }

        // perform whatever action CrouchToggleAction is set to.
        private void Update()
        {
            if (toggleAction == CrouchToggleAction.DoNothing)
                return;

            bool bFinished = false;

	   // if (toggleAction != CrouchToggleAction.DoNothing) removed it since it will never be not true

	   //I assume that CrouchToggleAction only has 3 states, therefore this should only run if CrouchToggleAction != DoNothing
           crouchTimer += Time.deltaTime;
           float t = (crouchTimer / timerMax);
           t = Mathf.Clamp(t, 0, 1); //Just to make sure that t never goes above 1 

           if (toggleAction == CrouchToggleAction.DoCrouching)
                    UpdateCameraPosition( Mathf.Lerp(camStandHeight, camCrouchHeight, t));
           
           else if (toggleAction == CrouchToggleAction.DoStanding)
                    UpdateCameraPosition( Mathf.Lerp(camCrouchHeight, camStandHeight, t));

           bFinished = (crouchTimer >= timerMax);
        
            if (bFinished)
            {
		//Since we are using Mathf.Clamp above to make sure that the camera will be in correct position at the end, there shouldn't be any need for this
		/*
                if (toggleAction == CrouchToggleAction.DoCrouching)
                    yPosMod = camCrouchLevel;
                else
                    yPosMod = camNormalLevel;

                UpdateCameraPosition();
		*/

                DoSnapToggleAction();
                crouchTimer = 0;
                toggleAction = CrouchToggleAction.DoNothing;
            }
        }

        private void UpdateCameraPosition(float yPosMod)
        {
            Vector3 camPos = mainCamera.transform.localPosition;
            headBobber.restPos.y = yPosMod; 
            mainCamera.transform.localPosition = new Vector3(camPos.x, yPosMod, camPos.z);
        } 

        private void DoSnapToggleAction()
        {
            Vector3 pos = controller.transform.position; //Moved this outside the if statements
        
            if (playerMotor.IsCrouching)
            {
                controller.height = crouchHeight;
                pos.y -= (standHeight - crouchHeight) / 2.0f; //Not sure why you divide by 2
            }
            else if (!playerMotor.IsCrouching)
            {
                controller.height = standHeight;
                pos.y += (standHeight - crouchHeight) / 2.0f; //Not sure why you divide by 2 
            }
            
            controller.transform.position = pos; //Moved this outside the if statements
        }

User avatar
MeteoricDragon
Posts: 141
Joined: Mon Feb 12, 2018 8:23 pm

Re: Experimental Smooth Crouch

Post by MeteoricDragon »

Uncanny_Valley wrote: Sat Apr 21, 2018 11:29 am I believe the following code should work. I haven't tested it and I'm making a few assumptions. I also added a few suggestions on improvements and tips, feel free to ignore any or all of them :)

The one thing I know that will break this, is if it possible to crouch(or uncrouch) before the CroucheToggleAction has reset to DoNothing. It is possible to code this so that player can cancel-out from a crouching "animation", but would require a bit of rewriting.

Code: Select all

 
   //Added the RequireComponent attribute to make sure that following components are indeed on this GameObject, since they are require to make this code work
   [RequireComponent(typeof(PlayerMotor))]
   [RequireComponent(typeof(CharacterController))]
   [RequireComponent(typeof(HeadBobber))]
    public class Croucher : MonoBehaviour
    {
        private CrouchToggleAction toggleAction;
        public CrouchToggleAction ToggleAction
        {
            get { return toggleAction; }
            set { toggleAction = value; }
        }
        //public float toggleActionSpeed; Not used
        private PlayerMotor playerMotor;
        private CharacterController controller;
        private HeadBobber headBobber;
        private Camera mainCamera;
        private float crouchHeight;
        private float standHeight;
        //private float eyeHeight; not used
        private float camCrouchLevel;
        private float camStandLevel;
        private float camNormalLevel;
        private float crouchTimer;
        ///private float standTimer; There is no need to have 2 seperate timers for standing and crouching, since the two will never be used at the same time
        //private float timerSpeed = 0.5f; Not really needed. Can also be easier to read the code since timerMax will equal the crouching time "animation" (or very close to it)
        //private float yPosMod; I made this redundat, see code below (makes code easier to read and follow in my opinion, and it saves a tiny little bit of memory)
        private const float timerMax = 0.3f;

        private void Start()
        {
            playerMotor = GetComponent<PlayerMotor>();
            controller = GetComponent<CharacterController>();
            headBobber = GetComponent<HeadBobber>();
            mainCamera = GameManager.Instance.MainCamera;
            standHeight = playerMotor.standingHeight;
            crouchHeight = playerMotor.crouchingHeight;
            //eyeHeight = playerMotor.eyeHeight; Not used
            //camNormalLevel = (standHeight / 2f) - eyeHeight;  // trying to figure out the right values to use.
            //camStandLevel = (standHeight - crouchHeight) / 2f;
            //camCrouchLevel = (standHeight - crouchHeight) / 2f;

	    camStandHeight = mainCamera.transform.localPosition.y; //With the assumption that the camera begins at correct standing position height
	    camCrouchHeight = camStandHeight - (standHeight - crouchHeight); //Assuming that we want the camera to lower the same amount as the character

            toggleAction = CrouchToggleAction.DoNothing;    //May be unnecessary if toggleAction default value is DoNothing
        }

        // perform whatever action CrouchToggleAction is set to.
        private void Update()
        {
            if (toggleAction == CrouchToggleAction.DoNothing)
                return;

            bool bFinished = false;

	   // if (toggleAction != CrouchToggleAction.DoNothing) removed it since it will never be not true

	   //I assume that CrouchToggleAction only has 3 states, therefore this should only run if CrouchToggleAction != DoNothing
           crouchTimer += Time.deltaTime;
           float t = (crouchTimer / timerMax);
           t = Mathf.Clamp(t, 0, 1); //Just to make sure that t never goes above 1 

           if (toggleAction == CrouchToggleAction.DoCrouching)
                    UpdateCameraPosition( Mathf.Lerp(camStandHeight, camCrouchHeight, t));
           
           else if (toggleAction == CrouchToggleAction.DoStanding)
                    UpdateCameraPosition( Mathf.Lerp(camCrouchHeight, camStandHeight, t));

           bFinished = (crouchTimer >= timerMax);
        
            if (bFinished)
            {
		//Since we are using Mathf.Clamp above to make sure that the camera will be in correct position at the end, there shouldn't be any need for this
		/*
                if (toggleAction == CrouchToggleAction.DoCrouching)
                    yPosMod = camCrouchLevel;
                else
                    yPosMod = camNormalLevel;

                UpdateCameraPosition();
		*/

                DoSnapToggleAction();
                crouchTimer = 0;
                toggleAction = CrouchToggleAction.DoNothing;
            }
        }

        private void UpdateCameraPosition(float yPosMod)
        {
            Vector3 camPos = mainCamera.transform.localPosition;
            headBobber.restPos.y = yPosMod; 
            mainCamera.transform.localPosition = new Vector3(camPos.x, yPosMod, camPos.z);
        } 

        private void DoSnapToggleAction()
        {
            Vector3 pos = controller.transform.position; //Moved this outside the if statements
        
            if (playerMotor.IsCrouching)
            {
                controller.height = crouchHeight;
                pos.y -= (standHeight - crouchHeight) / 2.0f; //Not sure why you divide by 2
            }
            else if (!playerMotor.IsCrouching)
            {
                controller.height = standHeight;
                pos.y += (standHeight - crouchHeight) / 2.0f; //Not sure why you divide by 2 
            }
            
            controller.transform.position = pos; //Moved this outside the if statements
        }
Here's my latest code with changes to yours and a video of how its behaving.

Notice that the crouch and stand are at the correct height now. Notice also that the crouch action seems perfect. However, when standing up there is a jerking motion upwards when the camera starts going up, and a jerking motion upwards when the camera reaches the top. This really makes me wonder what is going wrong with this code.

Code: Select all

namespace DaggerfallWorkShop.Game
{
    public enum CrouchToggleAction
    {
        DoNothing,
        DoStanding,
        DoCrouching
    }

    //Added the RequireComponent attribute to make sure that following components are indeed on this GameObject, since they are require to make this code work
    [RequireComponent(typeof(PlayerMotor))]
    [RequireComponent(typeof(CharacterController))]
    [RequireComponent(typeof(HeadBobber))]
    public class Croucher : MonoBehaviour
    {
        private CrouchToggleAction toggleAction;
        public CrouchToggleAction ToggleAction
        {
            get { return toggleAction; }
            set { toggleAction = value; }
        }
        private PlayerMotor playerMotor;
        private CharacterController controller;
        private HeadBobber headBobber;
        private Camera mainCamera;
        private float crouchHeight;
        private float standHeight;
        private float camCrouchLevel;
        private float camStandLevel;
        private float crouchTimer;

        private const float timerMax = 0.3f;

        private void Start()
        {
            playerMotor = GetComponent<PlayerMotor>();
            controller = GetComponent<CharacterController>();
            headBobber = GetComponent<HeadBobber>();
            mainCamera = GameManager.Instance.MainCamera;
            standHeight = playerMotor.standingHeight;
            crouchHeight = playerMotor.crouchingHeight;

            camStandLevel = mainCamera.transform.localPosition.y; //With the assumption that the camera begins at correct standing position height
            camCrouchLevel = camStandLevel - (standHeight - crouchHeight) / 2f; //Assuming that we want the camera to lower the same amount as the character

        }

        // perform whatever action CrouchToggleAction is set to.
        private void Update()
        {
            if (toggleAction == CrouchToggleAction.DoNothing)
                return;

            bool bFinished = false;

            crouchTimer += Time.deltaTime;
            float t = Mathf.Clamp((crouchTimer / timerMax), 0, 1);

            if (toggleAction == CrouchToggleAction.DoCrouching)
                UpdateCameraPosition(Mathf.Lerp(camStandLevel, camCrouchLevel, t));

            else if (toggleAction == CrouchToggleAction.DoStanding)
                UpdateCameraPosition(Mathf.Lerp(camCrouchLevel, camStandLevel, t));

            bFinished = (crouchTimer >= timerMax);

            if (bFinished)
            {
                DoSnapToggleAction();

                if (toggleAction == CrouchToggleAction.DoCrouching)
                    UpdateCameraPosition(mainCamera.transform.localPosition.y + (standHeight - crouchHeight) / 2f);
                //else
                //    UpdateCameraPosition(mainCamera.transform.localPosition.y + (standHeight - crouchHeight) / 2f);

                crouchTimer = 0;
                toggleAction = CrouchToggleAction.DoNothing;
            }
        }

        private void UpdateCameraPosition(float yPosMod)
        {
            Vector3 camPos = mainCamera.transform.localPosition;
            headBobber.restPos.y = yPosMod;
            mainCamera.transform.localPosition = new Vector3(camPos.x, yPosMod, camPos.z);
        }

        private void DoSnapToggleAction()
        {
            if (playerMotor.IsCrouching)
            {
                controller.height = crouchHeight;
                controller.transform.position -= new Vector3(0, (standHeight - crouchHeight) / 2.0f);
            }
            else if (!playerMotor.IsCrouching)
            {
                controller.height = standHeight;
                controller.transform.position += new Vector3(0, (standHeight - crouchHeight) / 2.0f);
            }
        }

User avatar
MeteoricDragon
Posts: 141
Joined: Mon Feb 12, 2018 8:23 pm

Re: Experimental Smooth Crouch

Post by MeteoricDragon »

Actually, I think I got it now. Except for a bounce motion when standing. It happens about 50% of the time. I'll see if I can get a video posted what it looks like.

I had to reverse what I was doing for standing.

There are two methods. one for crouching, and one for standing.

Both are simpler than the original update method because there are fewer checks for whether crouching or not.

Crouching does similarly what the code I showed you before does. It first lowers the camera, then snaps the controller to the right size. Standing is in the opposite order. It snaps first, then raises the camera. .

Can anyone tell me a place where I can test a function to disable standing if the player doesn't have headroom?

User avatar
MeteoricDragon
Posts: 141
Joined: Mon Feb 12, 2018 8:23 pm

Re: Experimental Smooth Crouch

Post by MeteoricDragon »

The smooth crouch now smoothly crouches/stands without any problems. The culprit that was messing up the start of the stand was a line in PlayerMotor.

It said

Code: Select all

if (!isCrouching) 
    controller.height = standingHeight.  
I made that line say

Code: Select all

if (!isCrouching && myCroucher.ToggleAction != CrouchToggleAction.DoStanding) // don't set to standing height while croucher is standing the player
     controller.height = standingHeight;

User avatar
MeteoricDragon
Posts: 141
Joined: Mon Feb 12, 2018 8:23 pm

Re: Experimental Smooth Crouch

Post by MeteoricDragon »

Everything is working as far as I can tell! I submitted PR for it.

User avatar
Interkarma
Posts: 7236
Joined: Sun Mar 22, 2015 1:51 am

Re: Experimental Smooth Crouch

Post by Interkarma »

MeteoricDragon wrote: Sun Apr 22, 2018 1:42 am Can anyone tell me a place where I can test a function to disable standing if the player doesn't have headroom?
A great spot is the angled bit of wall in the stone corridor leading out of the starting cave in Privateer's Hold. You can crouch down, wedge yourself under the angled bit, then stand back up again and your head will poke through the geometry.

There's a few other little spots like this (stand on top of shelves and bookcases close to the ceiling like the imp room in PH). It's not a massive problem though, the player capsule itself shouldn't be allow to clip through the planes. Just the camera can poke through.

It's something I'm going to address later in a polishing pass if you don't feel like tackling it now.

Edit: Just realised you have already addressed this in your PR. Awesome! :D

User avatar
Uncanny_Valley
Posts: 221
Joined: Mon Mar 23, 2015 5:47 pm

Re: Experimental Smooth Crouch

Post by Uncanny_Valley »

MeteoricDragon wrote: Sun Apr 22, 2018 9:46 pm Everything is working as far as I can tell! I submitted PR for it.
Great! :D

Post Reply