Reputation: 3889
I have two sprites: a robot sprite and an obstacle sprite. I am using mask.overlap to determine if there is an overlap to prevent the robot from moving into the area of the obstacle (it functions as a blocking obstacle). Below is a portion of the movement evaluation code. It tests to see if the movement will cause a collision:
if pressed_keys[pygame.K_s]:
temp_position = [robot.rect.x, robot.rect.y]
temp_position[1] += speed
offset_x = temp_position[0] - obstacle.rect.x
offset_y = temp_position[1] - obstacle.rect.y
overlap = obstacle.mask.overlap(robot.mask, (offset_x, offset_y))
if overlap is None:
robot.rect.y += speed
else:
# adjust the speed to make the objects perfectly collide
This code works. If the movement would cause a collision, then it prevents the robot from moving.
ISSUE
For high speeds, the code prevents movement like it should, but it leaves a visual gap between the robot and the obstacle.
For example: if the speed is 30 and the two obstacles are 20 pixels away, the code will prevent the movement because a collision would be caused. But leaves a 20 pixel gap.
GOAL
If there were to be a collision, adjust the speed to the remaining pixel distance (20px like in the example) so that the robot and the obstacle perfectly collide. The robot can't move 30, but he can move 20. How can I calculate that remaining distance?
Upvotes: 3
Views: 648
Reputation: 3889
I decided to go with the approach suggested by skrx in his comment: to basically back up by 1px until there is no longer a collision.
if pressed_keys[pygame.K_s]:
temp_position = [robot.rect.x, robot.rect.y]
temp_position[1] += speed
offset_x = temp_position[0] - obstacle.rect.x
offset_y = temp_position[1] - obstacle.rect.y
overlap = obstacle.mask.overlap(robot.mask, (offset_x, offset_y))
if overlap is None:
robot.rect.y += speed
else:
for step_speed in range(1, speed - 1):
collision[1] -= 1
offset_x = collision[0] - obstacle.rect.x
offset_y = collision[1] - obstacle.rect.y
overlap_adj = obstacle.mask.overlap(robot.mask, (offset_x, offset_y))
if overlap_adj is None:
robot.rect.y += (speed - step_speed)
break
This is a bit of a clumsy approach, but it will satisfy what i need for now and keep vector math at bay. For those who are looking for the proper way to approach this using normalized vectors and such, i would recommend using the answer skrx provided. I will likely come back to this and update it in the future. But for now, this will give users a couple options on how to proceed with perfect collision.
Upvotes: 1
Reputation: 20478
Here's what I described in the comment. Check if the sprites are colliding (I use spritecollide
and the pygame.sprite.collide_mask
functions here), and then use the normalized negative velocity vector to move the player backwards until it doesn't collide with the obstacle anymore.
import pygame as pg
from pygame.math import Vector2
pg.init()
screen = pg.display.set_mode((800, 600))
GRAY = pg.Color('gray12')
CIRCLE_BLUE = pg.Surface((70, 70), pg.SRCALPHA)
pg.draw.circle(CIRCLE_BLUE, (0, 0, 230), (35, 35), 35)
CIRCLE_RED = pg.Surface((170, 170), pg.SRCALPHA)
pg.draw.circle(CIRCLE_RED, (230, 0, 0), (85, 85), 85)
class Player(pg.sprite.Sprite):
def __init__(self, pos, key_left, key_right, key_up, key_down):
super().__init__()
self.image = CIRCLE_BLUE
self.mask = pg.mask.from_surface(self.image)
self.rect = self.image.get_rect(topleft=pos)
self.vel = Vector2(0, 0)
self.pos = Vector2(self.rect.topleft)
self.dt = 0.03
self.key_left = key_left
self.key_right = key_right
self.key_up = key_up
self.key_down = key_down
def handle_event(self, event):
if event.type == pg.KEYDOWN:
if event.key == self.key_left:
self.vel.x = -230
elif event.key == self.key_right:
self.vel.x = 230
elif event.key == self.key_up:
self.vel.y = -230
elif event.key == self.key_down:
self.vel.y = 230
elif event.type == pg.KEYUP:
if event.key == self.key_left and self.vel.x < 0:
self.vel.x = 0
elif event.key == self.key_right and self.vel.x > 0:
self.vel.x = 0
elif event.key == self.key_down and self.vel.y > 0:
self.vel.y = 0
elif event.key == self.key_up and self.vel.y < 0:
self.vel.y = 0
def update(self, dt):
self.pos += self.vel * dt
self.rect.center = self.pos
class Obstacle(pg.sprite.Sprite):
def __init__(self, pos):
super().__init__()
self.image = CIRCLE_RED
self.mask = pg.mask.from_surface(self.image)
self.rect = self.image.get_rect(topleft=pos)
class Game:
def __init__(self):
self.done = False
self.clock = pg.time.Clock()
self.screen = screen
self.player = Player((100, 50), pg.K_a, pg.K_d, pg.K_w, pg.K_s)
obstacle = Obstacle((300, 240))
self.all_sprites = pg.sprite.Group(self.player, obstacle)
self.obstacles = pg.sprite.Group(obstacle)
def run(self):
while not self.done:
self.dt = self.clock.tick(60) / 1000
self.handle_events()
self.run_logic()
self.draw()
pg.quit()
def handle_events(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self.done = True
elif event.type == pg.MOUSEBUTTONDOWN:
if event.button == 2:
print(BACKGROUND.get_at(event.pos))
self.player.handle_event(event)
def run_logic(self):
self.all_sprites.update(self.dt)
collided_sprites = pg.sprite.spritecollide(
self.player, self.obstacles, False, pg.sprite.collide_mask)
for obstacle in collided_sprites:
# The length of the velocity vector tells us how many steps we need.
for _ in range(int(self.player.vel.length())+1):
# Move back. Use the normalized velocity vector.
self.player.pos -= self.player.vel.normalize()
self.player.rect.center = self.player.pos
# Break out of the loop when the masks aren't touching anymore.
if not pg.sprite.collide_mask(self.player, obstacle):
break
def draw(self):
self.screen.fill(GRAY)
self.all_sprites.draw(self.screen)
pg.display.flip()
if __name__ == '__main__':
Game().run()
Upvotes: 2
Reputation: 40013
You can pretty easily get a precise (if not exact) solution via a bisection search: once the collision is detected at the end of the full step, try half a step, and then either one or three quarters, and so on. This is treating the collision test as a boolean-valued function of the movement distance and looking for a “zero” (really the transition from miss to hit).
Note that this does nothing to resolve the issue of clipping through a thin wall or corner (where the initial collision test fails to detect the obstacle) and with complicated obstacles will find an arbitrary edge (not necessarily the first) to stop at.
Upvotes: 1