Skip to main content

Command Palette

Search for a command to run...

Weekly Devlog 1: Unity Physics Based Movement

Updated
6 min readView as Markdown
Weekly Devlog 1: Unity Physics Based Movement
A
Game dev student. Unity Engine <3

https://www.youtube.com/watch?v=RPi-Gi9VhmM

For my new project I require a very stable and versatile physics based Movement, that I implemented this week. I'm using the default Unity physics Engine PhysX, the movement itself is very inspired by Minecraft and features crouching, sprinting and flying. It also behaves correctly on moving rigidbodies, slopes and rotating rigidbodies.

The Movement script is split into several parts:

private MovementInput _input;
private MovementCommands _commands;
private MovementDebug _debug;
private MovementGround _ground;
private MovementFly _fly;
private MovementCore _core;
private MovementJump _jump;
private MovementCrouch _crouch;

The main logic is found in MovementCore, it handles the default Movement for on ground and in air. The key to a good physics based movement is that you never modify the linearVelocity (with exception for the jump logic), so that the player can be pushed around by other physics object, without losing his velocity immediately. Instead you have to use Forces to make the Player move realistically.

        public void FixedUpdate()
        {
            if (!_ground.Grounded)
            {
                AirMovement();
                return;
            }

            var currentMaxSpeed = _input.Run ? _config.runMaxSpeed : _config.walkMaxSpeed;
            
            //Get the velocity of the ground we are standing on
            var baseVelocity = Vector3.zero;
            if (_ground.GroundRigidbody != null)
                baseVelocity = _ground.GroundRigidbody.GetPointVelocity(_config.groundCheck.position);
            
            var moveVec = new Vector3(_input.Horizontal, 0, _input.Vertical);
            var targetLocal = moveVec * currentMaxSpeed;
            
            //Calculate the current target Velocity and the current actual velocity
            //Local to world, relative to ground
            var targetVelocity = targetLocal.x * _ground.CurrentGroundRight + targetLocal.z * _ground.CurrentGroundForward;
            var currentVelocity = _rb.linearVelocity - baseVelocity;
            
            //Calculate the difference, pointing to the target
            var velDelta = targetVelocity - currentVelocity;
            //v = a * dt so a = v / dt
            var correction = velDelta / Time.fixedDeltaTime;
            //Dont move more than this magnitude or we overshoot and it causes jittering
            var correctionMagnitude = correction.magnitude;
            
            //Move into the target based on acceleration
            var accelerationVec = correction * _config.acceleration;
            accelerationVec = Vector3.ClampMagnitude(accelerationVec, correctionMagnitude);
            
            _rb.AddForce(accelerationVec, ForceMode.Acceleration);
        }

        private void AirMovement()
        {
            //Separate movement logic for not grounded state,
            //as in air the player should not be able to slow down as quickly as on ground
            
            //Max speed defines the radius of the circle around velocity that the player can move in
            var currentMaxSpeed = _input.Run ? _config.runMaxSpeed : _config.walkMaxSpeed;
            
            //Calculate the force the input would apply
            var accTerm = _config.airAcceleration;
            var accX = _config.look.right * (_input.Horizontal * accTerm);
            var accY = _config.look.forward * (_input.Vertical * accTerm);
            var acc = accX + accY;

            //Clamp to max velocity
            var velocityXZ = new Vector3(_rb.linearVelocity.x, 0, _rb.linearVelocity.z);
            var currentAllowedVelocity = Vector3.ClampMagnitude(velocityXZ, currentMaxSpeed);
            var finalVelocity = currentAllowedVelocity + acc * Time.fixedDeltaTime;

            var finalAllowedVelocity = Vector3.ClampMagnitude(finalVelocity, currentMaxSpeed);
            var allowedMoveVec = finalAllowedVelocity - currentAllowedVelocity;
            var finalAcc = allowedMoveVec / Time.fixedDeltaTime;
            
            _rb.AddForce(finalAcc, ForceMode.Acceleration);
        }

The Key to being able to walk on moving platforms are these lines:

var baseVelocity = Vector3.zero;
if (_ground.GroundRigidbody != null)
    baseVelocity = _ground.GroundRigidbody.GetPointVelocity(_config.groundCheck.position);

BaseVelocity then gets removed from the currentVelocity, making the Movement ignore it. This make it possible to stand on a platform that is moving at hundereds of km/h, while still being able to move around.

My Inspector setup looks like this:

The Player Collider also has a Physics Material with Friction set to Zero and Friction combine to Minimum. I did not want to use PhysX standard friction, as my movement naturally pushes the player to halt if he is not moving.

In order to get effects like terminal velocity and slowing down from high speeds, I also gave the Rigidbody some linear drag:

