Apply input while still keeping current velocity

Hi. When my characterController-controlled fps character goes airborne, I made him keep his current velocity until he lands again. This works, but I want my character to still receive movement input and apply it to the airVelocity variable. I tried this for while, but I couldn’t figure it out. Here’s my script (there are no bugs, and doesn’t contain any failed attempts at this problem):

using SerializableDictionary.Scripts;
using System;
using Ami.BroAudio;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;


[RequireComponent(typeof(CharacterController))]
public class Player : MonoBehaviour
{

    [Header("Motion")]

    [SerializeField] private Movement movement; // struct for movement values
    [SerializeField] private GadgetSettings gadgetSettings; // struct for gadget values
    [SerializeField] private float gravityScale; // how strong gravity is in this particular scene (remember to move this to manager GameObject)
    [SerializeField] private float buoyancy; // how quickly the player stops falling when entering water
    [SerializeField] private float swimSpeed; // how quickly the player swims
    [SerializeField, Frequency] private float freq = 800; // value for BroAudio effect when underwater
    [SerializeField] public float fadeTime = 1; // another value for BroAudio effect when underwater

    [Header("References")]

    [SerializeField] private CharacterController charcontrol; //character controller reference
    [SerializeField] private Weapon weaponScript; //Weapon script reference
    [SerializeField] private Inventory inventoryScript; //Inventory script reference
    [SerializeField] private Animator anim; // Animator Reference
    [SerializeField] private Animator armAnim; // Arm Animator Reference

    [Header("Other")]

    [SerializeField] public int maxWeapons;// how many weapons player can carry
    [SerializeField] private Transform groundDetector; // gameObject at player foot for detecting ground
    [SerializeField] private LayerMask ground; // objects considered as ground
    [SerializeField] private SerializableDictionary<string, SoundID> stepSounds; // dictionary for footstep audio clips
    [SerializeField] private SoundID[] grunts; // array holding the grunt sounds according to landing velocity in ascending order

    [HideInInspector] public int waterLevel; // how deep the player is in water

    // References
    private GameObject postprocess; // Main PostProcessing object reference
    private GameObject postprocesswater; // Underwater PostProcessing object reference

    // Physics and Movement
    public float sprintLeft; // sprinting energy remaining
    private bool sprintOut; // whether player can sprint
    private float gravity = -9.81f; // gravity multiplier
    public float fallVelocity; // current falling speed
    private float landVelocity; // speed the player was falling last time they landed
    private Vector2 airVelocity; // player's xz-axis velocity last time they left the ground
    private static Vector3 velocity; // current movement velocity in all axes

    // Misc.
    private List<AudioClip> prevSteps = new List<AudioClip>(); // previous footstep noises played
    private float timeSinceLastFootstep; // time since the last footstep
    private Vector2 input; // xz-movement values input through controller

    // States
    private bool sprint; // whether player is pressing sprint key
    private bool jump; // whether player is pressing jump key
    [HideInInspector] public bool crouch; // whether player is crouching
    private bool walk; // whether player is walking

    // States
    private bool isGrounded; // // whether player is grounded
    private bool isJumping; // whether player is actually jumping
    private bool isSprinting; // whether player is actually sprinting

    [Range(0, 200)]
    public int hp = 100; // health remaining;

    [Range(0, 100)]
    public int shield = 0; // shield remaining;

    private void Start()
    {
        sprintLeft = 100; // initialize sprint
        Spawn(); //spawn Player
        if (GameObject.Find("PostProcessing") != null)
            postprocess = GameObject.Find("PostProcessing");
        if (GameObject.Find("PostProcessing Water") != null)
            postprocesswater = GameObject.Find("PostProcessing Water");
    }//Start

    private void Update()
    {
        UpdateVars();
        Animate();
    }//Update

    void FixedUpdate()
    {
        UpdateVars();
        ApplyRotation();
        ApplyGravity();
        ApplyMovement();
        Footsteps();
    }//FixedUpdate


