While this is not by any means a new mechanic (variable jump) I did put a spin on it by scaling gravity, and allowing two different gravity scales for maximum design control
Variable Jump
Separate Gravity for Ascent and Descent
Separating ascent from descent gravity allows designers to really control the feel on the way UP as well as the way DOWN.
Mathematically this is achieved by drawing two parabolas and
taking the first half of the first graph until the apex
taking the second half of the second graph after the apex
Multi Jump
Using this strategy also allows for multi-jumps to have different scales either on the way up/down/both.
Double jumps are slightly less powerful (75%)
Design controls Jump Height and Time
Rather than specifying a specific velocity for the jump, which is hard for designers to visualize, instead they specify a Jump Height and Time to reach which is more visualization friendly.
if I told you the following, which is easier to visualize how high the jump will be, and how long will it take?
Jump at a velocity of 125m/s^2
hard to visualize
Jump 100 units in 0.5s
very clear to immediately visualize
This also helps Level Design know how to make levels by not needing to calculate "well if they can jump at his speed, how high can they reach".
Jump Ascent Control
Design can specify a MIN, MAX, and TIME TO MAX for the jump such that
It will take Time to Jump Max Height to reach Jump Max Height
The player will always perform a minimum jump of Jump Min Height
quick ascent
floaty ascent
Jump Descent Control
Similarly design can specify a Post Jump time (which uses the Jump Max Height) to apply separate gravity on the way down
quick descent
floaty descent
Calculating Velocity & Gravity given Height & Time
We have Height and Time so we need to calculate Gravity and Initial Velocity as functions of time and height.
Projectile Motion
Let's start with the projectile motion function which gives the height of an object
It's more correct to use th (t sub h) when indicating time at height, but for simplicity of typing I'll just use t in the equations
Height at apex equation
p0 can be ignored here because the start of the jump is our actors position (can be added later).
Apex of the height is at time th :
Velocity as a function of gravity and time
We want to find velocity at a given time which is the derivative of the projectile motion function
The Apex of the jump th (time at max height) is when Velocity = 0
so we can derive Velocity as a function of gravity and time
Gravity Equation as a function of height and time
Substitute Velocity into the equation for height and solve for gravity
Velocity as a function of height and time
Since the two knowns we have are height and time substitute gravity back in to find Velocity as a function of height and time
Can find Velocity & Gravity at any Height & Time
Now we have our two equations as functions of time
Velocity
Gravity
Which means for any given Height and Time we can find the corresponding Velocity and Gravity needed
Gravity Scale vs Gravity
Preface
You might be thinking "but wait Unreal already defines gravity as 9.8m/s^2 (or 980cm/s^2 for Unreal)" and is going to apply that gravity every tick?
Yes, I'm relying on the CharacterMovementComponent to apply a constant gravity, but what we can control is actually Gravity Scale instead of gravity itself.
Changing CharacterMovementComponent::GravityScale is how we're able to leverage UE's physics while still having control over the player's gravity
Slight modification to Gravity equation
Since we want gravity as a Scale and not a flat value, in the previous gravity equation
We can get the scale by dividing by the engine defined gravity constant UPhysicsSettings::Get()->DefaultGravityZ which we can call DefaultGravityZ
so our equation really becomes
This will provide a scalar that represents whether gravity is higher or lower than the default, which we can set in code.
Code Example
Velocity & Gravity functions
Cache gravity scales up front
At the start of the game we have our given designer values for Jump Max Height and Time...etc so we can pre-calculate a number of variables for
Default Gravity
Starting Gravity Scale
Max Pre Jump (ascent) Gravity Scale
Min Pre Jump (ascent) Gravity Scale
Post Jump (descent) Gravity Scale
Handle Variable Jump (ascent)
Variable jump height is achieved by calculating a gravity scale between the Min and Max gravity scales based on how long the input was held
Jump
Instantaneous Velocity is applied to make it to the height in the given time, and the appropriate gravity scale is also applied
Switch Gravity at Jump Apex (descent)
Technically we immediately enter the MovementMode(MOVE_Falling) state so that UE's starts applying gravity immediately.
We override PhysFalling which will get executed upon switching movement mode for a number of reasons other than jump, but for jumping its to check for when we switch from Positive to Negative velocity which means we're at the Apex of the jump.
Here is where we switch from our (ascent) Jump Gravity scale to the (descent) fall gravity
void UDeftMovementComponent::BeginPlay()
{
Super::BeginPlay();
UPhysicsSettings* physicsSettings = UPhysicsSettings::Get();
if (physicsSettings)
{
m_DefaultGravityZCache = UPhysicsSettings::Get()->DefaultGravityZ;
}
GravityScale = 1.f;
m_DefaultGravityScaleCache = 1.f;
m_MaxPreJumpGravityScale = CalculateJumpGravityScale(TimeToJumpMaxHeight, JumpMaxHeight);
// We need to scale time by the same factor as height since
// if it takes 1s to reach 4m, then it would take 0.5s to reach 2m
// so if the max height is 4m in 1s, and the min height is 2m we shouldn't use gravity that takes us to 2m over 1s, it should take us 0.5s instead
float timeScale = JumpMaxHeight / JumpMinHeight;
m_MinPreJumpGravityScale = CalculateJumpGravityScale(TimeToJumpMaxHeight / timeScale, JumpMinHeight);
m_PostJumpGravityScale = CalculateJumpGravityScale(PostTimeToJumpMaxHeight, JumpMaxHeight);
...
}
void UDeftMovementComponent::OnJumpPressed()
{
m_JumpKeyHoldTime = 0.f;
m_bIncrementJumpInputHoldTime = true;
m_bIsJumpButtonDown = true;
}
void UDeftMovementComponent::OnJumpReleased()
{
// apply a new gravity scale based on time held
if (m_bIncrementJumpInputHoldTime)
{
// Only switch to gravity if we need to
m_bIncrementJumpInputHoldTime = false;
// ex: max time is 2s, we hold for 1s, we get 1/2s = 0.5 == 50% of the max jump
// ex: max time is 2s, min time is 1s, we hold for 0.2s, we _should_ get 0.2/2s = 0.1 == 10% of the jump
float val = FMath::Clamp(m_JumpKeyHoldTime / JumpKeyMaxHoldTime, 0.f, 1.f);
// val == 1 that means we held it max time and shouldn't change gravity at all.
// val == 0 means we want the min height (more gravity applied)
float gravityScaledByInput = (val * (m_MaxPreJumpGravityScale - m_MinPreJumpGravityScale)) + m_MinPreJumpGravityScale;
GravityScale = gravityScaledByInput;
}
m_bIsJumpButtonDown = false;
}