- Home /
Problems with player movement at higher frame rates (Time.deltaTime not producing consistent results)
Hey all,
Working on an FPS game where I'm using the Unity CharacterController to handle movement. I had to try for a while to get a decent feeling Inertia system to work, but I got there and recently decided to add a mini debug menu where I can set Vsync and FPS limits for testing. If I increased FPS past ~100 the player would stop moving altogether. I believe that as the time between the frames decreases the move value that I'm creating gets so small that the engine essentially reads it as a zero, but mathematically the behavior should still be the same at high and low FPS (in my head at least). Any help would be much appreciated; I really hate the way VSync feels in shooters and I kinda need to be able to disable it and allow players to set their own frame rate.
Removing the inertial calculations (If I can call them that, kinda brute forced them 'til they worked) would solve the problem but I'm trying to make the game play slower/tactical so that's not really an option.
For some context:
This is all happening in Update() and the "move" variable is a vector3 that contains only x and z components. It gets added to a vector3 "velocity" variable that contains only a y component in the move command.
Also I'm using 2019.4.18f1
if (grounded)
{
if (Mathf.Abs(controller.velocity.magnitude) < maxSpeed)
{
if (x != lastX || z != lastZ)
move = (currVel * decelSpeed) + ((transform.right * x + transform.forward * z) * accelSpeed * Time.deltaTime);
else
move = currVel + ((transform.right * x + transform.forward * z) * accelSpeed * Time.deltaTime);
}
else if (Mathf.Abs(controller.velocity.magnitude) >= maxSpeed)
{
if (x != lastX || z != lastZ)
{
move = (currVel * decelSpeed) + ((transform.right * x + transform.forward * z) * accelSpeed * Time.deltaTime);
}
else
{
move = ((transform.right * x + transform.forward * z)) * maxSpeed;
}
}
if (x == 0 && z == 0)
{
move = controller.velocity;
move *= decelSpeed;
}
if (x != lastX)
lastX = x;
if (z != lastZ)
lastZ = z;
airTime = 0f;
}
controller.Move((move * Time.deltaTime) + (velocity * Time.deltaTime));
currVel = controller.velocity;
To break this down, it checks player input (x / z) against a stored previous input. If they're different it modifies current velocity by a deceleration value and then adds the regular acceleration. If values are the same it does the acceleration calculation which is based off a stored velocity value. If the velocity exceeds a maximum it clamps to the maximum unless the new player input direction differs from the last in which case it slows down and starts over, essentially.
What I've tried:
1. Moving and altogether removing the Time.DeltaTime from the calculation.
2. Clamping what delta time value is acceptable to 0.01 (the lowest value that consistently worked), though this obviously lead to different results based on framerate. I was able to move at 144 fps, but it accelerated far faster and the inertia was practically non-existent. In addition if I exceeded 144 the same problem occurred, which rules out Time.deltaTime as part of the calculation being a problem, kind of...
3. Separating the Time.DeltaTime into it's own bit of the calculation. (IE move *= Time.DeltaTime;). This resulted in jerky movements when accel was kept at 15 and was too fast at higher values.
From what I've deduced from testing, my only conclusion is that Time.DeltaTime is too small (between 0.006 and 0.007) at higher framerates and called too frequently to make significant changes to velocity. In comparison it sits around 0.016 or so when Vsync is on which clearly massively effects the calculation.
For my given values of "acceleration = 15", "deceleration = 0.7", and "x" and "z" at most being equal to 1 (positive or negative depending on input axis) the calculation would go as follows:
For initial movement :
"currVel" 0 + (("no input x" 0 + "true forward z" 1) * "accelSpeed" 15) x time.deltatime --> this should equal 0.24 at 60 FPS. This is an additional 0.24 movement added per frame (on average). However at high FPS this goes down to 0.0975 at 144 FPS (average). Mathematically the result should be similar if not identical over a set amount of time but instead no movement occurs.
Can't get ShadowPlay to record the app, but I have a script outputting the Time.deltatime values used above so they're accurate. I don't have the "move" variable being set anywhere other than in this loop, so I'm entirely unsure what could be causing this behavior.
Please don't hesitate to let me know if there's additional information needed, I've been trying to fix this for a few days now and am unable to find a solution. Thank you! Sorry for the long post, wanted to get as much detail as possible.
Answer by Lord_Grumpledorf · Jul 13, 2021 at 06:50 PM
No idea if anyone's seen this yet, or if anyone will but I managed to solve the problem by changing the math around a bit and then moving my calculations in fixed update instead of regular update. Feels good and responsive enough and stays consistent regardless of framerate. Behavior is mildly different at higher framerates, but the average player won't notice since the movement logic (the actual controller.move()) is still in update and the values being fed are small enough to not cause massive changes.
Please see updated code below.
if (grounded)
{
if (Mathf.Abs(controller.velocity.magnitude) < maxSpeed)
{
if (x != lastX || z != lastZ)
{
move = (currVel * decelSpeed) + ((transform.right * x + transform.forward * z) * (accelSpeed * Time.deltaTime / 1 / Time.deltaTime));
}
else
{
move = currVel + ((transform.right * x + transform.forward * z) * (accelSpeed * Time.deltaTime / 1 / Time.deltaTime));
}
}
else if (Mathf.Abs(controller.velocity.magnitude) >= maxSpeed)
{
if (x != lastX || z != lastZ)
{
move = (currVel * decelSpeed) + ((transform.right * x + transform.forward * z) * (accelSpeed * Time.deltaTime / 1 / Time.deltaTime));
}
else
{
move = ((transform.right * x + transform.forward * z)) * maxSpeed;
}
}
if (x == 0 && z == 0)
{
move = controller.velocity;
move *= decelSpeed;
}
if (x != lastX)
lastX = x;
if (z != lastZ)
lastZ = z;
airTime = 0f;
}
Got the idea for this from a velocity/acceleration post that you can find here: https://gamedev.stackexchange.com/questions/117250/movement-appears-to-be-frame-rate-dependent-despite-use-of-time-deltatime
Took a bit of fiddling to get it working the way I wanted, but it works wonderfully now.
The change, while likely not entirely necessary, is not causing any problems at runtime and is consistent while simultaneously making the movespeed variable more human-readable so I'm happy with how it is for now. Just wanted to let everyone know that I've figured it out. If the code seems applicable to your project, don't hesitate to use it. It's based on the Brackey's character controller video, so it should be easily implementable in most projects with that as a player movement base.
Also, just to prove it works I managed to get ShadowPlay to work so I've put a link below. https://youtu.be/ASn4eXS1a4Q
Your answer
Follow this Question
Related Questions
Character controller (3d) don´t move whith a moving platform. 1 Answer
Character controller without Deltatime 1 Answer
if the fps is high the camera rotate much faster 1 Answer
getting out of a vehicle 2 Answers
Animations interupting Aim-Script 0 Answers