    void UpdateVars()
    {
        //TASK: Update the variables
        if (charcontrol.isGrounded && !isGrounded) // the moment the player lands
        {
            landVelocity = fallVelocity; // lock the current fallingVelocity into landVelocity
            Landing();
        }//if
        if (isGrounded && !charcontrol.isGrounded) // the moment the player leaves the ground
        {
            airVelocity.x = velocity.x; // lock the current xz-axis velocity into airVelocity
            airVelocity.y = velocity.z;
        }//if
        isGrounded = charcontrol.isGrounded; // update isGrounded

        //States
        isJumping = jump && (isGrounded || waterLevel == 3); // decide whether player can jump
        isSprinting = sprint && waterLevel < 2 && !sprintOut; // decide whether player can sprint

        if (crouch) // update the collision geometry when crouching
        {
            charcontrol.height = 0.5f;
            charcontrol.center = new Vector3(0, -0.5f, 0);
        }//if
        else
        {
            charcontrol.height = 2;
            charcontrol.center = new Vector3(0, 0, 0);
        }//else

        if (postprocesswater != null) // adjust postprocessing according to state
        {
            if (waterLevel == 3) // if player is submerged
            {
                postprocess.SetActive(false);
                postprocesswater.SetActive(true);
                BroAudio.SetEffect(Effect.LowPass(freq, fadeTime));
            }//if
            else
            {
                postprocess.SetActive(true);
                postprocesswater.SetActive(false);
                BroAudio.SetEffect(Effect.ResetLowPass(fadeTime));
            }//else
        }//if

        if (isJumping)
        {
            if (inventoryScript.gadgets[2] == "Jump" && inventoryScript.gadgets[3] == "Jump") // if player has two jump boots
                fallVelocity = gadgetSettings.doubleJumpBootMultiplier * movement.jumpHeight;
            else if (inventoryScript.gadgets[2] == "Jump" || inventoryScript.gadgets[3] == "Jump") // if player has only one jump boot
                fallVelocity = gadgetSettings.jumpBootMultiplier * movement.jumpHeight;
            else
                fallVelocity = movement.jumpHeight;
            jump = false; // player will not jump again until jump key is released and pressed again
        } //if
    }//UpdateVars

    void Animate()
    {
        // TASK: adjust animator parameters
        armAnim.SetInteger("Weapon", weaponScript.loadout[weaponScript.currentIndex].GunNum); // adjust arms to conform to current gun
        if (Mathf.Abs(velocity.x) > 5 || Mathf.Abs(velocity.z) > 5) // if player is moving
        {
            if (walk)
                anim.SetInteger("Speed", 3); // walking
            else if (isSprinting)
                anim.SetInteger("Speed", 1); // sprinting
            else
                anim.SetInteger("Speed", 2); // running
        }//if
        else
            anim.SetInteger("Speed", 0); // standing

        if (velocity.z > 0)
            anim.SetInteger("Run Dir", 0); // running forwards
        else
            anim.SetInteger("Run Dir", 3); // running backwards
        anim.SetBool("IsGrounded", isGrounded); // decide whether to fall or stand
    }//Animate

    private void ApplyGravity()
    {
        // TASK: apply gravity
        if (isGrounded && fallVelocity < 0) // if on ground
            fallVelocity = -1; // set to -1 so that player doesn't bounce
        else
            fallVelocity += gravity * gravityScale * Time.deltaTime; // increase falling speed

        if (inventoryScript.gadgets[0] == "Parachute" && jump && !isGrounded && fallVelocity < 0) // if parachute is open and player is not moving up
            fallVelocity = -gadgetSettings.parachuteSpeed; // adjust accordingly
        if (inventoryScript.gadgets[0] == "Jetpack" && jump && !isGrounded) // if jetpack is active
        {
            fallVelocity += gadgetSettings.jetpackStrength; // increase velocity gradually
            fallVelocity = Mathf.Clamp(fallVelocity, -2, gadgetSettings.maxJetpack); // clamp at maxJetpack
        }//if
        velocity.y = fallVelocity; // apply falling speed to main velocity Vector3
    }//ApplyGravity

