0
\$\begingroup\$

I am working on an action platformer and am trying to implement an enemy with simple wall-following behavior. I want the enemy to follow the boundary of whatever connected constellation of walls it is standing on, whether it's a freestanding platform or a connected room.

The enemy has a position, a velocity, a bounding rectangle, various points on the bounding rectangle (topleft, bottomright, etc.), and various points raycasted a few pixels outside the bounding rectangle (midleft_raycast, bottom_raycast, etc.). The walls are rectangular and have the usual attributes of a rectangle.

Currently I am implementing the wall-following behavior in the following way. If the enemy has a positive horizontal velocity, I check whether various corners intersect or don't intersect a wall. For example, if the bottomleft and bottomright no longer intersect a wall, I know the enemy has moved over a gap and his velocity needs to be adjusted for him to move downward. I realign his bottomleft corner to the wall's topright corner and change his velocity to a positive vertical velocity. I do something similar for the other cases.

Here is an illustration:

![enter image description here

And here is my code:

        if self.state == "patrolling":

            # sprite travelling right
            if self.vel == vec(1, 0):
                if not tools.point_collides_any_wall(self.game, self.bottomleft) and not tools.point_collides_any_wall(self.game, self.bottomright):
                    # reposition sprite so it's touching the wall
                    self.bounding_rect.bottomleft = self.platform.rect.topright
                    # change velocity
                    self.vel = vec(0, 1)

                if tools.point_collides_any_wall(self.game, self.bottomleft) and tools.point_collides_any_wall(self.game, self.bottomright) and tools.point_collides_any_wall(self.game, self.topright):
                    # change velocity
                    self.vel = vec(0, -1)
                    # set platform
                    self.platform = [wall for wall in self.game.walls if wall.rect.collidepoint(self.midright_raycast)][0]

            # sprite travelling left
            elif self.vel == vec(-1, 0):
                if not tools.point_collides_any_wall(self.game, self.topleft) and not tools.point_collides_any_wall(self.game, self.topright):
                    # reposition sprite so it's touching the wall
                    self.bounding_rect.topright = self.platform.rect.bottomleft
                    # change velocity
                    self.vel = vec(0, -1)

                if tools.point_collides_any_wall(self.game, self.topleft) and tools.point_collides_any_wall(self.game, self.topright) and tools.point_collides_any_wall(self.game, self.bottomleft):
                    # change velocity
                    self.vel = vec(0, 1)
                    # set platform
                    self.platform = [wall for wall in self.game.walls if wall.rect.collidepoint(self.midleft_raycast)][0]


            # sprite travelling up
            elif self.vel == vec(0, -1):
                if not tools.point_collides_any_wall(self.game, self.topright) and not tools.point_collides_any_wall(self.game, self.bottomright):
                    # reposition sprite so it's touching the wall
                    self.bounding_rect.bottomright = self.platform.rect.topleft
                    # change velocity
                    self.vel = vec(1, 0)

                if tools.point_collides_any_wall(self.game, self.topleft) and tools.point_collides_any_wall(self.game, self.topright) and tools.point_collides_any_wall(self.game, self.bottomright):
                    # change velocity
                    self.vel = vec(-1, 0)
                    # set platform
                    self.platform = [wall for wall in self.game.walls if wall.rect.collidepoint(self.midtop_raycast)][0]


            # sprite travelling down
            elif self.vel == vec(0, 1):
                if not tools.point_collides_any_wall(self.game, self.topleft) and not tools.point_collides_any_wall(self.game, self.bottomleft):
                    # reposition sprite so it's touching the wall
                    self.bounding_rect.topleft = self.platform.rect.bottomright
                    # change velocity
                    self.vel = vec(-1, 0)

                if tools.point_collides_any_wall(self.game, self.topleft) and tools.point_collides_any_wall(self.game, self.bottomleft) and tools.point_collides_any_wall(self.game, self.bottomright):
                    # change velocity
                    self.vel = vec(1, 0)
                    # set platform
                    self.platform = [wall for wall in self.game.walls if wall.rect.collidepoint(self.midbottom_raycast)][0]

            self.pos = self.bounding_rect.center

This method works, but I can't help but think that there is a more elegant way. For example, what if I wanted the enemy to move in the opposite direction. I would need to write another chunk of (symmetric) code for this.

So my question is: Are there other, simpler, or more general ways to implement wall-following behavior?

\$\endgroup\$
1
\$\begingroup\$

You could store your test points in a 2D array, like...

[0, 0]     [1, 0]


[0, 1]     [1, 1]

Then store two pairs: one representing the direction of movement, and one representing the direction to the nearest wall.

Using these two vectors, you can compose the indices for each corner you need to test:

trailingOutsideCorner.x = (movement.x - toWall.x) > 0 ? 0 : 1

trailingOutsideCorner.y = (movement.y - toWall.y) > 0 ? 0 : 1

This gives you the index of the rear corner against the wall you're travelling along. You can test that point for collisions, and if you don't get one, then you turn outward:

// move in the direction toward the wall we were following
newMovement = toWall

// our new wall is behind the direction we were going
newToWall = -movement

You can form a similar expression to test your leading inside corner:

leadingInsideCorner.x = (movement.x - toWall.x) > 0 ? 1 : 0

leadingInsideCorner.y = (movement.y - toWall.y) > 0 ? 1 : 0

If that has a collision, then you need to make an inside turn:

// move away from the wall we were on
newMovement = -toWall

// our new wall is in the direction we were travelling
newToWall = movement
\$\endgroup\$
  • \$\begingroup\$ Great answer. In the case of the outward turn when no collision is detected on the trailing outside corner, I will still need to realign the sprite's rect to the wall's rect to make them flush again. Any tips on that? \$\endgroup\$ – GoldenGremlin Jan 26 '18 at 13:19
  • 1
    \$\begingroup\$ If you keep a memory of where you were last frame, then you know the last place you had a collision at this point. A raycast from the now vacant point back to that last collision point will give you the exact distance to the end of the wall. \$\endgroup\$ – DMGregory Jan 26 '18 at 13:41
  • \$\begingroup\$ I think I'm missing something. The previous frame may not have the trailing outside corner colliding flush with the wall's corner, especially at large velocities, so it doesn't really tell me the distance to the wall. Wouldn't I be better off just realigning my sprite using the trailing corner and the nearest corner on the wall, before I reassign my movement and toWall vectors? \$\endgroup\$ – GoldenGremlin Jan 26 '18 at 16:47
  • 1
    \$\begingroup\$ I think you missed "a raycast from...to" — I wasn't recommending using the previous point itself as your output, but about using it to bound your scan for the actual wall corner. \$\endgroup\$ – DMGregory Jan 26 '18 at 16:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.