changeset 97:a1d95c6152a0

Shiny new collision detection. Read code for usage information.
author Simon Cross <hodgestar@gmail.com>
date Mon, 04 Apr 2011 18:45:48 +0200
parents 4a8ee0395492
children 449a9579ec7a
files skaapsteker/level.py skaapsteker/physics.py skaapsteker/sprites/base.py skaapsteker/sprites/player.py
diffstat 4 files changed, 77 insertions(+), 54 deletions(-) [+]
line wrap: on
line diff
--- 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
 
--- 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)
--- 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()
--- 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