    private void ApplyRotation()
    {
        velocity = Quaternion.Euler(0.0f, GameObject.Find("Camera").transform.eulerAngles.y, 0.0f) * new Vector3(input.x, 0.0f, input.y); // adjust input to conform to looking direction
    }//ApplyRotation

    private void ApplyMovement()
    {
        var targetSpeed = 0.0f; // initialize targetSpeed
        targetSpeed = isSprinting ? movement.speed * movement.sprintMultiplier : movement.speed; // adjust speed depending on whether player is sprinting
        targetSpeed = walk || crouch ? movement.speed * movement.walkSpeed : targetSpeed; // adjust speed depending on whether player is walking or crouching
        if (inventoryScript.gadgets[2] == "Speed" && inventoryScript.gadgets[3] == "Speed") // if player has two speed boots
            targetSpeed *= gadgetSettings.doubleSpeedBootMultiplier;
        else if (inventoryScript.gadgets[2] == "Speed" || inventoryScript.gadgets[3] == "Speed") // if player has only one speed boot
            targetSpeed *= gadgetSettings.speedBootMultiplier;
        if (!isGrounded)
           velocity = new Vector3(airVelocity.x, fallVelocity, airVelocity.y); // if player is airborne, override current speed and replace it with airVelocity
        movement.currentSpeed = Mathf.MoveTowards(movement.currentSpeed, targetSpeed, movement.acceleration * Time.deltaTime); // accelerate

        charcontrol.Move(velocity * movement.currentSpeed * Time.deltaTime); // apply the movement

        if (isSprinting)
        {
            sprintLeft -= movement.sprintTime; // detract sprint energy
            if (sprintLeft < 0) // if you run out of sprint
            {
                sprintLeft = 0;
                sprintOut = true;
            }//if
        }//if
        else if (sprintLeft < 100) // if not sprinting and sprint is not full
            sprintLeft += movement.sprintRecover; // restore some energy

        Mathf.Clamp(sprintLeft, 0, 100);

        if (sprintLeft > 10) // when sprintLeft reaches the 10% mark after being depleted
            sprintOut = false; // player can sprint again
    }//ApplyMovement

    void Footsteps()
    {
        //TASK: Play footstep sounds
        RaycastHit hit;
        if (Physics.Raycast(groundDetector.position, Vector3.down, out hit, 0.1F, ground)) // Detect what player is standing on
            if ((Mathf.Abs(velocity.x) > 5 || Mathf.Abs(velocity.z) > 5) && isGrounded) // if player is moving on ground
            {
                float timeTillNextFootstep; // initiate timeCounter
                if (walk || crouch)
                    timeTillNextFootstep = 0.7f; // a longer period of time
                else if (isSprinting)
                    timeTillNextFootstep = 0.35f; // a shorter peripd of time
                else
                    timeTillNextFootstep = 0.5f;  // a moderate period of time
                if (Time.time - timeSinceLastFootstep > timeTillNextFootstep) // if long enough has gone by since last footstep
                {
                    string clip = "Rock"; // if item player is on is not in the dictionary, play this sound
                    if (waterLevel == 0) // if player is not in contact with water
                    {
                        if (stepSounds.ContainsKey(hit.transform.gameObject.tag)) // if dictionary contains a key for what the player is on
                            clip = hit.transform.gameObject.tag; // set the target clip to that key
                    }
                    else if (waterLevel == 1) // below waist
                        clip = "ShallowWater";
                    else if (waterLevel >= 2) // above waist
                        clip = "DeepWater";
                    BroAudio.Play(stepSounds.Get(clip)); // play the sound
                    timeSinceLastFootstep = Time.time; // next tick
                } //if
            }//if
    }//Footsteps

