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:
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?