Mercurial > skaapsteker
view skaapsteker/physics.py @ 460:8b9b4706a4d6
Blocking NPC's block
author | Neil Muller <drnlmuller@gmail.com> |
---|---|
date | Sat, 09 Apr 2011 20:09:58 +0200 |
parents | 7c0643e51f33 |
children | 62569f486ede |
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 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.topleft 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 deltaf(self, df): dv = df[0] / self.mass, df[1] / self.mass self.deltav(dv) def deltav(self, dv): v_x, v_y = self.velocity v_x, v_y = v_x + dv[0], v_y + dv[1] t_v = self.terminal_velocity v_x = max(min(v_x, t_v[0]), -t_v[0]) v_y = max(min(v_y, t_v[1]), -t_v[1]) self.velocity = (v_x, v_y) def deltap(self, dt): old_pos = self.rect.topleft v_x, v_y = self.velocity f_x, f_y = self._float_pos d_x, d_y = v_x * dt, v_y * dt f_x, f_y = f_x + d_x, f_y + d_y self._float_pos = f_x, f_y self.rect.topleft = int(f_x), int(f_y) delta_pos = self.rect.left - old_pos[0], self.rect.top - old_pos[1] 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): v_x, v_y = self.velocity self.velocity = self.friction_coeff[0] * v_x, self.friction_coeff[1] * v_y def bounce(self, other, normal): """Alter velocity after a collision. 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] dv_x = - normal[0] * b_x * v_x dv_y = - normal[1] * b_y * v_y if normal == (0, 1) and (other.floor or other.block) and v_y > 0 and self.collide_rect.top < other.collide_rect.top: # Colliding with the ground from above is special self.on_solid = True dv_y = -v_y if other.mobile: total_mass = self.mass + other.mass f_self = self.mass / total_mass f_other = other.mass / total_mass self.deltav((dv_x * f_self, dv_y * f_self)) other.deltav((- dv_x * f_other, - dv_y * f_other)) else: self.deltav((dv_x, dv_y)) # 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): print "Killing", self, self.rect, self._float_pos, bounds self.kill() class World(object): GRAVITY = 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 v_x, v_y = sprite.velocity abs_v_x, abs_v_y = abs(v_x), abs(v_y) # 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 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 # gravity dv = self.GRAVITY[0] * dt, self.GRAVITY[1] * dt for sprite in self._gravitators: if sprite.on_solid: sprite.deltav((dv[0], 0.0)) else: sprite.deltav(dv) # 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)