move_and_slide sometimes not detecting all of the collisions

Godot Version

v4.6.2

Question

Hello! I was working on a simple grappling hook ability for my game and to detect if the player reached the target object, i use the move_and_slide collision data from the player and while this works well in most cases, i have noticed that sometimes if i try to pull to a corner of an object, the move_and_slide doesnt detect that collision, only the wall the object was placed on. After that, the collision breaks entirely and even when i try spots where the collision would be detected in normal circumstances, it doesn’t and my ability breaks. At this point, i’m sure it has got to be some kind of engine bug but i’m posting this here in case a solution could still be found.

here’s my code:

the ability code (placed inside a state in a state machine):

extends WalkState

@export var magnetic_grapple_slash_sprite: AnimatedSprite2D
@export var magnetic_grapple_collision_raycast: RayCast2D

var can_pull: bool = true
var magnetic_grapple_no_input: float = 0.05
var magnetic_grapple_cooldown: int = 0
var target_object: Node2D
var reached_destination: bool
var target_position: Vector2 = Vector2.ZERO
var owner_initial_position: Vector2

func transition():
	super()

	if state_machine.current_state != self:
		can_pull = false

		await get_tree().create_timer(magnetic_grapple_cooldown).timeout

		can_pull = true

func enter():
	target_object = owner.magnetic_grapple_aim_raycast.get_collider()
	owner.can_attack = false
	auto_transition = false
	owner.affected_by_gravity = false
	reached_destination = false
	owner.velocity = Vector2.ZERO
	owner_initial_position = owner.position
	target_position = owner.magnetic_grapple_aim_raycast.get_collision_point()

	magnetic_grapple_collision_raycast.set_collision_mask_value(4, true if target_object is CharacterBody2D else false)
	magnetic_grapple_collision_raycast.set_collision_mask_value(1, true if target_object is TileMapLayer else false)

	movement_plane = get_current_movement_plane()

	if abs(movement_plane.y) > abs(movement_plane.x):
		owner.look_direction.y = round(movement_plane.y)

	wait_until_can_cancel()

	super()

func direction_check(direction):
	var initial_target_position: Vector2 = magnetic_grapple_collision_raycast.target_position
	var collided: bool

	magnetic_grapple_collision_raycast.target_position = direction * 50
	magnetic_grapple_collision_raycast.force_raycast_update()

	if magnetic_grapple_collision_raycast.is_colliding() and magnetic_grapple_collision_raycast.get_collider() == target_object:
		collided = true

	magnetic_grapple_collision_raycast.target_position = initial_target_position
	magnetic_grapple_collision_raycast.force_raycast_update()

	return true if collided else false

func wait_until_can_cancel():
	await get_tree().create_timer(magnetic_grapple_no_input).timeout

	auto_transition = true

