Search code examples
game-developmentgodot

In Godot 4.0 is there a way to detect only collisions that happen on the horizontal axis with move_and_collid function?


In a platformer game built in godot, this is how I would go about making an enemy move autonomously from side to side (meaning he will walk up to a wall, collide with it, and turn around.

extends CharacterBody3D

var speed = 0.1
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
func _physics_process(delta):

    if !is_on_floor():
        velocity.y = gravity

    velocity.x = speed
        #collided is a boolean representing whether or not the character collided with a body or not
    var collided = move_and_collide(velocity)
    if collided:
        speed *= -1

However, A problem arises with this code. since I have gravity which moves the enemy downwards the enemy collides with the floor, causing the collided boolean to return true. Which causes the enemy to turn around rapidly, keeping him in place.

A solution I thought of is distinguishing between colliding with the floor and a wall so that I could have the enemy turn around only when colliding with a wall. However I am not sure how to do this. I am also wondering whether or not there is a better and easier to implement solution for this.


Solution

  • With move_and_slide

    There was here an answer by "user22159971" suggesting to use move_and_slide and is_on_wall, which is a good idea. However the explanation provided was for Godot 3. And "user22159971" removed the answer instead of updating it.

    In Godot 3 you would have used move_and_slide in a KinematicBody like this:

    move_and_slide(velocity, up_direction)
    

    In Godot 4 you need to set the velocity and up_direction properties of the CharacterBody3D like this:

    velocity = something # you are already working with velocity, so keep that
    up_direction = something_else # probably Vector3.UP
    move_and_slide()
    

    In either case, you could then check is_on_wall to know if the body collided with a wall (or is_on_floor or is_on_ceiling to check for floor or ceiling repsectively):

    if is_on_wall():
        # it is a wall
        pass
    

    With move_and_collide

    Currently you are using move_and_collide. Perhaps you want to continue using that.


    Multiply by delta

    First of all, the method move_and_collide does not multiply internally by delta, so use it like this:

    move_and_collide(velocity * delta)
    

    Since delta is less than 1.0 (or you have bigger problems), this will result in the body moving slower than it did, so you might want to increase the velocity to compensate.


    Get the returned KinematicCollision3D

    The method move_and_collide returns a KinematicCollision3D or null if it didn't collide. It does not return a boolean.

    Actually this code:

    var collided = move_and_collide(velocity * delta)
    if collided:
        speed *= -1
    

    Is equivalent to this code:

    var collided:KinematicCollision3D = move_and_collide(velocity * delta)
    if collided != null:
        speed *= -1
    

    I bring this up, because you could look into the properties of the KinematicCollision3D to get more information, in particular the normal would allow you to distinguish if the body collided with a wall (or floor or ceiling):

    var collision := move_and_collide(velocity * delta)
    if collision == null:
        # did not collide
        pass
    else:
        var normal := collision.get_normal()
        var angle := mormal.angle_to(Vector3.UP) # or whatever up you are using
        if angle < floor_max_angle:
            # it is a floor
            pass
        elif angle > (PI - floor_max_angle):
            # it is a ceiling
            pass
        else:
            # it is a wall
            pass
    

    Handle multiple collisions

    I should further point out that obviously the body might have collided with multiple things. The last parameter of move_and_collide allows you to tell Godot how many collisions to get. It is 1 by default. We can change that:

    var collisions := move_and_collide(velocity * delta, false, 0.001, false, 32)
    

    Here we are telling it to:

    • Move velocity * delta distance.
    • Actually move (false here means to not do a simulation test).
    • With a safe margin of 0.001 (this is an extra margin distance for the collision, 0.001 is the default).
    • Don't do report collisions from the recovery from collision (when the physics engine detects that the body intersects another, it will push it out. That is the recovery. And it might result in it colliding with something else. false here means we don't want to get those in the result).
    • And report up to 32 collisions (the thing we actually wanted to change, again 1 is the default).

    Now, we can iterate over the collisions:

    var collisions := move_and_collide(velocity * delta, false, 0.001, false, 32)
    if collisions == null:
        # did not collide
        pass
    else:
        for index in collisions.get_collision_count():
            var normal := collision.get_normal(index)
            var angle := mormal.angle_to(Vector3.UP) # or whatever up you are using
            if angle < floor_max_angle:
                # it is a floor
                pass
            elif angle > (PI - floor_max_angle):
                # it is a ceiling
                pass
            else:
                # it is a wall
                pass