    void LoseHealth(int lostHealth)
    {
        //TASK: Subtract health from player
        if (shield > 0) // if player has shield
            shield -= lostHealth; // lose shield
        else if (hp < 20) // Lose less health when low on health to pump more adrenaline into human player
            hp -= lostHealth / 2;
        else
            hp -= lostHealth;

        hp = Mathf.Clamp(hp, 0, 200); // player can have up to 200 hp
        shield = Mathf.Clamp(shield, 0, 100); // but only 100 shield
    }//LoseHealth

    void Spawn()
    {
        //TASK: Spawn player
        GameObject[] spawnPoints = GameObject.FindGameObjectsWithTag("Spawn Point"); // make an array of every spawn point

        if (spawnPoints.Length > 0) // if there are spawn points
        {
            Vector3 spawnPos = spawnPoints[UnityEngine.Random.Range(0, spawnPoints.Length)].transform.position; // pick a random one and find its position
            transform.position = spawnPos; // spawn at point
        }//if
    }//Spawn

    void Landing()
    {
        RaycastHit hit;
        if (Physics.Raycast(groundDetector.position, Vector3.down, out hit, 0.1F, ground)) // Detect what player has landed on
        {
            string clip = "Rock"; // if item is not in dictionary, it will play a rock landing sound
            if (waterLevel == 0) // not in contact at all
            {
                if (stepSounds.ContainsKey(hit.transform.gameObject.tag)) // check if dictionary contains this item
                    clip = hit.transform.gameObject.tag; // if it does, assign it
            }
            else if (waterLevel == 1) // less than waist deep
                clip = "ShallowWater";
            BroAudio.Play(stepSounds.Get(clip)); // play sound
        }
        int landForce = -1; // initial declaration
        if (landVelocity < -9) // if player hit the ground HARD
            landForce = 3; // play a painful grunt (there is no fall damage)
        else if (landVelocity < -7) // if player hit the ground at a medium-high speed
            landForce = 2; // play a moderate grunt
        else if (landVelocity < -5) // if player hit the ground at a medium speed
            landForce = 1; // play a smaller grunt
        else if (landVelocity < -3) // if player hit the ground at a low speed
            landForce = 0; // play a quiet grunt
        if (landForce != -1) // falls from very low heights will not trigger a grunt
            BroAudio.Play(grunts[landForce]); // play the grunt
    }

    #region Inputs
    public void OnMove(InputAction.CallbackContext context)
    {
        input = context.ReadValue<Vector2>();
        velocity = new Vector3(input.x, 0.0f, input.y);
    }

    public void OnJump(InputAction.CallbackContext context)
    {
        if (waterLevel == 3) // if completely submerged
        {
            if (context.performed) // player can continually jump
                jump = true;
        }
        else
        {
            if (context.started) // otherwise they can only jump once
                jump = true;
        }
        if (context.canceled)
            jump = false;
    }
    
    public void OnSprint(InputAction.CallbackContext context)
    {
        crouch = false; // sprinting will uncrouch you
        sprint = context.performed;
    }

    public void OnWalk(InputAction.CallbackContext context)
    {
        walk = context.performed;
    }

    public void OnCrouch(InputAction.CallbackContext context)
    {
        crouch = context.performed;
    }

    public void OnCrouchToggle(InputAction.CallbackContext context)
    {
        if (context.started)
            crouch = !crouch;
    }

    public void OnMelee(InputAction.CallbackContext context)
    {
        if (context.started)
            armAnim.SetTrigger("Melee");
    }
    #endregion
}// class

[Serializable]
public struct Movement
{
    public float speed; // how fast player moves when not sprinting
    public float sprintMultiplier; // value to multiply to speed when sprinting
    public float acceleration; // how quickly player reaches top speed
    public float walkSpeed; // how fast player moves when holding walk
    public float sprintTime; // how much sprint energy is subtracted each tick while sprinting
    public float sprintRecover; // how much sprint energy is added each tick while not sprinting
    public float jumpHeight; // how high player jumps
    [HideInInspector] public float currentSpeed; // current velocity along the xz-axis
}

