Mercurial > skaapsteker
view skaapsteker/physics.py @ 627:35919d12b792
Path-based collision minimisation and axis-projection backout.
author | Jeremy Thurgood <firxen@gmail.com> |
---|---|
date | Sat, 07 May 2011 20:28:06 +0200 |
parents | 1abb53ae1a6a |
children | 59556235dec7 |
line wrap: on
line source
"""Model of gravity, acceleration, velocities and collisions. Works very closely with sprites/base.py. """ import time import pygame import pygame.draw import pygame.sprite from pygame.mask import from_surface from . import options from .constants import EPSILON from .utils import (cadd, csub, cmul, cdiv, cclamp, cabsmax, cint, cneg, cabs, rect_projection) class Sprite(pygame.sprite.Sprite): # physics attributes mobile = True # whether the velocity may be non-zero gravitates = True # whether gravity applies to the sprite terminal_velocity = (900.0, 500.0) # maximum horizontal and vertial speeds (pixels / s) bounce_factor = (0.95, 0.95) # bounce factor mass = 1.0 # used for shared collisions and applying forces friction_coeff = (0.99, 0.99) # friction 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 # set to True to have .update() called once per tick (and have .collision_group set) wants_updates = False floor = False # We special case collisions with ground objects block = False def __init__(self, *args, **kwargs): super(Sprite, self).__init__(*args, **kwargs) self.on_solid = False self.velocity = (0.0, 0.0) self.rect = pygame.Rect(0, 0, 10, 10) # sub-classes should override self.collide_rect = pygame.Rect(0, 0, 10, 10) # rectangle we use for collisions self.floor_rect = self.collide_rect self.image = pygame.Surface((10, 10)) self.image.fill((0, 0, 200)) self.collision_group = None self._mask_cache = {} # image id -> collision bit mask def init_pos(self): self._float_pos = cadd(self.rect.center, 0.0) # To make it a float def deltav(self, dv): self.velocity = cadd(self.velocity, dv) def get_vectors(self): self.velocity = cclamp(self.velocity, self.terminal_velocity) return (self.velocity, self._float_pos) def update_position(self, float_pos): self._float_pos = float_pos old_pos = self.rect.center self.rect.center = cint(float_pos) displacement = csub(self.rect.center, old_pos) self.collide_rect.move_ip(displacement) self.floor_rect.move_ip(displacement) def apply_velocity(self, dt): velocity, pos = self.get_vectors() new_pos = cadd(pos, cmul(velocity, dt)) self.update_position(new_pos) def _check_mask(self): mask = self._mask_cache.get(id(self.image)) if mask is None: mask = self._mask_cache[id(self.image)] = from_surface(self.image) self.mask = mask def check_collides(self, other): # check bitmasks for collision self._check_mask() other._check_mask() return pygame.sprite.collide_mask(self, other) def collided(self, other): pass def check_floors(self, floors): """Trigger of the current set of floors""" pass def apply_friction(self): self.velocity = cmul(self.velocity, self.friction_coeff) def bounce(self, other, normal): """Alter velocity after a collision. other: sprite collided with normal: unit vector (tuple) normal to the collision surface. """ bounce_factor = cadd(cmul(self.bounce_factor, other.bounce_factor), 1) deltav = cmul(cneg(normal), cmul(self.velocity, bounce_factor)) if normal == (0, 1) and (other.floor or other.block) and self.velocity[1] > 0 and self.collide_rect.top < other.collide_rect.top: # Colliding with the ground from above is special self.on_solid = True deltav = (deltav[0], -self.velocity[1]) if other.mobile: total_mass = self.mass + other.mass f_self = self.mass / total_mass f_other = other.mass / total_mass self.deltav(cmul(deltav, f_self)) self.deltav(cmul(cneg(deltav), f_other)) else: self.deltav(deltav) # oof def update(self): pass # only called in wants_update = True def check_collide_rect(self, new_collide_rect, new_rect, new_image): if self.collision_group is None: return True # TODO: decide whether to throw out checking of existing # collisions. Doesn't seem needed at the moment and takes # time. old_image = self.image old_rect = self.rect #rect_collides = self.collide_rect.colliderect old_collisions = set() #for other in self.collision_group: # if rect_collides(other.collide_rect) \ # and self.check_collides(other): # old_collisions.add(other) self.image = new_image self.rect = new_rect new_rect_collides = new_collide_rect.colliderect new_collisions = set() for other in self.collision_group: if new_rect_collides(other.collide_rect) \ and self.check_collides(other): new_collisions.add(other) self.image = old_image self.rect = old_rect return not bool(new_collisions - old_collisions) def fix_bounds(self, bounds): self.kill() class World(object): GRAVITY = cmul((0.0, 9.8), 80.0) # pixels / s^2 def __init__(self, bounds): self._all = pygame.sprite.LayeredUpdates() self._mobiles = pygame.sprite.Group() self._gravitators = pygame.sprite.Group() self._updaters = pygame.sprite.Group() self._actionables = pygame.sprite.Group() self._actors = pygame.sprite.Group() self._collision_groups = { None: pygame.sprite.Group() } self._last_time = None self._bounds = bounds def freeze(self): self._last_time = None def thaw(self): self._last_time = time.time() def add(self, sprite): sprite.init_pos() self._all.add(sprite) if sprite.mobile: self._mobiles.add(sprite) if sprite.gravitates: self._gravitators.add(sprite) if sprite.wants_updates: self._updaters.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) if sprite.wants_updates: self._updaters.add(sprite) sprite.collision_group = self._collision_groups[sprite.collision_layer] if getattr(sprite, 'player_action', None) is not None: self._actionables.add(sprite) if getattr(sprite, 'add_actionable', None) is not None: self._actors.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 abs_v_x, abs_v_y = cabs(sprite.velocity) # We only backout of "solide" collisions if sprite.block: for i, other in enumerate(others): if other.block or other.floor: clip = sprite.collide_rect.clip(other.collide_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 if idx is not None: # We can see no solide collisions now sprite.apply_velocity(max(-1.1 * frac, -dt)) sprite.bounce(others[idx], normal) for other in others: sprite.collided(other) def apply_gravity(self, dt): dv = cmul(self.GRAVITY, dt) for sprite in self._gravitators: if not sprite.on_solid: sprite.deltav(dv) def apply_friction(self): for sprite in self._mobiles: sprite.apply_friction() def handle_escaped_sprites(self): inbound = self._bounds.colliderect for sprite in self._mobiles: if not inbound(sprite): sprite.fix_bounds(self._bounds) def get_dt(self): now = time.time() dt = now - self._last_time self._last_time = now return dt # def collide_sprite(self, dt, sprite): # sprite.apply_velocity(dt) # sprite_collides = sprite.collide_rect.colliderect # collisions = [] # for other in self._collision_groups[sprite.collision_layer]: # if sprite_collides(other.collide_rect) \ # and sprite.check_collides(other): # collisions.append(other) # if collisions: # self._backout_collisions(sprite, collisions, dt) # contact_rect = pygame.Rect( # (sprite.collide_rect.left, sprite.collide_rect.bottom), # (sprite.collide_rect.width, 1)) # return contact_rect.colliderect def get_sprite_collisions(self, dt, sprite): sprite.apply_velocity(dt) sprite_collides = sprite.collide_rect.colliderect collisions = [] for other in self._collision_groups[sprite.collision_layer]: if (sprite_collides(other.collide_rect) and sprite.check_collides(other)): collisions.append(other) return collisions def path_collide(self, dt, sprite): dts = [dt/10] * 9 dts.append(dt - sum(dts)) dtf_acc = 0 collisions = [] for dtf in dts: dtf_acc += dtf collisions = self.get_sprite_collisions(dtf, sprite) for col in collisions: if sprite.block and (col.floor or col.block): return collisions, dtf_acc return collisions, dt def collide_sprite(self, dt, sprite): initial_pos = sprite._float_pos collisions = self.get_sprite_collisions(dt, sprite) escape_vector = (0, 0) if collisions: # If we've collided, reset and try again with smaller time increments sprite.update_position(initial_pos) collisions, dtf = self.path_collide(dt, sprite) for col in collisions: if sprite.block and (col.floor or col.block): escape_vector = cabsmax(escape_vector, rect_projection(sprite.collide_rect, col.collide_rect)) sprite.collided(col) sprite.update_position(cadd(sprite._float_pos, escape_vector)) # if escape_vector[0] != 0: # sprite.velocity = (0, sprite.velocity[1]) # if escape_vector[1] != 0: # sprite.velocity = (sprite.velocity[0], 1) # self._backout_collisions(sprite, collisions, dtf) contact_rect = pygame.Rect( cadd(sprite.collide_rect.bottomleft, (1, 0)), (sprite.collide_rect.width, 1)) return contact_rect.colliderect def update_sprite_positions(self, dt): # position update and collision check (do last) for sprite in self._mobiles: collides = self.collide_sprite(dt, sprite) # Are we currently in contact with the ground? if not sprite.block: continue floors = [] sprite.on_solid = False for other in self._collision_groups[sprite.collision_layer]: if (other.floor or other.block) and collides(other.floor_rect): sprite.on_solid = True if sprite.velocity[1] > 0: sprite.velocity = (sprite.velocity[0], 0) floors.append(other) sprite.check_floors(floors) def update(self): if self._last_time is None: self._last_time = time.time() return dt = self.get_dt() self.apply_gravity(dt) self.apply_friction() self.handle_escaped_sprites() self.update_sprite_positions(dt) self._updaters.update() self.handle_actions() def handle_actions(self): for sprite in self._actors: actor_collide_rect = sprite.collide_rect.inflate((4, 4)) for other in self._actionables: other_actor_collide_rect = other.collide_rect.inflate((4, 4)) if actor_collide_rect.colliderect(other_actor_collide_rect): sprite.add_actionable(other) def draw(self, surface): self._all.draw(surface) if options['debug_rects']: for sprite in self._all: sprite.draw_debug(surface)