view skaapsteker/physics.py @ 134:4713a2a3b0be

Initial cutscene screen
author Stefano Rivera <stefano@rivera.za.net>
date Tue, 05 Apr 2011 00:06:07 +0200
parents 36267deaccd8
children 6b488e1351a5
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 . 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 = (300.0, 300.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
    wants_updates = False

    debug_color = (240, 0, 0)

    def __init__(self, *args, **kwargs):
        super(Sprite, self).__init__(*args, **kwargs)
        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.image = pygame.Surface((10, 10))
        self.image.fill((0, 0, 200))

    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, (240, 0, 0), self.collide_rect, 1)
        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)

    def check_collides(self, other):
        return True # default to relying purefly on collision_layer and collides_with

    def collided(self, other):
        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 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


class World(object):

    GRAVITY = 0.0, 9.8 * 20.0 # pixels / s^2

    def __init__(self):
        self._all = pygame.sprite.LayeredUpdates()
        self._mobiles = pygame.sprite.Group()
        self._gravitators = pygame.sprite.Group()
        self._updaters = pygame.sprite.Group()
        self._collision_groups = { None: pygame.sprite.Group() }
        self._last_time = None

    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)

    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 = -1.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.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

        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:
            sprite.deltav(dv)

        # friction
        for sprite in self._mobiles:
            sprite.apply_friction()

        # 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) and sprite.check_collides(other):
                    collisions.append(other)
            if collisions:
                self._backout_collisions(sprite, collisions, dt)

        # call update methods
        self._updaters.update()

    def draw(self, surface):
        self._all.draw(surface)
        if options['debug_rects']:
            for sprite in self._all:
                sprite.draw_debug(surface)