view skaapsteker/physics.py @ 619:4ffa9d159588

Some coordinate operators, to reduce foo_x, foo_y everywhere.
author Jeremy Thurgood <firxen@gmail.com>
date Fri, 06 May 2011 16:37:43 +0200
parents 62569f486ede
children da331c80ec08
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.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 deltav(self, dv):
        velocity = cadd(self.velocity, dv)
        self.velocity = cclamp(velocity, self.terminal_velocity)


    def deltap(self, dt):
        old_pos = self.rect.topleft
        self._float_pos = cadd(self._float_pos, cmul(self.velocity, dt))
        self.rect.topleft = cint(self._float_pos)
        delta_pos = csub(self.rect.topleft, 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)