Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Movement Consistency Problems (Distance and speed within a time).

Discussion in 'Scripting' started by HiddenMonk, Jun 5, 2016.

  1. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    Main issue
    --------
    I am just overall unsure how to handle timed movement over a variable timestep. How do I get consistent movement distance as well as speed. Also, friction and drag must not ruin that consistency.
    -------

    I am running into an issue with trying to get consistent movement for a dash movement.
    How it works is, you press a button (A or D) to dash left or right. Pressing the button starts a timer, you start moving at a constant speed, and then when the timer is up you stop.
    The issue is I am running this code within a variable timestep (Update instead of FixedUpdate).

    Using what I learned in my previous thread here
    http://forum.unity3d.com/threads/movement-consistency-and-timesteps-framerate.365703/
    I am able to get fairly good framerate independence and movement consistency with lots of my movements, but this dash over time movement is causing problems.

    I have something that works, but I dont know how to blend it in with my other velocity code.

    Here is a video demonstrating what I see.


    Here is the code. I extracted a lot of code from my main player controller into this piece of code to give a example of the issue. I dont like posting a wall of code when asking for help and this seems like a lot, but you can probably just focus on the Dash method and the UpdateMovement method.
    Just drop this component on a capsule and watch the video to see how I demonstrate things in case you want to test things yourself.
    Remember to set the character controller minMoveDistance to 0 otherwise there will be problems. For some reason we cant set this via code...

    Code (CSharp):
    1.  
    2. using System;
    3. using UnityEngine;
    4.  
    5. [RequireComponent(typeof(CharacterController))]
    6. public class TestMovementConsistency : MonoBehaviour
    7. {
    8.     public float friction = 20;
    9.     public float drag;
    10.     public bool testInAirOnly; //just in case charactercontroller collision is affecting our movement consistency, we will test in air.
    11.     public float drawRayTime = 10f;
    12.     public bool badDashVersion;
    13.  
    14.     float dashSpeed = 12f;
    15.  
    16.     Timer timer = new Timer();
    17.     VariableFixedUpdater variableFixedUpdater;
    18.  
    19.     Vector3 velocity;
    20.     Vector3 impulses;
    21.     Vector3 leftoverImpulse;
    22.     Vector3 forces;
    23.     Vector3 forcesNoFriction;
    24.     bool handleFriction;
    25.  
    26.     Vector3 dashMovement;
    27.  
    28.     CharacterController controller;
    29.  
    30.     void Awake()
    31.     {
    32.         variableFixedUpdater = new VariableFixedUpdater(UpdateMovement);
    33.         controller = GetComponent<CharacterController>();
    34.     }
    35.  
    36.     void Update()
    37.     {
    38.         Debug.Log("Remember to set CharacterController minMoveDistance to 0 since this cannot be set via script!!! It is very important!! Comment this out after you set it.");
    39.      
    40.         Vector3 prevPosition = transform.position;
    41.  
    42.         AddForce(leftoverImpulse, ForceMode.Impulse);
    43.         leftoverImpulse = Vector3.zero;
    44.  
    45.         Dash();
    46.         Jump();
    47.         Gravity();
    48.  
    49.         velocity += impulses;
    50.         impulses = Vector3.zero;
    51.  
    52.         //This will look at our deltaTime and cut it up so that it stays fairly consistent and call our UpdateMovement with each delta cut.
    53.         variableFixedUpdater.Update();
    54.  
    55.         forces = Vector3.zero;
    56.         forcesNoFriction = Vector3.zero;
    57.  
    58.         //This shows our movement distance consistency
    59.         Debug.DrawRay(transform.position, Vector3.forward, Color.red, drawRayTime);
    60.         //This shows our movement speed consistency
    61.         Debug.DrawRay(transform.position, Vector3.up * (1f + (Vector3.Distance(prevPosition, transform.position) / Time.deltaTime)), Color.red, drawRayTime);
    62.     }
    63.  
    64.     void UpdateMovement(float deltaTime)
    65.     {
    66.         Vector3 prevPosition = transform.position;
    67.  
    68.         Vector3 acceleration = velocity + (forces * deltaTime);
    69.         if(handleFriction) acceleration = ApplyFriction(acceleration, deltaTime);
    70.         acceleration += (forcesNoFriction * deltaTime);
    71.         acceleration = ApplyDrag(acceleration, deltaTime);
    72.  
    73.         controller.Move(acceleration * deltaTime);
    74.  
    75.         velocity = (transform.position - prevPosition) / deltaTime;
    76.     }
    77.  
    78.     void Dash()
    79.     {
    80.         if(dashMovement == Vector3.zero)
    81.         {
    82.             dashMovement = GetDirection() * dashSpeed;
    83.             if(dashMovement != Vector3.zero)
    84.             {
    85.                 timer.SetTimeReference(.3f);
    86.                 handleFriction = false;
    87.             }
    88.         }
    89.  
    90.         //This will give inconsistent results with both the movement distance and movement speed.
    91.         if(badDashVersion)
    92.         {
    93.             if(dashMovement != Vector3.zero)
    94.             {
    95.                 Vector3 clampedDashMovement = Vector3.ClampMagnitude(dashMovement - velocity, dashSpeed);
    96.                 clampedDashMovement *= 100f;
    97.  
    98.                 if(!timer.IsTimeDone())
    99.                 {
    100.                     AddForce(clampedDashMovement, ForceMode.Force);
    101.                 }else{
    102.                     //Since we are in a variable timestep (not FixedUpdate), our timer will end, but the deltaTime would be greater then our target timer end time.
    103.                     //So we find out how much time was really left in our timer compared to the Time.deltaTime and use that percentage on our movement.
    104.                     //We add it to the forcesNoFriction since dash movement is not affected by friction.
    105.                     forcesNoFriction = clampedDashMovement * timer.RemainderPercent();
    106.                     handleFriction = true;
    107.                     dashMovement = Vector3.zero;
    108.                 }
    109.             }
    110.         }
    111.  
    112.         //This will give consistent results, however, drag will not work with this.
    113.         //Also, here we are using Move, but we would need to set something up to have this movement affect all our addforces and what not.
    114.         //We would also ideally not want to call move here since we are also calling Move in our UpdateMovement, which can lower performance.
    115.         if(!badDashVersion)
    116.         {
    117.             if(dashMovement != Vector3.zero)
    118.             {
    119.                 if(!timer.IsTimeDone())
    120.                 {
    121.                     //We move at a constant rate
    122.                     controller.Move(dashMovement * Time.deltaTime);
    123.                 }else{
    124.                     //Like explained above, we use the remaining time to decide how much is left of our movement to finish.
    125.                     controller.Move((dashMovement * timer.RemainderPercent()) * Time.deltaTime);
    126.                     //We now need to add 1 more full dashMovement, but this time have it be affected by friction
    127.                     //Se we addforce the remaining of what we just cut off from above to be affected by friction
    128.                     AddForce(dashMovement - (dashMovement * timer.RemainderPercent()), ForceMode.Impulse);
    129.                     //And then the leftover of the above will be ran next frame to complete the full dashMovement affected by friction
    130.                     leftoverImpulse = dashMovement * timer.RemainderPercent();
    131.                     handleFriction = true;
    132.                     dashMovement = Vector3.zero;
    133.                 }
    134.             }
    135.         }
    136.     }
    137.  
    138.     void Gravity()
    139.     {
    140.         if(!testInAirOnly) AddForce(-Vector3.up * 40f, ForceMode.Force);
    141.     }
    142.  
    143.     void Jump()
    144.     {
    145.         if(Input.GetKeyDown(KeyCode.Space)) AddForce(Vector3.up * 10f, ForceMode.Impulse);
    146.     }
    147.  
    148.     void AddForce(Vector3 velocity, ForceMode forceMode)
    149.     {
    150.         if(forceMode == ForceMode.Force) forces += velocity;
    151.         if(forceMode == ForceMode.Impulse) impulses += velocity;
    152.     }
    153.  
    154.     Vector3 GetDirection()
    155.     {
    156.         if(Input.GetKeyDown(KeyCode.A)) return Vector3.left;
    157.         if(Input.GetKeyDown(KeyCode.D)) return Vector3.right;
    158.         return Vector3.zero;
    159.     }
    160.  
    161.     public Vector3 ApplyFriction(Vector3 velocity, float deltaTime)
    162.     {
    163.         if(!testInAirOnly && (!controller.isGrounded || impulses.y > 0)) return velocity; //A quick hack for this test to make sure friction doesnt activate if we are jumping
    164.         return velocity * (1f / (1f + (friction * deltaTime)));
    165.     }
    166.     public Vector3 ApplyDrag(Vector3 velocity, float deltaTime)
    167.     {
    168.         return velocity * (1f / (1f + (drag * deltaTime)));
    169.     }
    170. }
    171.  
    172. class Timer
    173. {
    174.     float startTime;
    175.     float timeDelay;
    176.     float endTime;
    177.  
    178.     public void SetTimeReference(float delay)
    179.     {
    180.         timeDelay = delay;
    181.         startTime = Time.time;
    182.         endTime = startTime + timeDelay - Time.deltaTime;
    183.     }
    184.  
    185.     public bool IsTimeDone()
    186.     {
    187.         return endTime < Time.time;
    188.     }
    189.  
    190.     public float RemainderPercent()
    191.     {
    192.         float previousTime = Time.time - Time.deltaTime;
    193.         return (endTime - previousTime) / (Time.time - previousTime);
    194.     }
    195. }
    196.  
    197. //Acts kinda like FixedUpdate. We check if our deltaTime is too high (we are lagging) and cut it up to try and keep updates more consistent.
    198. class VariableFixedUpdater
    199. {
    200.     public float maxDeltaTime = .033f; // The clamp helps prevent our timestep going higher than the safe area of framerate independence
    201.     public int maxUpdateTimesteps = 8; //Higher = more movement accuracy, too high can cause problems. 8 seems good for framerates 30+ with targetPhysicsFramerate being 240
    202.     public int targetFramerate = 240; //If our framerate is low, we run more times for more accuracy. Higher = more movement accuracy, too high can cause problems..
    203.     public bool alwaysUseMaxUpdateTimestep; //not recommended as it is wasteful, but an option non the less.
    204.     public Action<float> variableUpdateMethod;
    205.  
    206.     public VariableFixedUpdater(Action<float> variableUpdateMethod)
    207.     {
    208.         this.variableUpdateMethod = variableUpdateMethod;
    209.     }
    210.  
    211.     public void Update()
    212.     {
    213.         int timesteps = maxUpdateTimesteps;
    214.         if(!alwaysUseMaxUpdateTimestep)
    215.         {
    216.             int safePhysicsIterator = Mathf.CeilToInt(Time.deltaTime / (1f / (float)targetFramerate)); //If our framerate is low, we run more times for more accuracy.
    217.             timesteps = Mathf.Clamp(safePhysicsIterator, 1, maxUpdateTimesteps);
    218.         }
    219.  
    220.         float deltaTime = Mathf.Clamp(Time.deltaTime / (float)timesteps, 0, maxDeltaTime);
    221.         if(deltaTime > 0f)
    222.         {
    223.             for(int i = 0; i < timesteps; i++)
    224.             {
    225.                 variableUpdateMethod(deltaTime);
    226.             }
    227.         }
    228.     }
    229. }

    Any help is appreciated =)
     
  2. Zaflis

    Zaflis

    Joined:
    May 26, 2014
    Posts:
    438
    clampedDashMovement doesn't seem to make sense. Why do you subtract velocity? I'm assuming you should just multiply dashMovement with Time.deltaTime there.

    Other than that, you should be using FixedUpdate. You did more work than you needed to by not using RigidBody.
     
  3. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    It has to do with how this system works. Dash is a movement over time, so I must use it with AddForce with forcemode of Force. This is required for the movement to work well with friction and drag in terms of framerate independence and consistency.
    For example, gravity is a constant force, not an impulse, but in fixedupdate using impulse or force doesnt seem to really male a difference in terms of consistency. However, if I were to use forcemode Impulse with gravity in this variable timestep environment, from what I remember, jumping was no longer consistent.
    I don't want dash to gradually increase. Instead I need to take my current velocity and see how much I would need to add to stay at that constant speed over time. I then multiply by 100 since this is a forcemode of force. It will then later get the subdivided deltaTime to help stay framerate independent and consistent, but something is wrong and its not consistent. I think even when I didnt clamp the movement, it was still not consistent.

    FixedUpdate might be simpler to work with, but I want fast reaction time and bumping the physics to 300+ times per frame with lots of physics objects is a waste. If I could just speed up physics for a single rigidbody then maybe.
    Either way, the work should already be pretty much done, its just that I am now stuck at this.
     
  4. Zaflis

    Zaflis

    Joined:
    May 26, 2014
    Posts:
    438
    So you could add the force just once and then do nothing per frame? It would stay constant if you disable drag.

    Reaction time doesn't have to do with using FixedUpdate to what it's meant for. You can still handle Input in Update. Theoretically Update runs 60 times per second, and FixedUpdate 50, but it's not that exact. Time.fixedDeltaTime is always what it's set to, by default 0.02 i think.
     
  5. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    I want drag to affect it. Besides, I dont think adding force once would fix the issue. Since I am in a variable timestep, when the dash timer ends it would have ended in a frame that part of that deltaTime is the remaining time of the timer for the dash that shouldnt be affected by friction, and the rest of the deltaTime is part of the movement that now should be affected by friction.

    Update could run as much as it can if you let it. Even if I poll my input in update, there will be a delay the user might feel from when they pressed to when the physics actually updated. I am making a very fast pace game, and players playing game such as this complain even about 200 fps feeling too low. Just look at the csgo players.

    Besides, there is no need to bring up fixedupdate since I hope this would be the last issue I have in regards to variable timestep movement consistency. I already did the work, no point in undoing it just for this one issue (yes, there will probably be more issues down the road, but whateva =))
     
  6. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    I tried to test to see if taking friction out of the equation would make things consistent again, and it doesnt.

    I replaced the "badDashVersion" in my Dash method with this
    Code (CSharp):
    1.  
    2.         //This will give inconsistent results
    3.         if(badDashVersion)
    4.         {
    5.             if(dashMovement != Vector3.zero)
    6.             {
    7.                 Vector3 dash = dashMovement * 10f;
    8.  
    9.                 if(!timer.IsTimeDone())
    10.                 {
    11.                     AddForce(dash, ForceMode.Force);
    12.                 }else{
    13.                     //We move and then zero our velocity. What this does is instantly stops our movement taking friction out of the equation.
    14.                     //However, even with friction not being a concern we are still getting inconsistent movement. Why?
    15.                     controller.Move((((dash * timer.RemainderPercent()) * Time.deltaTime) + velocity) * Time.deltaTime);
    16.                     velocity = Vector3.zero;
    17.                    
    18.                     handleFriction = true;
    19.                     dashMovement = Vector3.zero;
    20.                 }
    21.             }
    22.         }

    I AddForce even without clamping it so it gradually increases, and then when the timer ends I take the dash and multiply it by the remaining percent as I explained in the past, then I multiply it by deltaTime since this is a ForceMode.Force, and then add velocity and multiply by deltaTime once more. Velocity is then set to zero so no code involving friction or anything will do anything, taking them out of the equation.
    However, the movement is still inconsistent and I dont know why =(.
     
  7. Zaflis

    Zaflis

    Joined:
    May 26, 2014
    Posts:
    438
    Part of the inconsistency can come from misuse of Time.deltaTime. It is a different value everytime it comes to that code block. That's why i was so heavily recommending on using FixedUpdate, because it 100% guarantees to become consistent for physics like this.

    Is it dashing like you want it to either? If you move by amount of percent remaining, it moves 100% at first, and then slower and slower on next frames. On the first frames when percent is largest you also multiply with the deltatime so you get different movements every time.
     
  8. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    I understand that deltaTime is different every frame (variable timestep) and that fixedupdate can fix the consistency problems.

    I think you are misunderstanding what the code is doing.
    The dash timer starts and each frame it does plain old addforce, however, after .3 seconds the timer will end, but since this is a variable timestep, the timer could have really ended near the start of this frame or near the middle or near the end. So I calculate the remaining time that is really left of my timer and get the percentage of that compared to the total current Time.deltaTime. With that value I multiply my dashMovement by it and move one final time before completing my dash and stopping.
    I didnt clamp the addforce to make the code simpler to find the issue, so the movement will gradually increase which is not the dash movement I want, but it does demonstrate the issue properly.
    This all works perfectly well when basically dealing with a non accumulating static velocity. You can see this all work in the good dash version of my original post. However, there are problems with it in that it doesnt blend well with my other velocity methods, and using drag on it would make it non consistent.
    When trying to make things work with my other dash version, which accumulates the velocity, things are not consistent anymore.

    I am starting to feel like what I am trying to do might not be possible, at least not with euler integration. Even if I integrate dash with my VariableFixedUpdater, maybe things will be closer, but not enough.

    I think I will just work with the working dash version and live with the limitations.
    Thanks for trying to help though =)
     
  9. Zaflis

    Zaflis

    Joined:
    May 26, 2014
    Posts:
    438
    I don't think there's nothing that isn't possible to solve, if you carefully go through the math. One thing that caught my eye is this function. Have you debugged that it's reporting right values?
    Code (CSharp):
    1.     public float RemainderPercent()
    2.     {
    3.         float previousTime = Time.time - Time.deltaTime;
    4.         return (endTime - previousTime) / (Time.time - previousTime);
    5.     }
    There are maybe 200 frames per dash calculated (or i don't know how long it's dashing), and the after-dash remainder calculation should be less than 1% of the whole journey. So even if you don't move it after timer ends, you propably won't see any notable difference in position anyway. If there's inconsistency it comes from somewhere else, or that the finishing count is made hugely wrong.
     
  10. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    From what I can see, it is doing its job properlly.
    Also, I think it can be wrote like this

    Code (CSharp):
    1. public float RemainderPercent()
    2. {
    3.     float previousTime = Time.time - Time.deltaTime;
    4.     return (endTime - previousTime) / Time.deltaTime; //(Time.time - previousTime) is just Time.deltaTime. Didnt realize it when writing this =)
    5. }
    Stating that there are 200 frames per dash makes no sense. There could be 5 frames per dash for all I know. That is the point of all of this. I do not know how many frames there are since its a variable timestep. Yes, if it was indeed 200+ frames, then the error would be smaller, but the framerate might be 30 for all I know. However, making the calculations think we are at 200+ fps instead of 5 is what my VariableFixedUpdater is all about. I just dont have that integrated with my actual timer (tried, but had issues), but I assumed I would have been able to get it to work anyways so long as I handle this RemainingPercent part properly, which as long as the velocity isnt being accumulated things do seem to work, but when I am accumulating the velocity it all falls apart.

    This is what I meant by
    The math involved with euler integration might just not be enough for this =(.

    Edit - one way I tried was to put the timer and dash code within the sub updaters loop and use the subdivided deltaTime with my timer. The more we subdivide the deltaTime, the higher the accuracy.
    For example
    Code (CSharp):
    1.  
    2.     float currentTime;
    3.     void DoMovementInVariableFixedUpdater(float deltaTime)
    4.     {
    5.         if(dashMovement == Vector3.zero)
    6.         {
    7.             dashMovement = GetDirection() * dashSpeed;
    8.             if(dashMovement != Vector3.zero)
    9.             {
    10.                 currentTime = Time.time - Time.deltaTime + deltaTime;
    11.                 timer.SetTimeReference(.3f);
    12.                 handleFriction = false;
    13.             }
    14.         }else{
    15.             currentTime += deltaTime;
    16.         }
    17.        
    18.         if(dashMovement != Vector3.zero)
    19.         {
    20.             Vector3 clampedDashMovement = Vector3.ClampMagnitude(dashMovement - velocity, dashSpeed);
    21.            
    22.             if(!timer.IsTimeDone(currentTime))
    23.             {
    24.                 AddForce(clampedDashMovement * deltaTime, ForceMode.Impulse);
    25.             }else{
    26.                 handleFriction = true;
    27.                 dashMovement = Vector3.zero;
    28.                 logged = false;
    29.             }
    30.         }
    31.     }

    Notice how we are keeping track of our own currentTime and using that for the timer. The more we subdivide the main deltaTime, the less of an error our currentTime will have.
    However, I still find it way more accurate and better for performance to just hack all of this in the way I showed working before where I use the remaining time and just move my character outside of its physics system.
     
    Last edited: Sep 4, 2016