Rigidbody Controller Issues

Hello, I’m experiencing a lot of trouble with player controls, and what the best method is to use.

I’ll start off by saying I cannot use the Character Controller for my needs, as my player character has functional elements attached that require multiple colliders. As Character Controllers can’t seem to work with multiple colliders (to the best of my knowledge), it’s unfortunately out of the question. As such, I’m stuck using a Rigidbody-based controller, and this is where my problem begins.

I’ve been researching this for a long time, hoping to figure this out, but I have found a lot of conflicting information about this. To start, I know that you should not modify rigidbody.velocity directly, as it makes physics unrealistic, and also causes issues with gravity. I also know that transform.position is teleporting - so player collisions can potentially clip through in various scenarios. I’ve tried rigidbody.MovePosition, and it works perfectly, EXCEPT that I am also aware that it does teleporting exactly like transform.position setting, since the rigidbody is non-kinematic.

So, right now, when I use rigidbody.MovePosition (which is my most-functional attempt), some collisions are not an issue, but with smaller colliders, or when the player is going at a high enough speed, there’s a good chance the player character is going to go straight through something. I am aware this is because of the teleporting aspect of non-kinematic rigidbody-based MovePosition, and that’s given me some serious problems. Because of this, I question MovePosition’s usefulness, since to my knowledge, while smooth transitions are useful for kinematic rigidbodies, it’s also important for non-kinematic ones, so I fail to see why MovePosition’s behavior on non-kinematic rigidbodes is good in any respect. Wouldn’t it be useful if MovePosition worked the same way with both kinematic and non-kinematic rigidbodies? Am I missing something here?

And finally, I’ve been working with rigidbody.AddForce and rigidbody.AddRelativeForce. I’ve had a lot of trouble figuring these particular methods out. Most of the time, the character either doesn’t move at all, or it rockets off at insane speeds. I’ve gotten something partially working since, but it is very unstable. I based it off of some FPS controller that went in four directions, but I wanted it to work only in forward/backward directions with steering (car-based), and my changes aren’t perfect. For one thing, it zooms off if it faces a certain direction, and regardless, fails to go up slopes at all. I believe it relates to gravity when trying to go up a ramp.

Ultimately, what I’m asking for here is to get some direction as to the best way to approach making a rigidbody-based controller (NOT a Character Controller one, as it does not fit my needs, unfortunately). I’ve seen a lot of different options, and a lot of reasons why each doesn’t work. And so I am at a loss. Is anyone willing to provide some insight?

The issues you’re experiencing with AddForce may simply be related to using the wrong force mode, or not taking mass into account. It’s probably easiest if you use ForceMode.Impulse when using AddForce, otherwise you should be multiplying your force by your rigidbody’s mass to get reasonable results.

I use a home-made rigidbody character controller. The basic idea is that I use AddForce to push the character forward/backward/left/right depending on which inputs are currently held. For this kind of approach to work, you can’t keep applying constant equal force, or the character will accelerate non-stop. So the basic approach is that the player has a Max force they can apply, but this gets reduced gradually the faster the player is moving. At a certain point, when the player is moving their max speed, AddForce adds very little force. Here’s a quick example of how that’s done:

var currentForwardSpeed = Vector3.Dot(_rigidbody.velocity, transform.forward);

var forwardAdjustmentFactor = Mathf.Max(0, 1.0f - (currentForwardSpeed / (ForwardMovementCap * (currentForwardMovementAmount > 0 ? maxForwardSpeed : maxBackwardsSpeed))).Squared());

This forwardAdjustmentFactor get multiplied by the amount of force to be applied. You’d have to play around with the parameters to get smooth movement, but this allows the player to speed up gradually (or quickly) to a max speed, but not exceed it.

Thanks for replying, dgoyette. Unfortunately, I use C#, not JavaScript (I forgot to mention that earlier), and the two lines of code you have provided aren’t enough for me to figure out what you have described. Could you explain this process you described in more detail? I can’t see how to use this in AddForce or AddRelativeForce, and there’s a few variables that you haven’t explained.