[Serializable]
public struct GadgetSettings
{
    public float speedBootMultiplier; // value to multiply to speed when speed boot is equipped
    public float doubleSpeedBootMultiplier; // value to multiply to speed when two speed boots are equipped
    public float jumpBootMultiplier; // value to multiply to jumpHeight when jump boot is equipped
    public float doubleJumpBootMultiplier; // value to multiply to jumpHeight when two jump boots are equipped
    public float maxJetpack; // max flying velocity in jetpack
    public float jetpackStrength; // how quickly player acclerates upwards with jetpack
    public float parachuteSpeed; // how quickly player falls with open parachute
}

Here are the particular areas related to this:

private void ApplyGravity()
    {
        // TASK: apply gravity
        if (isGrounded && fallVelocity < 0) // if on ground
            fallVelocity = -1; // set to -1 so that player doesn't bounce
        else
            fallVelocity += gravity * gravityScale * Time.deltaTime; // increase falling speed

        if (inventoryScript.gadgets[0] == "Parachute" && jump && !isGrounded && fallVelocity < 0) // if parachute is open and player is not moving up
            fallVelocity = -gadgetSettings.parachuteSpeed; // adjust accordingly
        if (inventoryScript.gadgets[0] == "Jetpack" && jump && !isGrounded) // if jetpack is active
        {
            fallVelocity += gadgetSettings.jetpackStrength; // increase velocity gradually
            fallVelocity = Mathf.Clamp(fallVelocity, -2, gadgetSettings.maxJetpack); // clamp at maxJetpack
        }//if
        velocity.y = fallVelocity; // apply falling speed to main velocity Vector3
    }//ApplyGravity

    private void ApplyRotation()
    {
        velocity = Quaternion.Euler(0.0f, GameObject.Find("Camera").transform.eulerAngles.y, 0.0f) * new Vector3(input.x, 0.0f, input.y); // adjust input to conform to looking direction
    }//ApplyRotation

    private void ApplyMovement()
    {
        var targetSpeed = 0.0f; // initialize targetSpeed
        targetSpeed = isSprinting ? movement.speed * movement.sprintMultiplier : movement.speed; // adjust speed depending on whether player is sprinting
        targetSpeed = walk || crouch ? movement.speed * movement.walkSpeed : targetSpeed; // adjust speed depending on whether player is walking or crouching
        if (inventoryScript.gadgets[2] == "Speed" && inventoryScript.gadgets[3] == "Speed") // if player has two speed boots
            targetSpeed *= gadgetSettings.doubleSpeedBootMultiplier;
        else if (inventoryScript.gadgets[2] == "Speed" || inventoryScript.gadgets[3] == "Speed") // if player has only one speed boot
            targetSpeed *= gadgetSettings.speedBootMultiplier;
        if (!isGrounded)
           velocity = new Vector3(airVelocity.x, fallVelocity, airVelocity.y); // if player is airborne, override current speed and replace it with airVelocity
        movement.currentSpeed = Mathf.MoveTowards(movement.currentSpeed, targetSpeed, movement.acceleration * Time.deltaTime); // accelerate

        charcontrol.Move(velocity * movement.currentSpeed * Time.deltaTime); // apply the movement

        if (isSprinting)
        {
            sprintLeft -= movement.sprintTime; // detract sprint energy
            if (sprintLeft < 0) // if you run out of sprint
            {
                sprintLeft = 0;
                sprintOut = true;
            }//if
        }//if
        else if (sprintLeft < 100) // if not sprinting and sprint is not full
            sprintLeft += movement.sprintRecover; // restore some energy

        Mathf.Clamp(sprintLeft, 0, 100);

        if (sprintLeft > 10) // when sprintLeft reaches the 10% mark after being depleted
            sprintOut = false; // player can sprint again
    }//ApplyMovement

Try changing line 217 to:

targetSpeed *= 0.5f; // half the target speed while in the air
1 Like

Hey!

Soo… there are a lot of problems with your code but I’m assuming that you are learning so that’s fine, code is bad until it becomes good as they say!