With 0.35, I get a terminal velocity of about 300 km/h when falling straight down, which is somewhat realistic. Also notice that the mass is still set to 1. The Movement work only with Acceleration, so changing the Mass has no effect on speed, jumps or any other movement mechanic.

One of the more complex challenges was developing a reliable Groundcheck and stopping the player from sliding off sloped platforms as long has he grounded. The final implementation looks like this:

public void FixedUpdate()
{
    GroundCheck();
    GroundStabilization();
}

private void GroundCheck()
{
    GroundRigidbody = null;
    CurrentGroundNormal = Vector3.up;
    
    if (BlockGround)
    {
        Grounded = false;
        return;
    }
    
    var ray = new Ray(_config.groundCheck.position, Vector3.down);
    var c = Physics.SphereCastNonAlloc(ray, _config.groundCheckRadius, _groundHits, _config.groundCheckRange, _config.groundLayer);

    if (c == 0)
    {
        Grounded = false;
        return;
    }
    
    int minIndex = -1;
    float currentMinAngle = 999;
    
    for (int i = 0; i < c; i++)
    {
        var cHit = _groundHits[i];

        var groundAngle = Vector3.Angle(cHit.normal, Vector3.up);
        
        if (groundAngle < _config.maxGroundAngle && groundAngle < currentMinAngle)
        {
            currentMinAngle = groundAngle;
            minIndex = i;
        }
    }
    
    if (minIndex == -1)
        return;
    
    var hit = _groundHits[minIndex];
    
    CurrentGroundNormal = hit.normal;
    CurrentGroundForward = Vector3.Cross(_config.look.right, CurrentGroundNormal);
    CurrentGroundRight = Vector3.Cross(CurrentGroundNormal, CurrentGroundForward);
    Grounded = true;
    GroundRigidbody = hit.rigidbody;
}

public void GroundStabilization()
{
    if (!Grounded)
        return;

    if (CurrentGroundNormal.sqrMagnitude <= 0.01f)
        return;
    
    //Counteract gravity on slopes in order to make the player stand steadily
    var slideForce = Vector3.ProjectOnPlane(Physics.gravity, CurrentGroundNormal);
    _rb.AddForce(-slideForce, ForceMode.Acceleration);
}

public void OnDrawGizmos()
{
    Gizmos.color = Grounded ? Color.red : Color.green;

    var config = _config;
    Gizmos.DrawWireSphere(config.groundCheck.position, config.groundCheckRadius);
    Gizmos.DrawWireSphere(config.groundCheck.position - new Vector3(0, config.groundCheckRange, 0), config.groundCheckRadius);
}

GroundCheck uses a SphereCastNonAlloc to find all the ground under the player without allocating any runtime memory. I use the ground hit with the minimum slope that I can find. This might not be ideal in all cases, but worked well in my tests. In GroundStabilization I apply a force to counteract gravity pushing the player off a sloped platform as long as he is grounded.

The jump logic is simpler:

    public void FixedUpdate()
    {
        ExtraGravity();
        
        _jumpCooldown -= Time.deltaTime;

        if (_jumpCooldown <= 0)
            _ground.BlockGround = false;
        
        if (_input.Jump && _jumpCooldown <= 0 && _ground.Grounded)
        {
            _jumpCooldown = _config.jumpDelay;
            _ground.BlockGround = true;

            var vel = _rb.linearVelocity;
            var baseVelocity = Vector3.zero;
            
            if (_ground.GroundRigidbody != null)
                baseVelocity = _ground.GroundRigidbody.GetPointVelocity(_config.groundCheck.position);
            
            _rb.linearVelocity = new Vector3(vel.x, baseVelocity.y, vel.z);
            var upDir = (_ground.CurrentGroundNormal + Vector3.up) * 0.5f;
            _rb.AddForce(upDir * (_config.jumpForce * Time.fixedDeltaTime), ForceMode.VelocityChange);
        }
    }
    
    private void ExtraGravity()
    {
        if (_ground.Grounded)
            return;
        
        _rb.AddForce(Vector3.down * _config.extraGravity, ForceMode.Acceleration);
    }

I reset the Y velocity before jumping in order to make sure that the jump is consistent. In order to make sure players can also jump while they are on a platform moving upwards, I get the current Y velocity of the rigidbody the player is standing on, instead of just setting Y to zero.

Also note that I left the Physics.gravity unchanged and kept the standard -9.81 m/s². This feels too slow for a first person movement so I added some extra gravity to the player. This is not optimal, but I don't want to change the gravity for all objects as I will need realistic gravity for the other systems in this project.

Final Sandbox Devlogs

Part 1 of 1

Devlogs for my physics based Sandbox game.