Math help to calculate easing distance

Godot Version

v4.4.dev6.official [1f47e4c4e]

Question

I want to move a node programmatically, but i can’t figure out the math to get the exact motion that I want. The node will be moving a fairly long distance, I want it to start still, ease in to the motion, then move at a constant speed for the bulk of it’s trip, ease out, and stop at exactly it’s destination.

It’s easy enough to tween it from place to place, but the easing continues evenly along the whole trip. I can tween a speed property from 0 to target speed, then multiply by delta… but how do I calculate the point at which I should tween the speed back to 0 in order to land on my mark?

I feel confident there is math for that… and that it’ll be different based on which transition type I’m using with my tweens… any math whizzes out there know the formula I should use?

or just create an AnimationPlayer node on the fly, which has that exact easing type.

I somewhat understand your problem.

I understand that you want to create a system that moves a node on a path from A to B. The speed of the node should be configurable and should be accelerated from/to 0 near the start and end of the path.

Is it right to assume that time is not important here; it doesn’t matter how long it takes for the node to reach its destination?

Let me know whether if what I just described is correct.

Movement with eased acceleration

To produce a system that is capable of starting and stopping a node between two points, you have to know some information:

  • Distance to B
  • Stopping distance of easing function (relative to the max speed)
  • Acceleration distance of easing function (relative to the max speed)

To elaborate, the distance to B must be known so the node knows when to start “braking”. The crucial part of this system is, as you pointed out, knowing when to stop the node.

By using a (easing) function to define the speed of an object, we can use calculus to compute the distance that the object of that speed travels.

Using calculus to compute travel distance

Before describing how we can use calculus to compute the travel distance, it is worth explaining why we can do so in the first place.

It is trivially known in physics that the distance an object travels over a period of time is:

distance_travelled = speed[m/s] * time[s]

The presupposition here is that speed and time are constant. In your case, they are not – delta and speed varies over time. So how do we compute the distance travelled? The answer is to slice up time into discrete chunks and sum up these chunks to approximate the travel distance. For physics simulations (such as those seen in games), these chunks are often large so the game can run in real-time, but in mathematics we can slice up time into infinitely many chunks and arrive at the exact solution.

While being a rather crude description, an integral in calculus is essentially that – slicing up a function into infinitely many pieces in order to compute the area defined by a function and an upper and lower bound.

The area under the function, sin(x), within the bounds of [0, 1] is seen in blue.

Let us now use a function that is more applicable to your use case.

Easing function: sin(x * PI / 2) * speed
speed = 10.0

As seen in the image above, the travel distance for sin(x) with a speed of 10.0 is 6.3662.

Translating the math into usable code

We have now learned how to compute the travel distance of an easing function, and we can now use this distance as a threshold so we know when to start tweening.

However, I imagine you would want to configure the system on the fly without heading to some third-party math website (Wolfram Alpha) every time you want to use a different function or different values. Unfortunately, GDScript (and most other languages as far as I know) do not offer a simple integral() function. You will have to make your own. Without going into too much detail, I have added my attempt at an integral() function below that uses the principle of “time slicing” previously outlined.

# Takes the integral of a Curve instance with the specified step size and bounds.
func integral(custom_function: Curve, lower_bound, upper_bound, step_size) -> float:
	var value_range = upper_bound - lower_bound
	var steps_in_range = value_range / step_size # Slices in the value range

	var result = 0
	for i in range(steps_in_range):
		var x = lower_bound + step_size * i  # The input value
		var y = custom_function.evaluate(x)  # The output value
		var area = y * step_size             # The slice area
		result += area

	return result

Context-dependent invocation example:

var easing_function: Curve
var distance_travelled = integral(easing_function, 0, 1, 100)

Performing the movement

I think you got this part down already but just to be complete, I’ll provide some code that uses the distance_travelled to start and stop a node on a path between two points as originally described.

This code example is not done yet and contains missing lines of code and errors. I was too tired to go on. It will be corrected ASAP.

@export var node_A: Node2D
@export var node_B: Node2D
@export var easing_function: Tween.TransitionType
@export var max_speed = 1
var speed = 0

var node: Node2D # The node that moves

var start_moving = false
enum MoveState { STARTING, MOVING, STOPPING, STOPPED }
var current_state = MoveState.STOPPED

var current_tween: Tween

func _process(delta):
	var toB = node_B.global_position - node.global_position
	var distance = toB.length()
	var direction = toB.normalized()

	# TODO: Find a good way to match transtion types with functions for use with integral().
	var stopping_distance = integral(###, ###, ###, ###)

	if distance <= stopping_distance && current_state == MoveState.MOVING:
		stop()

func start():
	if current_tween:
		current_tween.kill()

	current_tween = create_tween()
	current_tween.tween_property(self, speed, max_speed, 1).set_trans(easing_function)

	current_state = MoveState.STARTING

	current_tween.finished.connect(func(): current_state = MoveState.MOVING)

func stop():
	if current_tween:
		current_tween.kill()

	current_tween = create_tween()
	current_tween.tween_property(self, speed, 0, 1).set_trans(easing_function)

	current_state = MoveState.STOPPING

	current_tween.finished.connect(func(): current_state = MoveState.STOPPED)

This code example is not done yet and contains missing lines of code and errors. I was too tired to go on. It will be corrected ASAP.


I hope this helps you achieve what I think you’re going for. It’s a lot easier to move an object with easing functions if the speed can be arbitrary, but since your goal is constant speed “mid-flight” I think this might be a good approach.

Let me know if anything needs correction – I hardly use GDScript so I’ve been reading the docs quite a bit.

1 Like

This explanation is incredible! Yes, your analysis of my problem is accurate. I’m in awe of the effort you took to answer my question. Thank you so so much!

Thinking of the distance traveled as area really helped. I’m not gonna lie, the idea of using calculus here is a bit intimidating, but AREA, that makes sense!

It made me realize: if I switch over to using a tween that both eases in and out, then the line representing the change will be rotationally symmetrical over an axis in the middle. That means the area on each side will be exactly half of speed*time (or close enough)!

This was so helpful, thank you so much for taking the time. I’m going to read your math explanation a couple more time to be sure I understand it. Knowing the kind of stuff I like to make, I’m likely to need it down the road.

Update: works great! Here’s some code for anybody else looking to parallax without a camera (which is what I’m doing, cuz reasons)

var base_speed = 1000
var acceleration_time = 3
var braking_distance  = 0
var current_speed = 0
var distance = 10000

func _ready():
	braking_distance = base_speed*acceleration_time*.5
	change_speed(base_speed)

func change_speed(target_speed):
	var tween = create_tween()
	tween.set_trans(Tween.TRANS_CUBIC)
	tween.tween_property(self, "current_speed", target_speed, acceleration_time)
	await tween.finished
	tween.kill()

func _process(delta):
	if current_speed > 0: 
		for child in $Road.get_children():
			child.position.x -= current_speed*delta
		if distance > braking_distance && distance - (current_speed*delta) <= braking_distance: change_speed(0)
		distance -= current_speed*delta



1 Like

You’re right, I didn’t think about that. Approximating the easing function to be linear works for many cases – especially if the acceleration time (easing duration) is fast. However, do bare in mind that the error from the real value becomes larger and larger for slower acceleration.


I’m glad you made it work!

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.