func physics_update(delta):
	if is_instance_valid(target_object):
		magnetic_grapple_collision_raycast.target_position = owner.position.direction_to(target_position) * Vector2(40, 40) if target_object is TileMapLayer else movement_plane * walk_speed * delta

		#checking if the player collided with the target object.

		for i in owner.get_slide_collision_count():
			if owner.get_slide_collision(i).get_collider() == target_object:
				reached_destination = true

		if reached_destination:
			#code for when the player reached their destination.

			owner.velocity = Vector2.ZERO

			SignalBus.ui_ability_cooldown_animation_requested.emit("magneticgrapple", magnetic_grapple_cooldown)

			if target_object is CharacterBody2D:
				owner.iframe_timer.start()

				var magnetic_grapple_slash_sprite_size = magnetic_grapple_slash_sprite.sprite_frames.get_frame_texture("slash", 0).get_size()

				if abs(movement_plane.y) > abs(movement_plane.x):
					owner.take_knockback(Vector2.ZERO if target_object.position.y < owner.position.y else Vector2(0, -1), owner.POGOPOWER, false)
					magnetic_grapple_slash_sprite.position = (Vector2.UP if movement_plane.y < 0 else Vector2.DOWN)
					magnetic_grapple_slash_sprite.flip_h = false
					magnetic_grapple_slash_sprite.rotation = -PI / 2 if movement_plane.y < 0 else PI / 2
				else:
					owner.take_knockback(Vector2(1, -1) if target_object.position.x < owner.position.x else Vector2(-1, -1), owner.knockback_power, false)
					magnetic_grapple_slash_sprite.position = (Vector2.LEFT if movement_plane.x < 0 else Vector2.RIGHT)
					magnetic_grapple_slash_sprite.rotation = 0
					magnetic_grapple_slash_sprite.flip_h = true if movement_plane.x < 0 else false

				magnetic_grapple_slash_sprite.position *= (owner.hurtbox_size / 2 + Vector2(magnetic_grapple_slash_sprite_size.x, magnetic_grapple_slash_sprite_size.x) / 2)	
				magnetic_grapple_slash_sprite.show()
				magnetic_grapple_slash_sprite.play("slash")
				owner.magnetic_grapple_cooldown_timer.start()
				owner.combo += owner.player_ability_combo[GlobalEnums.PlayerAbilities.MAGNETIC_GRAPPLE]
				target_object.change_health(owner.player_ability_damage[GlobalEnums.PlayerAbilities.MAGNETIC_GRAPPLE])

		if !reached_destination:
			super(delta)

		owner.look_direction.x = (-1 if target_position.x < owner_initial_position.x else 1) if target_object is TileMapLayer else (-1 if target_object.position.x < owner.position.x else 1)

		# if magnetic_grapple_collision_raycast.is_colliding() and magnetic_grapple_collision_raycast.get_collider() == target_object and !reached_destination:
		# 	reached_destination = true

		# 	owner.velocity = Vector2.ZERO

func get_current_movement_direction():
	return 1

func get_current_movement_plane():
	if target_object is CharacterBody2D:
		return owner.position.direction_to(target_object.position)
	else:
		return owner_initial_position.direction_to(target_position)

func exit():
	owner.velocity = Vector2.ZERO
	owner.affected_by_gravity = true

	if !magnetic_grapple_slash_sprite.is_playing():
		owner.can_attack = true

func on_slash_sprite_animation_finished():
	magnetic_grapple_slash_sprite.hide()
	owner.can_attack = true

code of the base entity and thats where the move_and_slide call is located (extended by the player and all of the entities):

class_name ActionEntity extends CharacterBody2D

@export var movement_controller: Node
@export var state_machine: StateMachine
@export var animation_tree: AnimationTree
@export var body_parts: Node2D
@export var affected_by_gravity: bool = true
@export var collision_shape: CollisionShape2D

signal death

var horizontal_raycasts: Dictionary[String, RayCast2D]
var sliding: bool = false
var friction: int = 1000
var iframe_timer: Timer
var iframe_duration: float = 0.3
var knockback_power: int = 300
var hurtbox_size: Vector2
var max_health: int = 100:
	set(value):
		max_health = value

		health = clamp(value, 0, max_health)

var health: int = max_health:
	set(value):
		health = clamp(value, 0, max_health)

var gravity: Vector2 = Vector2(0, ProjectSettings.get_setting("physics/2d/default_gravity"))
var look_direction: Vector2 = Vector2.RIGHT
var original_velocity: Vector2

func _ready():
	if collision_shape:
		hurtbox_size = collision_shape.shape.get_rect().size

		if find_children("", "RayCast2D").size() > 0:
			for horizontal_raycast in find_children("", "RayCast2D"):
				if horizontal_raycast.is_in_group("horizontal_raycasts"):
					horizontal_raycasts[horizontal_raycast.name.to_lower()] = horizontal_raycast

	if state_machine:
		state_machine.setup(self, movement_controller, animation_tree)

	if movement_controller:
		movement_controller.owner = self
		movement_controller.state_machine = state_machine

	iframe_timer = GlobalFunctions.create_timer_shortcut(self, iframe_duration)

