TimeScale affects Jump height apex

SpawnStorm

New member
1. Character controller variant (Ultimate Character Controller, First Person Controller, etc.).
Third Person Controller

2. Unity version (include which SRP, beta Unity versions aren't supported)
2020.3.18f1 also tested on 2021.1.3f1

3. Bug description
Jump, and probably all AddExternalForce calls, do not appropriately scale to m_TimeScale changes.

4. Steps to reproduce
Launch Demo Scene
Move to main room, and use the default Jump ability
Note the height Nolan's head reaches at the apex of the jump. I suggest using a breakpoint to ensure only one frame of force is added.
Change UltimateCharacterLocomotion > TimeScale to 0.2f
Repeat the Jump
Observe the height is incorrect.

5. The full error message (if any)
None.

A bit of background:
I'm working on a project that has several time manipulation mechanics. If I slow a player or NPC down, I do not expect their Jump (or possible other force based abilities) to suddenly change in effective power.
I have inspected and duplicated the Jump ability, modified the ApplyJumpForce > AddForce(vector, frames, scaleByMass, scaleByTime), set scaleByTime to false.
This sort of improves the situation, but now it jump apex goes down by too much. The greater the delta from Timescale=1 the greater the effect.

Changing the update location to FixedUpdate isn't feasible, because this seems to mess with the Platform logic. Nolan's feet sink into vertical platforms.
 
For consistent results you will need to use FixedUpdate. In the next major update of the controller I am completely switching to FixedUpdate and am not supporting Update anymore. For moving platforms make sure they are also updating during FixedUpdate.
 
Cool, just tested FixedUpdate. Same issue, Jump apex shifts based on TimeScale.

I did see that Platform logic changed (or maybe I changed it previously) to FixedUpdate. So I'm good with platforms.
 
I can take a look at this. I'm not sure if I'll be able to get it in the next update though. There's a lot that goes into the force calculation so with a different timescale there are a lot of variables that affect it.
 
Understood. I've come to the same conclusions, and instead of re-writing the forces calculation systems (without having your unit tests to confirm against) it seems fraught with pitfalls.

I discovered this issue while testing CharacterLocomotion's time scale features and calculations.
Something that would be very helpful to do is provide a protected layer into Time calls. A simple wrapper layer would be great. Replace instances of 'Time.deltaTime' with some wrapper equivalent like 'DeltaTime'.



/// <summary>
/// Applies the local or global deltaTime - Time.deltaTime wrapper
/// </summary>
protected virtual float DeltaTime() => Time.deltaTime;

/// <summary>
/// Applies the local or global TimeScale - Time.timeScale wrapper
/// </summary>
protected virtual float Time_TimeScale() => Time.timeScale;

/// <summary>
/// Applies the delta Time scale - TimeUtility.DeltaTimeScale wrapper
/// </summary>
protected virtual float DeltaTimeScaled() => TimeUtility.DeltaTimeScaled; //Time.deltaTime * Time.timeScale();

/// <summary>
/// Applies the framerate Time scale - TimeUtility.FramerateDeltaTime wrapper
/// </summary>
protected virtual float FramerateDeltaTime() =>
TimeUtility.FramerateDeltaTime; //DeltaTime() * 60f;



I've then inherited from UltimateCharacterLocomotion, and override these time functions. Unfortunately, several of the Time based calculations are deep within long private methods. So my choices so far are: replace the Time and TimeUtility calls, or make the private methods protected, and override in the child class (large copy/paste). Neither seem all that great when I'm hoping to keep the UCC on the latest version.

As above, without solid unit testing, or benchmarks from existing Abilities, I'm not 100% confident each of these need to be overridden. But after several hours of digging around in there, I'm pretty sure. I intended on brining this up after I could verify the changes didn't break anything obvious, and were necessary. But my tests will be blocked until some future update.

Regarding the unit testing, I saw some chatter about them for the other assets. Are there plans to make them available for UCC as well?
 
The time class is used throughout more than just the character locomotion so it would have to be a separate component similar to the Character Layer Manager. I'll need to think about the right way to implement this.

Regarding the unit tests, it uses a structure that is very specific to my tests so at this
 
I put some thought into this and actually think that it's behaving correctly. As a basic example let's consider falling from a height. No jumping forces are being applied and only the gravity accumulation affects the position. If the accumulation is scaled every frame you'll have values similar to below (assuming a value of 1 is used for the gravity amount):

1639473320904.png

Notice that even though the accumulation is halved the character will still land on the ground only two frames later than the normal timescale. So just because your timescale is cut in half doesn't mean that the character will take twice as long to land.
 
So I spent some more time testing this. I do hope you've given it a try. Setting the UCC timescale to 0.1, then jumping so high that you kill yourself from fall damage seems a bit /unintended/ from a gameplay point of view.

So I captured real demo scene numbers for fall height = 50; those two frames in your example become significant.

I haven't pinned down where the logical error is in time, there is a lot of forces going on that I plan to isolate out next. My immediate suspicions are around the damping forces, but my quick tests yesterday didn't clean that up. I'm also a bit suspicious of CharacterLocomotion > AddExternalForce > if(timescale < 1) force/=timeScale.
It first pass, this looks like it is a way to clean up a bug when time was > 1; i.e. at some point in development TimeScale > 1 didn't play nice with scaleByTime. That was fixed (I suspect, I haven't tested > 1 cases yet), then the <1 check was added. It doesn't make sense to me that force should only scale in slow-mo time.

