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

