Skip to content

Commit

Permalink
Merge 7249947 into 4963d09
Browse files Browse the repository at this point in the history
  • Loading branch information
kawa-kokosowa committed Sep 7, 2016
2 parents 4963d09 + 7249947 commit 5cb936c
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 50 deletions.
3 changes: 2 additions & 1 deletion demo/demo.py
Expand Up @@ -85,7 +85,8 @@ def update(self, camera, wall_collision_group, layer_size, timedelta):
self.sprite.rect.topleft = oldie
else:
# we did NOT wrap around the screen
collided_with = collide.move_as_close_as_possible(self.sprite, new_coord, wall_collision_group)
closest_position, collided_with = collide.move_as_close_as_possible(self.sprite, new_coord, wall_collision_group)
self.sprite.rect.topleft = closest_position

camera.scroll_to(self.sprite.rect)

Expand Down
70 changes: 53 additions & 17 deletions sappho/collide.py
@@ -1,9 +1,7 @@
"""Handle generic pygame collision.
The ColliderSprite lets you have a positional
sprite, with a mask and rect, which can be
efficiently and easily tested against sprite
groups with similar data.
Maybe this shouldn't ever move a sprite, but always return
a coordinate and any sprites it collides with.
"""

Expand Down Expand Up @@ -56,31 +54,69 @@ def collides_rect_mask(sprite, sprite_group):


def move_as_close_as_possible(sprite, destination, sprite_group):
"""Move along a line to destination coordinate, stopping before
potential collision.
Move as close as possible to `destination` without collision.
"""Return how close sprite can go to destination without collision,
along with the first sprite blocking its progress (if any).
"Position" herein will always refer to a position
for sprite.rect.topleft.
Say you have a SPACESHIP (S), "blank space" (-), the
destination (D), and a collidable BLOCK (B). If the
SPACESHIP moves at a velocity of x+5 y+5, this function
would would take (6, 6) as the `destination`, and it would
return ((3, 3), <sprite of the BLOCK>). This directly
mitigates "bad" collision where you could simply jump from
1,1 to 6,6 despite there being a collision in the path (BLOCK).
123456
1 S-----
2 ------
3 ------
4 ---B--
5 ------
6 -----D
In the example above, if there were no BLOCK (B) the return value
would be ((6,6), None).
Warning:
This isn't fast! It lacks any decent heuristics, such as
line-based collisions. I'll be benchmarking this method
against line-based collision heuristics in the future.
Arguments:
sprite (pygame.Sprite): the sprite to move as close...
sprite (pygame.Sprite): This sprite is used to incrementally
move toward the destination, continuously checking
if colliding with any in sprite_group. The sprite's position
will not be affected (you will have to update the
sprite.rect.topleft respectively, yourself).
destination (tuple[x, y]): The goal coordinate to move to, or
at least as close to as possible before colliding.
sprite_group (pygame.sprite.Group): Pygame sprite group, whose
sprites are check each time we move one pixel
toward the destination.
Returns:
pygame.Sprite: The first sprite detected which prevented
moving further in the path.
None: Moved to destination without collision.
tuple: The first element is the "topleft" coordinate
representing the closest sprite may move toward the
destination before a collision occurs. The first element
coordinate will be one of the following:
* At the original "topleft" position of sprite, i.e.,
sprite.rect.topleft
* The destination provided: if there were no collisions
moving between original position and destination.
* In between original and destination positions: when
there was a collision, this will be the last value
that didn't collide along that path toward destination.
The second element is the first sprite which
prevents progressing further toward destination. The second
element could be None if sprite can move to destination
without any collisions from sprite_group.
"""

original_position = sprite.rect.topleft

# Figure out the x and y increments!
#
# I use "increment" herein to mean "step which approaches,
Expand Down Expand Up @@ -123,19 +159,19 @@ def move_as_close_as_possible(sprite, destination, sprite_group):
colliding_with = collides_rect_mask(sprite, sprite_group)

if colliding_with:
sprite.rect.topleft = last_safe_topleft
return colliding_with
sprite.rect.topleft = original_position
return (last_safe_topleft, colliding_with)

return None
return (destination, None)


def sprites_in_orthogonal_path(sprite, new_coord, sprite_group):
"""Return the sprites this ColliderSprite would "run through"
and thus collide with if it moved to new_coord.
Warning:
This does not work diagonally! This is shamefully bad, but
works perfectly for orthogonal movement.
This does not work diagonally! This is only good for
quickly checking collisions along an orthogonal path.
Arguments:
new_coord (tuple[int, int]): topleft coordinate value this
Expand Down
72 changes: 40 additions & 32 deletions tests/test_collide.py
Expand Up @@ -2,46 +2,54 @@

import pygame

from sappho import collide, animate
from sappho import animate
from sappho import collide


class TestColliderSprite(object):
# this sprite is 10x10
testpath = os.path.realpath(__file__)
path = os.path.abspath(os.path.join(testpath,
"..",
"resources",
"animatedsprite.gif"))

# NOTE, TODO: this is a pretty bad test. Ideally, it
# would do something more specific in addition to retesting
# after the collision_sprite is updated, which should then
# correspond to that position of the GIF/AnimatedSprite.
def test_basic_attributes(self):
testpath = os.path.realpath(__file__)
path = os.path.abspath(os.path.join(testpath,
"..",
"resources",
"animatedsprite.gif"))
animsprite = animate.AnimatedSprite.from_gif(path,
mask_threshold=254)
collision_sprite = collide.ColliderSprite(animsprite)
animsprite_mask_20_20 = animate.AnimatedSprite.from_gif(
path,
mask_threshold=254
)

assert collision_sprite.rect.size == (10, 10)
assert hasattr(collision_sprite, 'mask')
# TODO: should test after updating sprite
animsprite_mask_20_20.rect.topleft = (20, 20)

def mock_sprite_group(self):
pass
animsprite_mask_40_40 = animate.AnimatedSprite.from_gif(
path,
mask_threshold=254
)

def test_collides_rect(self):
pass
animsprite_mask_40_40.rect.topleft = (40, 40)

def test_collides_rect_mask(self):
pass
animsprite_group_sans_one = pygame.sprite.Group(animsprite_mask_40_40)


# The below pattern can be used for collides_rect, collides_Rect_mask,
# try_to_move
def test_move_close_as_possible():
"""
# Create a group of sprites with various unique positions
Move `animsprite_mask_20_20` to (60, 60), which should collide
with both `animsprite_mask_40_40` and `animsprite_35_40`.
# Create a sprite which will be checked for collisions
# against the group of sprites created in the last step.
# We intentionally place this collidersprite somewhere that'll
# collide with at least one colliddersprite from the group of
# the last step
"""

closest_to_goal, collided_with = collide.move_as_close_as_possible(
animsprite_mask_20_20,
(60, 60),
animsprite_group_sans_one
)
assert closest_to_goal == (30, 30)
assert collided_with is animsprite_mask_40_40

closest_to_goal, collided_with = collide.move_as_close_as_possible(
animsprite_mask_20_20,
(10, 10),
animsprite_group_sans_one
)
assert closest_to_goal == (10, 10)
assert collided_with is None

0 comments on commit 5cb936c

Please sign in to comment.