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
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…
Click for code
using System;
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class TestMovementConsistency : MonoBehaviour
{
public float friction = 20;
public float drag;
public bool testInAirOnly; //just in case charactercontroller collision is affecting our movement consistency, we will test in air.
public float drawRayTime = 10f;
public bool badDashVersion;
float dashSpeed = 12f;
Timer timer = new Timer();
VariableFixedUpdater variableFixedUpdater;
Vector3 velocity;
Vector3 impulses;
Vector3 leftoverImpulse;
Vector3 forces;
Vector3 forcesNoFriction;
bool handleFriction;
Vector3 dashMovement;
CharacterController controller;
void Awake()
{
variableFixedUpdater = new VariableFixedUpdater(UpdateMovement);
controller = GetComponent<CharacterController>();
}
void Update()
{
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.");
Vector3 prevPosition = transform.position;
AddForce(leftoverImpulse, ForceMode.Impulse);
leftoverImpulse = Vector3.zero;
Dash();
Jump();
Gravity();
velocity += impulses;
impulses = Vector3.zero;
//This will look at our deltaTime and cut it up so that it stays fairly consistent and call our UpdateMovement with each delta cut.
variableFixedUpdater.Update();
forces = Vector3.zero;
forcesNoFriction = Vector3.zero;
//This shows our movement distance consistency
Debug.DrawRay(transform.position, Vector3.forward, Color.red, drawRayTime);
//This shows our movement speed consistency
Debug.DrawRay(transform.position, Vector3.up * (1f + (Vector3.Distance(prevPosition, transform.position) / Time.deltaTime)), Color.red, drawRayTime);
}
void UpdateMovement(float deltaTime)
{
Vector3 prevPosition = transform.position;
Vector3 acceleration = velocity + (forces * deltaTime);
if(handleFriction) acceleration = ApplyFriction(acceleration, deltaTime);
acceleration += (forcesNoFriction * deltaTime);
acceleration = ApplyDrag(acceleration, deltaTime);
controller.Move(acceleration * deltaTime);
velocity = (transform.position - prevPosition) / deltaTime;
}
void Dash()
{
if(dashMovement == Vector3.zero)
{
dashMovement = GetDirection() * dashSpeed;
if(dashMovement != Vector3.zero)
{
timer.SetTimeReference(.3f);
handleFriction = false;
}
}
//This will give inconsistent results with both the movement distance and movement speed.
if(badDashVersion)
{
if(dashMovement != Vector3.zero)
{
Vector3 clampedDashMovement = Vector3.ClampMagnitude(dashMovement - velocity, dashSpeed);
clampedDashMovement *= 100f;
if(!timer.IsTimeDone())
{
AddForce(clampedDashMovement, ForceMode.Force);
}else{
//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.
//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.
//We add it to the forcesNoFriction since dash movement is not affected by friction.
forcesNoFriction = clampedDashMovement * timer.RemainderPercent();
handleFriction = true;
dashMovement = Vector3.zero;
}
}
}
//This will give consistent results, however, drag will not work with this.
//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.
//We would also ideally not want to call move here since we are also calling Move in our UpdateMovement, which can lower performance.
if(!badDashVersion)
{
if(dashMovement != Vector3.zero)
{
if(!timer.IsTimeDone())
{
//We move at a constant rate
controller.Move(dashMovement * Time.deltaTime);
}else{
//Like explained above, we use the remaining time to decide how much is left of our movement to finish.
controller.Move((dashMovement * timer.RemainderPercent()) * Time.deltaTime);
//We now need to add 1 more full dashMovement, but this time have it be affected by friction
//Se we addforce the remaining of what we just cut off from above to be affected by friction
AddForce(dashMovement - (dashMovement * timer.RemainderPercent()), ForceMode.Impulse);
//And then the leftover of the above will be ran next frame to complete the full dashMovement affected by friction
leftoverImpulse = dashMovement * timer.RemainderPercent();
handleFriction = true;
dashMovement = Vector3.zero;
}
}
}
}
void Gravity()
{
if(!testInAirOnly) AddForce(-Vector3.up * 40f, ForceMode.Force);
}
void Jump()
{
if(Input.GetKeyDown(KeyCode.Space)) AddForce(Vector3.up * 10f, ForceMode.Impulse);
}
void AddForce(Vector3 velocity, ForceMode forceMode)
{
if(forceMode == ForceMode.Force) forces += velocity;
if(forceMode == ForceMode.Impulse) impulses += velocity;
}
Vector3 GetDirection()
{
if(Input.GetKeyDown(KeyCode.A)) return Vector3.left;
if(Input.GetKeyDown(KeyCode.D)) return Vector3.right;
return Vector3.zero;
}
public Vector3 ApplyFriction(Vector3 velocity, float deltaTime)
{
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
return velocity * (1f / (1f + (friction * deltaTime)));
}
public Vector3 ApplyDrag(Vector3 velocity, float deltaTime)
{
return velocity * (1f / (1f + (drag * deltaTime)));
}
}
class Timer
{
float startTime;
float timeDelay;
float endTime;
public void SetTimeReference(float delay)
{
timeDelay = delay;
startTime = Time.time;
endTime = startTime + timeDelay - Time.deltaTime;
}
public bool IsTimeDone()
{
return endTime < Time.time;
}
public float RemainderPercent()
{
float previousTime = Time.time - Time.deltaTime;
return (endTime - previousTime) / (Time.time - previousTime);
}
}
//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.
class VariableFixedUpdater
{
public float maxDeltaTime = .033f; // The clamp helps prevent our timestep going higher than the safe area of framerate independence
public int maxUpdateTimesteps = 8; //Higher = more movement accuracy, too high can cause problems. 8 seems good for framerates 30+ with targetPhysicsFramerate being 240
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..
public bool alwaysUseMaxUpdateTimestep; //not recommended as it is wasteful, but an option non the less.
public Action<float> variableUpdateMethod;
public VariableFixedUpdater(Action<float> variableUpdateMethod)
{
this.variableUpdateMethod = variableUpdateMethod;
}
public void Update()
{
int timesteps = maxUpdateTimesteps;
if(!alwaysUseMaxUpdateTimestep)
{
int safePhysicsIterator = Mathf.CeilToInt(Time.deltaTime / (1f / (float)targetFramerate)); //If our framerate is low, we run more times for more accuracy.
timesteps = Mathf.Clamp(safePhysicsIterator, 1, maxUpdateTimesteps);
}
float deltaTime = Mathf.Clamp(Time.deltaTime / (float)timesteps, 0, maxDeltaTime);
if(deltaTime > 0f)
{
for(int i = 0; i < timesteps; i++)
{
variableUpdateMethod(deltaTime);
}
}
}
}
Any help is appreciated =)