My code is C#. Here’s the whole script I use for my rigidbody movement. There will be parts of this you might not want, as I’m handling some special cases like going up slopes, going up “stairs”, and some other things. But maybe you want walk through it.

There are a couple of dependencies I haven’t included. One is my GroundedCheck script, which just checks whether the player is on the ground or not. There is also a dependency on a RigidbodyMovementSettings object, but that’s just a class with some float values in it to control things like max speed, etc, which you can just replace with private variables. But I think you can walk through most of HandleRigidbodyMovement and get an idea of what’s happening. Let me know if any specific lines of this are confusing.

Note that ForwardMovementAmount and RightMovementAmount are values from -1 to 1, representing whether the player is pressing the forward/backward/right/left movement keys. They’re public properties of this class, rather than being calls to Input.GetAxis, because I have a different class to handle player input, which updates the ForwardMovementAmount and RightMovementAmount in this class.

using GraviaSoftware.Gravia.Code.Controllers.Behaviors;
using GraviaSoftware.Gravia.Code.Data.GameSettings.Movement;
using GraviaSoftware.Gravia.Code.Utilities;
using UnityEngine;

namespace GraviaSoftware.Gravia.Code.Controllers
{
    /// <summary>
    /// Handles movement kinematics for rigidbodies
    /// </summary>
    [RequireComponent(typeof(Rigidbody))]
    [RequireComponent(typeof(GroundedCheck))]
    public class GroundBasedRigidbodyKinematicController : MonoBehaviour
    {
        public RigidbodyMovementSettings RigidbodyMovementSettings;
        public Transform RelativeTransform;

        [HideInInspector]
        public float ForwardMovementAmount;
        [HideInInspector]
        public float RightMovementAmount;
        [HideInInspector]
        public bool IsBracing;
        [HideInInspector]
        public bool IsStunned;

        [HideInInspector]
        public float ForwardMovementCap;
        [HideInInspector]
        public float RightMovementCap;

        [HideInInspector]
        public float ReversoDragContribution;

        [HideInInspector]
        public float RigDragContribution;


        // When on stairs, the speed cap is lowered to prevent very fast running up stairs.
        private float _stairMovementSpeedCapFactor = 0.7f;

        public float EffectiveForwardMovementAmount
        {
            get
            {
                return ForwardMovementAmount * ForwardMovementCap;
            }
        }

        public float EffectiveRightMovementAmount
        {
            get
            {
                return RightMovementAmount * RightMovementCap;
            }
        }

        public float StairsUpwardForceMagnitude { get; private set; } = new Vector3(0, -Physics.gravity.y * 0.55f, 0).magnitude;


        private Rigidbody _rigidbody;
        private GroundedCheck _groundedCheck;

        private void Start()
        {
            if (RelativeTransform == null)
            {
                RelativeTransform = this.transform;
            }

            _rigidbody = this.GetComponent<Rigidbody>();
            _groundedCheck = this.GetComponent<GroundedCheck>();

            ForwardMovementCap = 1;
            RightMovementCap = 1;
        }

        private void FixedUpdate()
        {
            HandleRigidbodyMovement();
        }


