2D Physics Funkyness - Juttering, Launching Across Map, Pushing Others, Stuck in Terrain

Godot Version

v4.2.2.stable.official [15073afe3]

Question

Goal

I want to spawn phyics objects on free/available spaces on a tilemap

Situation

I have a CharacterBody2D, Unit, which spawns some CharacterBody2Ds, Actor. When spawning I am using PhysicsDirectSpaceState2D to check if the space at the target location is empty of other Actors. Occasionally it works, the Actors appear and follow their path. Most of the time, however, I get one or more of the following issues.

Issues

  1. A Unit and its child Actors all stutter back and forth, in place.
  2. A Unit and its child Actors all have the movement locked together, i.e. they all do exactly the same movements.
  3. An Actor shoots across the screen, leaving the map.
  4. Actors push one another around, rather than stopping at the end of their path.

I assume part of the issue is that the Actor is getting caught on the Tilemap collisions and the physics is dealing with that collision as best it can, but I can’t seem to find a way to get the Tilemap’s collisions from the space state query (read the docs, looked around google).

Examples

  1. Instantiating through editor (aka working):
    Godot_v4.2.2_nMRBTCLKVa

  2. Spawning through code and not working:
    Godot_v4.2.2_SgzFXbelWg

Godot_v4.2.2_1kotWdk7aE

Godot_v4.2.2_K1vSckmjlJ

Code

Repo here. Key excerpts below.

# unit.gd
# [...]
func _physics_process(delta: float) -> void:
	if target_destination != Vector2.ZERO:
		var next_path_pos: Vector2 = nav_agent.get_next_path_position()
		var new_velocity: Vector2 = global_position.direction_to(next_path_pos) * movement_speed

		# apply new velocity
		if nav_agent.avoidance_enabled:
			nav_agent.set_velocity(new_velocity)
		else:
			_on_velocity_computed(new_velocity)
# [...]

## spawn required number of actors randomly around the unit's position
func _spawn_actors() -> void:
	var max_attempts: int = 32
	var pos_variance: int = floor(spawn_radius / 2)

	for i in unit_size:
		for j in max_attempts:

			var rand_x : int = randi_range(global_position.x - pos_variance, global_position.x + pos_variance)
			var rand_y : int = randi_range(global_position.y - pos_variance, global_position.y + pos_variance)
			var spawn_pos: Vector2 = Vector2(rand_x, rand_y)

			var space_state: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state
			var shape_query_params: PhysicsShapeQueryParameters2D = PhysicsShapeQueryParameters2D.new()
			var shape: CircleShape2D = CircleShape2D.new()
			shape.radius = 10
			shape_query_params.shape = shape
			shape_query_params.transform.origin = spawn_pos

			# check query
			var results: Array[Dictionary] = space_state.intersect_shape(shape_query_params)
			var filtered_array: Array = results.filter(
				func (collision_object):
						return (collision_object.collider.is_in_group("actor") or collision_object.collider.is_in_group("actor")) 
			)

			# if no collisions
			if filtered_array.size() == 0:
				var actor: Actor = actor_scene.instantiate()
				add_child(actor)
				actor.global_position = spawn_pos

				# colour enemy
				if ally_team_group == "team2":
					actor.modulate = Color.CRIMSON

				print("Unit pos: ", global_position, " | actor (", actor, ") spawned at: ", spawn_pos)
				break

# [...] 
# actor.gd
# [...]

func _physics_process(delta) -> void:
	if current_state == STATE.MOVING and target_destination != Vector2.ZERO:
		var next_path_pos: Vector2 = nav_agent.get_next_path_position()

		# get target vectors and modify by steering forces
		var target_vec: Vector2 = global_position.direction_to(next_path_pos) * movement_speed * target_force
		var flock_status: Array = _get_flocking_info()
		var cohesion_vec: Vector2 = flock_status[0] * cohesion_force
		var align_vec: Vector2 = flock_status[1] * alignment_force
		var separation_vec: Vector2 = flock_status[2] * separation_force

		# consolidate forces
		var acceleration: Vector2 = align_vec + cohesion_vec + separation_vec + target_vec

		# calculate target velocity
		var target_velocity: Vector2 = (velocity  + acceleration).limit_length(max_speed)
		if target_velocity.length() <= min_speed: # TODO: not really sure what this is for
			target_velocity = (target_velocity * min_speed).limit_length(max_speed)

		# lerp towards target velocity
		var new_velocity: Vector2 = velocity.lerp(target_velocity, acceleration_ramp_up)

		# apply new velocity
		if nav_agent.avoidance_enabled:
			nav_agent.set_velocity(new_velocity)
		else:
			_on_velocity_computed(new_velocity)
# [...] 

## get the cohesion, alignment, separation and flock size as an array
func _get_flocking_info() -> Array:
	var centre_vec: Vector2 = Vector2.ZERO
	var flock_centre: Vector2 = Vector2.ZERO
	var align_vec: Vector2 = Vector2.ZERO
	var avoid_vec: Vector2 = Vector2.ZERO
	var flock_count: int   = 0

	var actors: Array = local_neighbours.duplicate() + unit_allies.duplicate()
	#actors.append_array(local_neighbours)  # FIXME: always empty
	#actors.append_array(unit_allies)

	# get uniques only
	var flock: Array = []
	for actor in actors:
		if not flock.has(actor):
			flock.append(actor)

	# get steering forces from flock
	for actor in flock:

		# ignore self
		if actor == self:
			continue

		var other_pos: Vector2 = actor.global_position
		var other_velocity: Vector2  = actor.velocity
		var distance_to_other: float = global_position.distance_to(other_pos)

		flock_count += 1
		align_vec += other_velocity
		flock_centre += other_pos

		if distance_to_other < avoid_distance:
			avoid_vec -= other_pos - global_position

	# average values
	if flock_count:
		align_vec /= flock_count
		flock_centre /= flock_count
		centre_vec /= flock_count

	return [
		centre_vec.normalized(),
		align_vec.normalized(),
		avoid_vec.normalized()
	]

# [...] 

Ask

If there’s any suggestion on what I am doing wrong I’d appreciate it. Alternatively, if there’s a better way to spawn physics objects I’d be happy to try it.