Safely teleport two things without colliding

Godot Version

4.7.1

Question

I have two “pets” which follow the player in a straight line (I haven’t tried adding navigation yet). I’ve added code to teleport the pets next to the player if they end up more than a certain distance away. I used a ShapeCast2D to test spots near the player for collisions before performing the teleport. This sort of works, but has some problems. The pets sometimes teleport to the same spot, and then one of them gets shoved into a wall or the player.


The two overlapping white ovals are the successful shape casts that the pets decided to teleport to. The egg stayed in that spot; the frog got shoved north into a wall.

I was hoping that after the first pet teleports to a location, the second pet’s ShapeCast test would detect a collision with the first pet and the second pet would be forced to choose another teleport location. That doesn’t seem to be happening. The second pet teleports “successfully” right on top of the first pet. I assume the problem is happening when they both teleport in the same frame. Is there a way to detect collisions with objects that have just teleported in the current frame?

Alternative solutions are also welcome. I’d be glad to know a more graceful way to accomplish this.

Code

The component the pets use to follow the player:

class_name FollowComponent extends Node
## Follows a specified entity at a given distance.
## Navigation not included. This just follows the target in a straight line.

## The owner of this component
@export var body: Character # <-- extends CharacterBody2D
## The entity to follow around
@export var initial_target: Node2D
## If the creature gets farther away than this distance, teleport near the target.
@export var teleport_distance_threshold: float = 300
## Used to find a safe teleport destination if needed
var shape_cast: ShapeCast2D

## Called every physics frame by the character this component is part of.
func update() -> void:
	assert(body != null, "FollowComponent requires a body")
	if target == null:
		return
	
	var target_vector: Vector2 = target.global_position - body.global_position
	
	# First check if we need to teleport
	if target_vector.length() > teleport_distance_threshold:
		body.global_position = find_teleport_destination()
		body.reset_physics_interpolation()
		target_vector = target.global_position - body.global_position # Recalculate
	
	# Then update movement direction
	if _still_too_far_away(): # implementation not relevant for this post
		body.movement_direction = target_vector.normalized()
	elif _close_enough(): # implementation not relevant for this post
		body.movement_direction = Vector2.ZERO


## Try to find a teleport destination near the target that won't collide with
## anything. This might be expensive. If a good destination isn't available,
## give up and return the current position.
func find_teleport_destination() -> Vector2:
	# Create a ShapeCast2D at the estimated teleport location. Give it the
	# same collision shape that `body` has.
	# Move the ShapeCast2D until it doesn't collide with anything.
	if not shape_cast:
		shape_cast = ShapeCast2D.new()
		add_child(shape_cast)
		shape_cast.shape = body.collision_shape_2d.shape
		shape_cast.rotation = body.collision_shape_2d.rotation
		shape_cast.collision_mask = body.collision_mask
		shape_cast.target_position = Vector2.ZERO # Don't sweep, just test the single shape
	shape_cast.enabled = true
	# Account for the collision shape's offset within the CharacterBody2D.
	var collision_shape_offset: Vector2 = body.collision_shape_2d.position
	
	var start_dir: Vector2 = Vector2.DOWN
	var success: bool = false
	
	# Try up to NUM_DIRECTIONS directions. This might be expensive :(
	const NUM_DIRECTIONS: int = 4
	for i_attempt in range(NUM_DIRECTIONS):
		var start_position: Vector2 = target.global_position + collision_shape_offset
		for distance: int in range(5, 50, 5):
			shape_cast.global_position = start_position + start_dir * distance
			shape_cast.force_shapecast_update()
			if not shape_cast.is_colliding():
				success = true
				break
		start_dir = start_dir.rotated(2 * PI / NUM_DIRECTIONS)
	
	shape_cast.enabled = false
	if success:
		return shape_cast.global_position - collision_shape_offset
	else:
		return body.global_position # Don't teleport

How the pets use the FollowComponent:

### Excerpt from EggSprite's script

extends Character
var speed: int = 100

func _physics_process(_delta: float) -> void:
	follow_component.update()
	_process_animation()
	move_and_slide()

	# This next line is actually executed by the character's
	# WalkingState._physics_process.
	# I've moved it here to simplify the code for this post.
	velocity = movement_direction * speed
Scene tree


Ignore the egg’s CommandMove node, that’s for dialogue stuff.

Add a timer and a bool like var processing = false. Timer on like 0.1 sec. When teleporting set bool to true. When timeout set to false and call back to the failed teleporter to call teleport again. Dont run the teleport if true.

Thanks, but I think that’s a response to a different question. I left an unrelated TODO comment in my code which was probably misleading. I’ll remove that. Right now, the perf impact of rerunning a failing check every frame is not a problem.

My problem is not that the teleport fails, but that the shape cast collision check succeeds (reports no collision) when I want it to fail (detect a collision).

Or were you proposing using a global timer to coordinate all teleports for both the pets and the player so that no character teleports in the same tick as another character? That could work, however I’m still hoping there’s a solution that keeps all the characters decoupled.

I meant to keep it as it is but whenever it starts just create a tiny pause to not run twice simultaneously.

You could also skip the timer and instead connect Engine.physics_frame signal to a function that would do same as above. Disconnect the signal in the function. Then you get a call back to the node that tried to teleport the frame after.

Or just keep the timer/physics frame connect locally in the node that tries to teleport.

Add a return false to the teleporter function itself, if it cant teleport this frame.

If teleporter.can_teleport():

Do teleport

Else:

Start timer or connect signal depending on what you choose. At completion, retry teleport call.

This way you wont have to pass nodes around as arguments.

IIRC, teleported objects won’t get collision updates until the next physics frame and that’s why the shapecast isn’t finding those collisions even with forced update.

I think that’s why baba suggests gating the teleporter.

AFAIK, you’ll either need to wait for the next physics frame, or track teleport locations and avoid them yourself.

Thanks! That’s an unfortunate engine limitation. Based on this thread, last night I implemented a global autoload called TeleportTiming and gave it methods lock_for_n_frames and is_teleportation_allowed. That was working and I was going to post it here…

But just now I was searching through Godot’s Github issues and found some relevant ones with interesting discussions, including a workaround that makes it so I don’t have to mess around with timing after all.

Apparently, after you teleport an object you’re supposed to be able to update the physics position of that object immediately by calling force_update_transform(). But force_update_transform is broken for CharacterBody2D and other kinematic bodies and it doesn’t do anything.

This comment gave a workaround for that bug which worked for me (at least, I don’t see any problems so far). Set the body’s physics mode to static before calling force_update_transform, and then change it back to kinematic.

My teleports now look like this:

body.global_position = destination
body.reset_physics_interpolation()
PhysicsServer2D.body_set_mode(body.get_rid(), PhysicsServer2D.BODY_MODE_STATIC)
body.force_update_transform()
PhysicsServer2D.body_set_mode(body.get_rid(), PhysicsServer2D.BODY_MODE_KINEMATIC)

and the ShapeCasts are correctly detecting collisions with the bodies that have already teleported in the same frame.