# HG changeset patch # User Simon Cross # Date 1301935548 -7200 # Node ID a1d95c6152a0ddd5d83d09f2a643b29e58e2da6c # Parent 4a8ee039549212b18d95576719e9ca57f1bd4a83 Shiny new collision detection. Read code for usage information. diff -r 4a8ee0395492 -r a1d95c6152a0 skaapsteker/level.py --- a/skaapsteker/level.py Mon Apr 04 17:38:48 2011 +0200 +++ b/skaapsteker/level.py Mon Apr 04 18:45:48 2011 +0200 @@ -45,6 +45,8 @@ tile.block = block tile.floor = floor tile._layer = layer + if not (tile.block or tile.floor): + tile.collides_with = set() return tile return _tilefac diff -r 4a8ee0395492 -r a1d95c6152a0 skaapsteker/physics.py --- a/skaapsteker/physics.py Mon Apr 04 17:38:48 2011 +0200 +++ b/skaapsteker/physics.py Mon Apr 04 18:45:48 2011 +0200 @@ -11,12 +11,19 @@ class Sprite(pygame.sprite.DirtySprite): + # physics attributes mobile = True # whether the velocity may be non-zero gravitates = True # whether gravity applies to the sprite - terminal_velocity = (300.0, 300.0) # maximum horizontal and vertial speeds (pixels / s) bounce_factor = (0.98, 1.05) # bounce factor + # collision attributes + # Sprite X collides with Y iff (X.collision_layer in Y.collides_with) and X.check_collides(Y) + # Collisions result in the colliding movement being partially backed out, a call to X.bounce(frac) and a call to X.collided(Y) + # X.bounce(frac) is only called for the first (as determined by backing out distance) collision in a multi-collision event + collision_layer = None # never collides with anything + collides_with = set() # nothing collides with this + def __init__(self, *args, **kwargs): super(Sprite, self).__init__(*args, **kwargs) self.velocity = (0.0, 0.0) @@ -51,35 +58,25 @@ self._float_pos = f_x, f_y self.rect.topleft = int(f_x), int(f_y) - def collide(self, other): + def check_collides(self, other): + return True # default to relying purefly on collision_layer and collides_with + + def collided(self, other): print "Collided:", self, other - def collide_immobile(self, immobile): - print "Collided with immobile:", self, immobile - if not self.rect.colliderect(immobile.rect): - print " Collision avoided!" - return - - v_x, v_y = self.velocity - clip = self.rect.clip(immobile.rect) - MAX_DT = 0.1 - frac_x = clip.width / abs(v_x) if abs(v_x) > EPSILON else MAX_DT - frac_y = clip.height / abs(v_y) if abs(v_y) > EPSILON else MAX_DT + def bounce(self, other, normal): + """Alter velocity after a collision. - if frac_x > frac_y: - # collision in y - frac = frac_y - b_y = -v_y * self.bounce_factor[1] * immobile.bounce_factor[1] - b_x = v_x - else: - # collision in x - frac = frac_x - b_x = -v_x * self.bounce_factor[0] * immobile.bounce_factor[0] - b_y = v_y - - self.velocity = (-v_x, -v_y) - self.deltap(frac * 1.1) - self.velocity = (b_x, b_y) + other: sprite collided with + normal: unit vector (tuple) normal to the collision + surface. + """ + v_x, v_y = self.velocity + b_x = 1.0 + self.bounce_factor[0] * other.bounce_factor[0] + b_y = 1.0 + self.bounce_factor[1] * other.bounce_factor[1] + v_x = (1.0 - normal[0] * b_x) * v_x + v_y = (1.0 - normal[1] * b_y) * v_y + self.velocity = v_x, v_y class World(object): @@ -89,8 +86,8 @@ def __init__(self): self._all = pygame.sprite.LayeredUpdates() self._mobiles = pygame.sprite.Group() - self._immobiles = pygame.sprite.Group() self._gravitators = pygame.sprite.Group() + self._collision_groups = { None: pygame.sprite.Group() } self._last_time = None def add(self, sprite): @@ -98,10 +95,39 @@ self._all.add(sprite) if sprite.mobile: self._mobiles.add(sprite) - elif (sprite.floor or sprite.block): - self._immobiles.add(sprite) if sprite.gravitates: self._gravitators.add(sprite) + self._add_collision_group(sprite.collision_layer) + for layer in sprite.collides_with: + self._add_collision_group(layer) + self._collision_groups[layer].add(sprite) + + def _add_collision_group(self, layer): + if layer in self._collision_groups: + return + self._collision_groups[layer] = pygame.sprite.Group() + + def _backout_collisions(self, sprite, others, dt): + frac, normal, idx = 0.0, None, None + v_x, v_y = sprite.velocity + abs_v_x, abs_v_y = abs(v_x), abs(v_y) + + for i, other in enumerate(others): + clip = sprite.rect.clip(other.rect) + # TODO: avoid continual "if abs_v_? > EPSILON" + frac_x = clip.width / abs_v_x if abs_v_x > EPSILON else dt + frac_y = clip.height / abs_v_y if abs_v_y > EPSILON else dt + if frac_x > frac_y: + if frac_y > frac: + frac, normal, idx = frac_y, (0, 1), i + else: + if frac_x > frac: + frac, normal, idx = frac_x, (1, 0), i + + sprite.deltap(max(-1.1 * frac, -dt)) + sprite.bounce(others[idx], normal) + for other in others: + sprite.collided(other) def update(self): if self._last_time is None: @@ -117,29 +143,16 @@ for sprite in self._gravitators: sprite.deltav(dv) - # position update (do last) + # position update and collision check (do last) for sprite in self._mobiles: sprite.deltap(dt) - - # check for collisions - collisions = [] - collide = collisions.append - for sprite1 in self._mobiles.sprites(): - spritecollide = sprite1.rect.colliderect - for sprite2 in self._mobiles.sprites(): - if id(sprite1) < id(sprite2) and spritecollide(sprite2): - collide((sprite1, sprite2)) - for sprite2 in self._immobiles.sprites(): - if spritecollide(sprite2): - collide((sprite1, sprite2)) - self.dispatch_collisions(collisions) - - def dispatch_collisions(self, collisions): - for s1, s2 in collisions: - if not s2.mobile: - s1.collide_immobile(s2) - else: - s1.collide(s2) + sprite_collides = sprite.rect.colliderect + collisions = [] + for other in self._collision_groups[sprite.collision_layer]: + if sprite_collides(other) and sprite.check_collides(other): + collisions.append(other) + if collisions: + self._backout_collisions(sprite, collisions, dt) def draw(self, surface): self._all.draw(surface) diff -r 4a8ee0395492 -r a1d95c6152a0 skaapsteker/sprites/base.py --- a/skaapsteker/sprites/base.py Mon Apr 04 17:38:48 2011 +0200 +++ b/skaapsteker/sprites/base.py Mon Apr 04 18:45:48 2011 +0200 @@ -9,9 +9,16 @@ TILE_SIZE = (64, 64) +# Collision Layers (values are ids not numbers) +PC_LAYER = 0 +MONSTER_LAYER = 1 + class Monster(Sprite): + image_file = None + collision_layer = MONSTER_LAYER + collides_with = set([PC_LAYER]) def __init__(self, pos, **opts): Sprite.__init__(self) @@ -42,7 +49,7 @@ class Geography(Sprite): mobile = False gravitates = False - + collides_with = set([PC_LAYER, MONSTER_LAYER]) def __init__(self, pos, image): Sprite.__init__(self) @@ -51,7 +58,6 @@ self.rect = Rect((pos[0] * TILE_SIZE[0], pos[1] * TILE_SIZE[1]), TILE_SIZE) - def find_sprite(descr): """Create a sprite object from a dictionary describing it.""" descr = descr.copy() diff -r 4a8ee0395492 -r a1d95c6152a0 skaapsteker/sprites/player.py --- a/skaapsteker/sprites/player.py Mon Apr 04 17:38:48 2011 +0200 +++ b/skaapsteker/sprites/player.py Mon Apr 04 18:45:48 2011 +0200 @@ -3,7 +3,7 @@ import pygame.transform import os -from skaapsteker.sprites.base import TILE_SIZE +from skaapsteker.sprites.base import TILE_SIZE, PC_LAYER from skaapsteker.physics import Sprite from skaapsteker.constants import Layers from skaapsteker.data import get_files, load_image @@ -11,6 +11,8 @@ class Player(Sprite): + collision_layer = PC_LAYER + def __init__(self): Sprite.__init__(self) self.image = None