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.