Either way, I've uploaded a google sheet with my test results. You can see that gravity appears to be scaling exponentially with Time. This makes some sense as g = m/s/s; I haven't worked out the algebra on the trendlines to confirm it matches mathematically to the TimeScale shifting, but from a gameplay point of view, I think it is close enough.

So I moved on to how ExternalForces work, because those must be tied to a different system than the gravity.

If you look at the Graphical Jump graphs, you'll see that the player jumps approximately 15x higher when time is significantly slowed down. The initial forces are about 10x. This fits with the force/=timeScale.

Looking at the Graphical Jump No Time scaling sheet, the 1 and 0.5 cases are pretty close in Jump Height. This is accomplished by replacing the scaleByTime in each of Ability:Jump's calls to AddForce to False.
However, this breaks down when timescales are significantly small. At 0.1, the player doesn't even leave the ground.



My current understanding of what the ideal case is this:
When the player jumps (regardless of UCC's scaled time) the apex of the jump should remain consistent. I expect some variability in the timeScale > 1 cases, as there are less calls FixedUpdate in the same real-world time (or a single jump).
 
From my table above, how would you expect the simple gravity case to work? Jumping involves external forces and gravity so nailing just gravity down will help determine the correct functionality.
 
So, I've had a chance to think and run calculations on this.

Imagine a person walking, one has a timescale of 1, the other a timescale of 0.5
We intuitively believe the person with a timescale of 0.5 should take 2x longer to reach the same destination.
So, I calculated the velocity and acceleration values for timescale 1 and timescale 0.5

I've updated the Google Sheet to view this, and graphs with trendlines to help with the formulas. Screenshot below for convenience

Note, acceleration remains constant (as we would expect from a constant gravitational force). This is because acceleration is a function of change in velocity over change in time. Since the timescale affects both proportionally, the acceleration remains the same.

So, "how would you expect the simple gravity case to work?"
I would expect an object falling with a timescale of 0.5 to take a bit less than 50% longer to reach it's destination. Or more precisely:
Timescale = 1:
0.5x^2 + 0.5x
Timescale = 0.5:
0.25x^2 + 0.75x + 0.5

Take the inverse (to complicated to write here):
A 1000 unit drop takes 44.2241 timeUnits at TimeScale =1
A 1000 unit drop takes 61.7475 timeUnits at TimeScale = 0.5
So a difference of 39.6239%

TimescaleEffectOnVelocity.jpg
 
To add to this, I based TimeScale = 0.5 on TimeScale = 1, and only modified the 'y' values for each TimeUnit by the TimeScale. This is to make the analogy of walking take 2x longer.
 
That works well for regular acceleration with no dampening, but what about gravity? Gravity is accumulating so it's not just a constant value that is being added every frame. You mentioned that it should take almost twice as long, but if only the gravity force for that frame is being added then it's not taking into account the accumulation.

If you take the graph from this post, what would you expect the forces to look like?
 
Last edited:
Yes, I've read your post with the table. Several times. Your assumption that 'Gravity is accumulating' is false.
Gravity affects the velocity. Velocity accumulates Gravity's acceleration per unit of time.
Take a look at Example 2.7.3, Position should be in a parabolic curve, Velocity should be in a linear curve, and Acceleration (gravity) should be flat (constant).

In my google sheet, I've shown how these arbitrary gravity (acceleration) = 1 produces identical curves.

"You mentioned that it should take almost twice as long," That is referring to 2 bodies operating at different timescales going a set distance at a constant velocity (from their relative frames of reference). I had to think about this in terms of general relativity, then apply that logic to the constant acceleration of gravity.
 
The implementation for forces/gravity was based off of UFPS version 1 because it worked so well for a game environment. These are some pretty major changes to the core locomotion component to make it based more on real life physics rather than the game physics that has been in place since the beginning. I'm not saying that it's a bad change, just that I'll need to think about it some more and test it out. This is a major change from the current locomotion though so am hesitant to change it because it would affect so many projects. This could be a change for the new major update that I am working on.
 
See, that's where I think we've gotten sidetracked.

Gravity isn't a big problem. It is parabolic like it is supposed to be.

Here is a graph from the data I collected of Timescale = 1 falling from height = 50. Notice Blue(position) is parabolic. Red is roughly linear, and Yellow (gravity) is flat. I totally get that the game world should use estimates, and more arbitrary values for calculations.

Gravity doesn't even call AddForce.
1639585669259.png
 
When you jump vertically there are two forces in play:

- External Force
- Gravity Accumulation

I was focused on gravity because it's a factor in jumping when the character leaves the ground. If a character is at height and falls straight down and the 0.5 timescale character lands roughly at the same time as the 1 timescale character then jumping doesn't have a chance at working correctly because it's more complicated than a pure fall.

If the game world applies a constant force and doesn't accumulate then the 0.5 character would land at roughly 2x the 1 timescale character. So then it's a problem with the external force. External force has a damping modifier which is likely going to also cause some problems. In the next major update I have gotten rid of the external force timescale division when the timescale is > 1 (this post) but it still has the same damping division every frame. I will continue to run some tests and let you know after I have some more info.
 
Found a single-directional vector answer, and it doesn't look like it will have any impact on other functionality at TimeScale = 1.

As noted above, Gravity forces are correctly arcing as timescale goes approaches zero. So I looked into the upward forces, to see why the apex shrank as timescale approaches zero. Then I needed to look expanding the time-in-jump (frame count or TimeUnit quantity) by the factors listed here

In CharacterLocomotion > UpdatePosition
While calculating the upward velocity you need to divide by the squareroot of the timescale.

Something like this works in my demo:
m_MoveDirection.y += (float)((m_ExternalForce.y * deltaTime / Math.Sqrt(m_TimeScale)) + (m_GravityAmount * deltaTime)) ;

I haven't worked out the entire Vector3 version of this to account for custom gravity directions and it messes with any lateral movement. Also, the spreadsheets I made to derive this solution are too complex and note-less to link and discuss on this post. If you'd prefer to see them, send a DM.

All of the damping forces 'should' continue to function as expected. Though it is late for me, and I'd rather not calculate air resistance in my sheets then verify in-engine.
 
Top