Just please for the love of God don’t use Time.deltaTime within FixedUpdate, that makes no sense :smiling_face_with_tear: use Time.fixedDeltaTime instead!

I think I managed to understand what goes wrong with your code when airborne but I honestly can’t provide any working code that would match your structure as… well let’s say that it is very unconventional.

However, the problem seems to be with the line 217. You override the value of velocity by setting it to be

velocity = new Vector3(airVelocity.x, fallVelocity, airVelocity.y);

The problem is that you already called the method ApplyRotation() prior to that as this point (in your FixedUpdate callback), so the actual input data given by the controller is completely lost when you override like shown above. And it gets overriden by the data you had prior to the jump, so your velocity when airborne keeps its “grounded” direction and its “grounded” magnitude as well. The latter is what you seek, the former is the issue.

A simple first step should probably be simply taking into account the current input values you have when you update your velocity on line 217. Something as simple as this should work, in theory:

velocity = new Vector3(
    x: InputDirection.x * InputMagnitude * TargetSpeed,
    y: gravityForce * Vector3.down,
    x: InputDirection.z * InputMagnitude * TargetSpeed
) * Time.fixedDeltaTime;

To make debugging easier though, consider breaking your code down into more defined sections. It would give you a clearer structure, which will make it faster and simpler to spot and resolve issues when they come up. But it takes experience so step by step is also OK, we all start from somewhere, the most important is that you keep trying! :relieved:

This might help you in the future but not right away, as a rule of thumb when designing a custom character controller, the global “sections” or “steps” are as follow:

  • Fetching the Input’s direction and magnitude (in Update).
  • Numerical Integration to get your next velocity (in FixedUpdate).
  • Collision Resolution with the displacement vector obtained by the previous step (in FixedUpdate).
  • Overlapping Resolution post physics simulation (in LateFixedUpdate, which isn’t natively enabled by Unity).

Here you are using the CharacterController given by Nvidia PhysX which is wrapped by Unity, so step 2 and 3 are handled internally. You can already focus on step 1 and 2.

Update should only fetch the input data and update your player’s state.

1 Like

Time.deltaTime holds the same value as Time.fixedDeltaTime when used from FixedUpdate.

But you have raised an issue in that a Character Controller is meant to be used from Update. So they should call their ApplyRotation, ApplyMovement etc from Update instead, and then switch to using Time.deltaTime instead of Time.fixedDeltaTime.

1 Like

@zulo3d Oh you right I didn’t know the value was automatically switched like this! So I don’t know about the API of the Character Controller itself and what it does, maybe you are right and it should be called from Update, however any numerical integration should be done in FixedUpdate for sure. What I simply mean by that is that your integration needs numerical stability so computing velocities in Update won’t give you stable data (since delta time varies).

1 Like

And this is why I eventually gave up on using the Character Controller and switched to using a rigidbody. The character controller is meant to be used from Update and as a result the movement won’t be consistent when running on devices with very different frame rates. Although I’m sure it’ll be fine for most casual games that don’t rely on pixel perfect jumping across gaps. :slight_smile:

1 Like

Oh I see, well then yes it is an issue indeed :sweat_smile:.
Using a rigidbody is definitely the correct approach.

2 Likes

@zulo3d Alright, so I made some research and I thought the results were interesting enough for the both of us to share them here :slightly_smiling_face:.

As I had previously mentioned, the Character Controller of Unity is actually a simple wrapper around the CCT implementation of Nvidia PhysX.

The documentation for the CCT is publicly available at this link: CCT Documentation.

The C++ source code of the CCT can be found there: CctCharacterController.cpp.

The wrapper in C# made by Unity can be checked here: CharacterController.bindings.cs.

So, the interesting part is that the C++ code of Nvidia is actually using a Kinematic Rigidbody, which is the correct approach, but Unity only wraps the high level interface of the CCT. So from the C# code, you don’t have access to the Rigidbody Actor and thus no way to extract anything and get a Rigidbody Component (which I think is stupid but anyway…).

