Mercurial > skaapsteker
view skaapsteker/physics.py @ 622:da331c80ec08
Clean up sprite inheritance hierarchy a bit.
author | Jeremy Thurgood <firxen@gmail.com> |
---|---|
date | Sat, 07 May 2011 13:42:27 +0200 |
parents | 4ffa9d159588 |
children | 65881746dc20 |
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, cint, cneg, cabs 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 debug_color = (240, 0, 0) 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 = self.rect.center def get_debug_color(self): return self.debug_color def draw_debug(self, surface): pygame.draw.rect(surface, self.get_debug_color(), self.collide_rect, 1) def deltav(self, dv): velocity = cadd(self.velocity, dv) self.velocity = cclamp(velocity, self.terminal_velocity) def deltap(self, dt): old_pos = self.rect.center self._float_pos = cadd(self._float_pos, cmul(self.velocity, dt)) self.rect.center = cint(self._float_pos) delta_pos = csub(self.rect.center, old_pos) self.collide_rect.move_ip(delta_pos) self.floor_rect.move_ip(delta_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.deltap(max(-1.1 * frac, -dt)) sprite.bounce(others[idx], normal) for other in others: sprite.collided(other) def do_gravity(self, dt): dv = cmul(self.GRAVITY, dt) for sprite in self._gravitators: if sprite.on_solid: sprite.deltav((dv[0], 0.0)) else: sprite.deltav(dv) def update(self): if self._last_time is None: self._last_time = time.time() return # find dt now = time.time() self._last_time, dt = now, now - self._last_time self.do_gravity(dt) # friction for sprite in self._mobiles: sprite.apply_friction() # kill sprites outside the world inbound = self._bounds.colliderect for sprite in self._mobiles: if not inbound(sprite): sprite.fix_bounds(self._bounds) # position update and collision check (do last) for sprite in self._mobiles: sprite.deltap(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)) collides = contact_rect.colliderect floors = [] if sprite.on_solid: # Check if we are still in contact with the ground still_on_solid = False for other in self._collision_groups[sprite.collision_layer]: if (other.floor or other.block) and collides(other.floor_rect): still_on_solid = True floors.append(other) sprite.on_solid = still_on_solid else: # Are we currently in contact with the ground for other in self._collision_groups[sprite.collision_layer]: if (other.floor or other.block) and collides(other.floor_rect): sprite.on_solid = True floors.append(other) sprite.check_floors(floors) # call update methods self._updaters.update() # Action stuff. # Happens after updates, because we only want it for the next frame. 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)