m

How to prevent character being stucked inside a wall when the powerup runs out?

momo_mimi

If the ghost powerup runs out when we are inside a wall, our character can be stucked. I imagine it should be popped out from the wall instead.

Any suggestion how to solve this?

Screen-Shot-2023-10-05-at-10-18-38

  • Nathan Lovato replied

    It's a pretty long answer and beyond the level you're meant to be at in the course. I tried to find a relatively accessible solution this morning with raycasts, but it fails in some cases, so I'll come back to you with a more complete video.

    It's a topic that could help many people in the community so I think we'll put it on youtube. But I'll need some more time to do this one.

    What I tried, that didn't work reliably enough: using a Raycast2D node (you'll learn about this later in the course). With code like this (I tried a bunch of variations). You'll find some comments explaining the logic.

    onready var ray_cast_2d: RayCast2D = $CollisionShape2D/RayCast2D
    onready var collision_shape_2d: CollisionShape2D = $CollisionShape2D
    
    func get_character_unstuck() -> void:
    	# Make the ray point in the direction the player is moving
    	if velocity.length() > 0.0:
    		ray_cast_2d.cast_to = velocity.normalized() * 140.0
    	# If the character isn't moving, default to the RIGHT direction
    	else:
    		ray_cast_2d.cast_to = Vector2.RIGHT * 140.0
    	
    	# Force the ray to update to get accurate collision information.
    	ray_cast_2d.force_update_transform()
    	ray_cast_2d.force_raycast_update()
    	
    	# If it's colliding, move the character to the collision point, and offset by the character's collision shape radius in the ray's direction.
    	if ray_cast_2d.is_colliding():
    		var collision_point := ray_cast_2d.get_collision_point()
    		global_position = collision_point + collision_shape_2d.shape.radius * ray_cast_2d.cast_to.normalized()

    It almost always works, but in some cases, like if the ray is pointing down, it will detect a collision with the wall it's inside of, while in other directions, it will usually reliably detect the edges of the walls. I think it's just not designed to be used from inside of a collision shape.

    If it worked, it would have been a relatively beginner-friendly solution.

    In short, to get a reliable solution to this problem, I would:

    1. Manually create physics queries in code. It's like using an Area2D node, but in code, and it lets us instantly check multiple places for collisions, while we don't control when an Area2D's physics information updates.

    2. In a loop, we do physics queries with a small circle, we keep looping and offsetting the shape forward in the player's movement direction until the shape doesn't touch any walls. That's a safe place to move the character.

    3. We place or animate moving the character at the position of the last query.

    I'll work on a code example and a video screencast as soon as possible. I've got a big workload and just spent 1h on a raycast-based solution, so I can't complete it right now. But I'll get back to this!

    3 loves
  • Nathan Lovato replied

    The general problem here is that when the player is stuck, we need to probe around the character to find a good place to move it out of the walls.

    We can find a safe position for the player with a loop, using the character's collision shape. In a loop:


    • We virtually offset the physics shape by a small distance.
    • We test that offset shape for collisions with walls.
      • If the test resulted in a collision, it means we're still over a wall. So, we go to the next loop iteration and offset the shape more.
      • If there is no collision, it means we're not over a wall, so we move the player to that position and break from the loop.

      Here's code that can reliably address the issue. Please don't let it intimidate you too much, it's a tad more advanced than where you're supposed to be at while following along the course. I've included comments in the code too to explain the different lines, which makes it seem even longer.

      This function finds a safe position to move the player, based on the direction they're moving (the default is Vector2.RIGHT if the player stopped moving).


      onready var collision_shape_2d: CollisionShape2D = $CollisionShape2D
      
      
      func find_safe_global_position() -> Vector2:
      	# The position we'll return from the function. It should be a safe place, that is to say, a position where the player won't collide with walls.
      	var safe_global_position := Vector2.INF
      	
      	# Prepare a physics query. Physics2DShapeQueryParameters is the type the physics engine asks us to provide to make a direct physics test.
      	var query := Physics2DShapeQueryParameters.new()
      	query.set_shape(collision_shape_2d.shape)
      	query.collision_layer = 1
      
      	# Here we precalculate how much to offset the collision test each time if we're still over a wall.
      	var direction := velocity.normalized() if velocity.length() > 0.0 else Vector2.RIGHT
      	var offset_per_iteration: Vector2 = collision_shape_2d.shape.radius * direction
      
      	# The Physics2DDirectSpaceState type gives us direct access to the physics engine.
      	var direct_space_state := get_world_2d().direct_space_state
      	
      	var query_offset := Vector2.ZERO
      	# We test for physics in a loop, up to 100 times here.
      	for i in range(100):
      		# We change the position of the query (transform combines position + rotation + scale)
      		query.transform = global_transform.translated(query_offset)
      		# And test for collisions. If the shape collides with a wall, the function will return an array with one collider. Otherwise, it'll return an empty array.
      		var intersecting_objects: Array = direct_space_state.intersect_shape(query, 1)
      		# If the array is empty, then this is a safe spot so we store the value and end the loop.
      		if intersecting_objects.empty():
      			safe_global_position = query.transform.origin
      			break
      		# Otherwise, we update the desired query offset and loop and do the next collision test. 
      		else:
      			query_offset += offset_per_iteration
      
      	return safe_global_position

      You would use it like this:

      func toggle_ghost_effect(is_on: bool) -> void:
      #...
      	var safe_position := find_safe_global_position()
      	if safe_position != Vector2.INF:
      		global_position = safe_position

      In this case, I've written a function that returns a value, but of course, you could directly move the character inside of the find_safe_global_position() function. Either way is fine.

      I'm not 100% sure we'll do a youtube vid based on this, but at least I hope this helps. If you have questions about the code please let me know.

      4 loves