func _physics_process(delta: float) -> void:
	if affected_by_gravity:
		velocity += gravity * delta

	if is_on_floor() and sliding and velocity * gravity.normalized() < Vector2.ZERO and movement_controller.current_movement_direction() == 0:
		velocity *= gravity.normalized()

	move_and_slide()

	sliding = false

	if is_on_floor():
		var slide_axis = velocity.normalized() * -Vector2(gravity.normalized().y, gravity.normalized().x)

		if abs(slide_axis) > Vector2.ZERO:
			velocity += delta * slide_axis * friction
			sliding = true

	if horizontal_raycasts.size() > 0:
		for horizontal_raycast_name in horizontal_raycasts:
			horizontal_raycasts[horizontal_raycast_name].rotation = 0.0 if look_direction.x > 0 else -PI
	
	if body_parts:
		body_parts.scale.x = look_direction.x

func take_knockback(direction, power = knockback_power, no_input = true):
	if state_machine.states.has(GlobalConstants.KNOCKBACK_STATE):
		var knockback_state =  state_machine.states[GlobalConstants.KNOCKBACK_STATE]

		if no_input:
			knockback_state.no_input_timer.start()

		knockback_state.bounce_direction = direction

		if power != knockback_power:
			knockback_state.auto_set_bounce_power = false
			knockback_state.bounce_power = power

		state_machine.change_state(GlobalConstants.KNOCKBACK_STATE)

		knockback_state.auto_set_bounce_power = true

func change_health(health_value, _dmg_knockback_direction = Vector2.ZERO, _entity = null):
	if iframe_timer.is_stopped() and state_machine:
		iframe_timer.start()

		if health_value < 0 and knockback_power > 0:
			take_knockback(_dmg_knockback_direction)
		
		health += health_value

	if health == 0:
		entity_died()

func entity_died():
	death.emit(self)

I’m sorry if i missed anything important for my case or if my explanation is not enough, this is the first time i post here in quite a while.

Hard to say with the code, I find it difficult to follow without any documentation.

Did you set up the collision layers and masks correctly? What do those look like?
Have you enabled “Visible Collision Shapes” to see if the collision shapes act as you expect?

I think i have set them up correctly because its not the issue of it just not registering collision at all, the problem is that it WORKS but not all of the time and sometimes it doesnt register the second collision happening when im on a wall with an object. and i always have visible collision shapes on so i would notice anything wrong with them. Also sorry there’s not much documentation in there and i should’ve stated that the part where you’re velocity is actually changed is in the walk state class which i can show you. the velocity is set to the direction to target position multiplied by speed(which is collision point of the raycast that i use to get the position of where the player wants to be).

here’s how the collision mask and layers look like (first one is for the player and the second one is for target object im trying to pull myself towards):

additionally, on the third pic you can see the frame where the collision should be detected by move_and_slide but it isn’t (the target object it should be colliding with is red rock with bright red gem in the center)

Your layers and masks should collide, looks good there. It looks like your player may just not be colliding with the other object, the upper wall blocks them from moving closer to the gem-block even if it’s less than a pixel distance. You may have to use an Area2D to detect nearby objects without blocking movement.

How did you conclude the collision with the red box should be detected?

i guess im not much into how collision would actually work here behind the scenes but i can tell you that in that frame, it detected collision with the wall (i have tested this by printing the collision datas) thus theres no debate that it SHOULD’VE also detected collision with the other object considering they visibly collided at the same time.

okay thanks but im using a tilemap for both the walls and the other objects so this might not be the case unless theres some kind of error that makes this possible. This all tells me i should probably really just abandon the idea of using move_and_slide and use raycasts or area2Ds how you said it

move_and_slide() works in iterations. If it succeeds in fully separating objects only by looking at the first collider in the queue, it may not bother to do more work than needed.

Use an area or direct shape queries to get all overlaps.

I didn’t know that it works that way but i understand now. I guess i’ll just use the other methods to do it. thank you.