        public void HandleRigidbodyMovement()
        {
            // Generally, if the player is holding in a direction, apply force moving the player in that direction.
            // However, the amount of force diminishes proportionally to the current speed in that direction.
            if (IsStunned)
            {
                return;
            }



            // Note: This used to now allow bracing while moving, but it felt better to reenable that. Although this means
            // you can generate more friction when bracing than when walking, this makes sense. You SHOULD be able to. This also
            // means we can use IsBracing to slow/stop AI enemies when they are stunned.

            if (_groundedCheck.IsGrounded && IsBracing /*&& _rigidbody.velocity.sqrMagnitude < RigidbodyMovementSettings.MaximumSqrSpeedForBracing*/)
            {
                // Increase drag up to a certain maximum.
                if (_rigidbody.drag < RigidbodyMovementSettings.MaximumBraceDrag)
                {
                    _rigidbody.drag = RigDragContribution + ReversoDragContribution + Mathf.Min(_rigidbody.drag + (RigidbodyMovementSettings.BraceDragIncreasePerSecond * Time.deltaTime), RigidbodyMovementSettings.MaximumBraceDrag);
                }
            }
            else
            {
                // Adjust brace drag
                _rigidbody.drag = RigDragContribution + ReversoDragContribution + RigidbodyMovementSettings.StandardDrag;

                var currentForwardMovementAmount = EffectiveForwardMovementAmount;
                var currentRightMovementAmount = EffectiveRightMovementAmount;

                // If the entity is walking up or down a slope, the force exerted should be parallel to the slope.
                // We know the slope of the floor from the groundNormal value.
                // Vector3.ProjectOnPlane is used to turn the groundNormal into a Vector parallel to the floor.
                // Then we evaluate whether we are going up or down hill.

                var forwardSlopedDirection = Vector3.ProjectOnPlane(RelativeTransform.forward * currentForwardMovementAmount, _groundedCheck.GroundNormal).normalized;
                var rightSlopedDirection = Vector3.ProjectOnPlane(RelativeTransform.right * currentRightMovementAmount, _groundedCheck.GroundNormal).normalized;

                var forwardUnsignedAngle = Vector3.Angle(currentForwardMovementAmount >= 0 ? RelativeTransform.forward : -RelativeTransform.forward, forwardSlopedDirection);
                var rightUnsignedAngle = Vector3.Angle(currentRightMovementAmount >= 0 ? RelativeTransform.right : -RelativeTransform.right, rightSlopedDirection);

                var forwardTravelDirectionIsUphill = forwardSlopedDirection.y > 0;
                var rightTravelDirectionIsUphill = rightSlopedDirection.y > 0;



                // If we are going uphill with either the Forward or Right movement, we may entirely
                // negate the force if the angle is too steep. Or, we might reduce the force if the angle is
                // steeper than a certain angle but less steep than the max angle.
                if (forwardTravelDirectionIsUphill)
                {
                    if (_groundedCheck.IsOnStairs)
                    {
                        // Stairs ignore the normal slope limitations, and get extra speed.
                        //forwardSlopedDirection *= 0.8f;
                    }
                    else
                    {
                        if (forwardUnsignedAngle > RigidbodyMovementSettings.MaximumRampSlopeAngle)
                        {
                            forwardSlopedDirection = Vector3.zero;
                        }
                        else if (forwardUnsignedAngle > RigidbodyMovementSettings.MinimumRampSlopeAngle)
                        {
                            var percentOfMaximum = 1 - ((forwardUnsignedAngle - RigidbodyMovementSettings.MinimumRampSlopeAngle) / ((RigidbodyMovementSettings.MaximumRampSlopeAngle - RigidbodyMovementSettings.MinimumRampSlopeAngle)));
                            forwardSlopedDirection *= percentOfMaximum;
                        }
                    }
                }

                if (rightTravelDirectionIsUphill)
                {
                    if (_groundedCheck.IsOnStairs)
                    {
                        // Stairs ignore the normal slope limitations, and get extra speed.
                        //rightSlopedDirection *= 0.8f;
                    }
                    else
                    {
                        if (rightUnsignedAngle > RigidbodyMovementSettings.MaximumRampSlopeAngle)
                        {
                            rightSlopedDirection = Vector3.zero;
                        }
                        else if (rightUnsignedAngle > RigidbodyMovementSettings.MinimumRampSlopeAngle)
                        {
                            var percentOfMaximum = 1 - ((rightUnsignedAngle - RigidbodyMovementSettings.MinimumRampSlopeAngle) / ((RigidbodyMovementSettings.MaximumRampSlopeAngle - RigidbodyMovementSettings.MinimumRampSlopeAngle)));
                            rightSlopedDirection *= percentOfMaximum;
                        }
                    }
                }



                // Initially, how much force should we apply to going forward or backwards?
                var forwardForce = forwardSlopedDirection * (currentForwardMovementAmount > 0 ? RigidbodyMovementSettings.ForwardMovementForce : RigidbodyMovementSettings.BackwardsMovementForce);
                // And what is out current forward speed?
                var currentForwardSpeed = Vector3.Dot(_rigidbody.velocity, RelativeTransform.forward);

                // Similarly, how much force should be applied left or right?
                var rightForce = rightSlopedDirection * RigidbodyMovementSettings.StrafeMovementForce;
                // And out current right speed.
                var currentRightSpeed = Vector3.Dot(_rigidbody.velocity, RelativeTransform.right);



                // Only constrain force when trying to add more force in the current movement direction.
                // This means that we can exert significant force forward if we're moving backwards, but
                // much less force forward if we're already moving forward.
                // We use the Pow function to let the initial dampening be small, while approaching the maximum eventually.
                var forwardAdjustmentFactor = Mathf.Max(0, 1.0f - (currentForwardSpeed / (ForwardMovementCap * (_groundedCheck.IsOnStairs ? _stairMovementSpeedCapFactor : 1) * (currentForwardMovementAmount > 0 ? RigidbodyMovementSettings.MaxForwardSpeed : RigidbodyMovementSettings.MaxBackwardsSpeed))).Squared());
                var rightAdjustmentFactor = Mathf.Max(0, 1.0f - (currentRightSpeed / (RightMovementCap * (_groundedCheck.IsOnStairs ? _stairMovementSpeedCapFactor : 1) * RigidbodyMovementSettings.MaxStrafeSpeed)).Squared());




                if (currentForwardMovementAmount != 0 && currentRightMovementAmount != 0)
                {
                    // Constrain both motion vectors by the max forward adjustment.
                    forwardForce *= forwardAdjustmentFactor * rightAdjustmentFactor * .5f;
                    rightForce *= forwardAdjustmentFactor * rightAdjustmentFactor * .5f;
                }
                else
                {
                    if (MathUtil.SameSign(currentForwardMovementAmount, currentForwardSpeed))
                    {
                        forwardForce *= forwardAdjustmentFactor;
                    }

                    if (MathUtil.SameSign(currentRightMovementAmount, currentRightSpeed))
                    {
                        // Same as above, but for right/left movement.
                        rightForce *= rightAdjustmentFactor;
                    }
                }

                // We clamp the new force to be no greater in magnitude than the greatest kind of force we're applying
                var clampMax = Mathf.Max(currentRightMovementAmount == 0 ? 0 : RigidbodyMovementSettings.StrafeMovementForce,
                   currentForwardMovementAmount == 0 ? 0 : (currentForwardMovementAmount > 0 ? RigidbodyMovementSettings.ForwardMovementForce : RigidbodyMovementSettings.BackwardsMovementForce));

                //var newForce = (forwardForce + rightForce);
                var newForce = Vector3.ClampMagnitude((forwardForce + rightForce), clampMax);

                //if (this.gameObject.CompareTag("Player"))
                //{
                //    Debug.Log($"T: {newForce}; M: {newForce.magnitude}: Speed: {_rigidbody.velocity.magnitude}, Stairs: {_groundedCheck.IsOnStairs}: P: {this.transform.position.y}");
                //
                //    Debug.DrawRay(RelativeTransform.position, newForce.normalized * 5, Color.magenta);
                //}




                _rigidbody.AddForce(newForce * (_groundedCheck.IsGrounded ? 1 : RigidbodyMovementSettings.AirbornMovementModifier) * _rigidbody.mass);




                if (_groundedCheck.IsOnStairs)
                {
                    // Offset gravity when on stairs.
                    _rigidbody.AddForce(StairsUpwardForceMagnitude * RelativeTransform.up * _rigidbody.mass);
                }

            }

        }
    }
}

I’ve been looking over the code you provided. Thanks for taking the time to respond to my question.

dgoyette, Is crouching included with your code?

No, it appears that crouching is not included. However, may I suggest using an if statement to change the Transform.Scale of the object you’re using as a player? Obviously, this will only work on simple objects and it’s probably best to only use as a start while you work on a better system later, but it really helped me.