However, the C++ code that moves and updates the underlying Kinematic Actor follows the structure I suggested in my previous post, and thus can be safely called from FixedUpdate.

It’s weird that Unity writes in their manual an example of the Move() method called from Update.

Edit:

OK so I dug even further and now I understand why Unity doesn’t care about Update or FixedUpdate for the CCT. The thing is that the C++ code itself is only about the collision resolution and overlapping resolution parts (so step 3 and 4 from my previous outline). This part technically can be done in either Update or FixedUpdate, because it will just sweep the physics world to move the CCT.

Now technically the CCT’s position is only updated within the FixedUpdate phase of the game loop when the physics simulation is about to be executed, so ultimately, since FixedUpdate is 99.99% of the time at a lower frame rate than Update, one would waste CPU cycles if calling the Move() or SimpleMove() method from Update, the transforms of the physics world and the rendering scene being only synchronized once per FixedUpdate iteration.

For steps 1 and 2, Inputs should still be fetched from Update (obviously haha) and any numerical integration naturally in FixedUpdate.

But if one doesn’t need physics based movement (so no numerical integration needed), then technically everything (Inputs, Movement, States, etc…) could be done in Update and will do nothing but to loose just a few CPU cycles.

1 Like

Yes, I agree with you. You can “safely” call CharacterController.Move from anywhere as there isn’t really much going on behind the scenes because as you say it simply does a sweep (CapsuleCast) to test for obstacles in the direction of the ‘motion’ and deflects the movement if necessary.

But the main reason why the Character Controller should be used from Update is because it’s not a rigidbody and therefore it’s movement won’t be interpolated when moving it from FixedUpdate.

1 Like

Mmh… I disagree with that part. The Character Controller is using a Rigidbody you just don’t have access to it. Interpolation is never used on Kinematic Actors, only on Dynamic Actors, so it also doesn’t seem to be a problem here?

1 Like

Interpolation is used on kinematic rigidbodies when moving them as intended with MovePosition and MoveRotation from FixedUpdate.

2 Likes

Oh I see, you are right these two methods can be used and yes the first one is indeed using interpolation because it allows PhysX to interpolate the movement induced by Kinematic Actors to make them interact with Dynamic Actors (internally using SetKinematicTarget()).

So yes indeed if you want non-scripted Dynamic ↔ Kinematic interactions you have to use the MovePosition() and MoveRotation() methods.

I forgot about this because I use the second approach which is to set the position of the Rigidbody using SetGlobalPose() internally, accessible in Unity via simply updating the position of the Rigidbody.

This doesn’t perform any interpolation and in Nvidia’s documentation they explain that you don’t really want have the natural Dynamic ↔ Kinematic interactions (so when using Interpolation between the two) because it makes “weird” results (the chapter is called “Character Interactions: CCT-vs-dynamic actors” in the doc I mentioned). That’s why I didn’t even consider the option and went like interpolation and kinematic? No never :joy:.

Anyway sorry, but that was progress that’s great!
Thank you very much. :slightly_smiling_face:

1 Like

Hi everyone, thanks for the replies. But I figured it out on my own. Here are my changes:

Following zulo3d’s instructions, I removed fixedUpdate and changed Update to contain my fixedUpdate functions:

private void Update()
{
    UpdateVars();
    Animate();
    ApplyRotation();
    ApplyGravity();
    ApplyMovement();
    Footsteps();
}//Update
[Serializable]
public struct Movement
{
    //...//
    public float acceleration; // how quickly player reaches top speed
    public float airResistance; // this is new. how much player's control is dropped when airborne
    public float walkSpeed; // how fast player moves when holding walk
    //...//
}
private void ApplyRotation()
{
    velocity = Quaternion.Euler(0.0f, GameObject.Find("Camera").transform.eulerAngles.y, 0.0f) * new Vector3(input.x, 0.0f, input.y); // adjust input to conform to looking direction
    airVelocity += new Vector2(velocity.x / movement.airResistance, velocity.z / movement.airResistance); // this is new
}//ApplyRotation