How to create a slow motion effect for single entity?

Godot Version

4.4.1.stable

Question

Hi, everyone! I’m trying to add a slow-motion area effect to my game.

Engine.time_scale works very well but affects all entities.

Partial source
func handle_move(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta

	# Handle jump.
	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	move_and_slide()

func _on_area_2d_body_entered(body: Node2D) -> void:
	Engine.time_scale = 0.2

func _on_area_2d_body_exited(body: Node2D) -> void:
	Engine.time_scale = 1.0

I tried then scaling the velocity using a local variable called time_scale.

It affect only the desired entity but the velocity is not correctly scaled.

Partial source
var time_scale: float = 1.0

func handle_move(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta * time_scale

	# Handle jump.
	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY * time_scale

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED * time_scale
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED * time_scale )

	move_and_slide()

func _on_area_2d_body_entered(body: Node2D) -> void:
	time_scale = 0.2

func _on_area_2d_body_exited(body: Node2D) -> void:
	time_scale = 1.0

Please, what am I doing wrong?

In the first case time_scale is automatically effecting every time-dependent aspect, but in the second one your script is only adjusting the horizontal motion. The second video shows the horizontal motion slowing down but the vertical one continuing at normal speed.
AFAIK there is no way available in Godot to slow down time for parts of the scene, other than by scripting it all directly. Try getting AI to generate you some code, but otherwise I’d say you are on your own in this.

Yes, I’m having some difficulty in scaling vertical velocity… I believe that’s because it’s an accelaration, as metioned here.

I wonder how can I scale both acceleration and velocity of a body…

Another video showing how different the vertical velocity gets when inside and outside the slow-motion area:

You can make it work by keeping track of your unscaled velocity in a separate variable and calculate movement with it, and only scale it in the end. (You still need to multiply gravity with time_scale.) Something like:

var unscaled_velocity := Vector2.ZERO

func handle_move(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		unscaled_velocity += get_gravity() * delta * time_scale

	# Handle jump.
	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		unscaled_velocity.y = JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		unscaled_velocity.x = direction * SPEED
	else:
		unscaled_velocity.x = move_toward(unscaled_velocity.x, 0, SPEED)

	velocity = unscaled_velocity * time_scale

	move_and_slide()

edit: Also, using SPEED in move_toward will make your character stop in a single frame, so the same as just doing velocity.x = 0 instead. To make your character gradually stop use a smaller value, also you should probably use delta. Like try:
unscaled_velocity.x = move_toward(unscaled_velocity.x, 0, 1800 * delta)

2 Likes