Grease logo

Source code for grease.collision

#############################################################################
#
# Copyright (c) 2010 by Casey Duncan and contributors
# All Rights Reserved.
#
# This software is subject to the provisions of the MIT License
# A copy of the license should accompany this distribution.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#
#############################################################################
"""
**Grease collision detection systems**

Grease uses two-phase broad and narrow collision detection. *Broad-phase*
collision systems are used to efficiently identify pairs that may be colliding
without resorting to a brute-force check of all possible pairs. *Narrow-phase*
collision systems use the pairs generated by the broad-phase and perform more
precise collision tests to determine if a collision has actually occurred. The
narrow-phase system also calculates more details about each collision,
including collision point and normal vector for use in collision response.

A typical collision detection system consists of a narrow-phase system that
contains a broad-phased system. The narrow-phase system is usually the only

one that the application directly interacts with, though the application is
free to use the broad-phased system directly if desired. This could be
useful in cases where speed, rather than precision is paramount.

The narrow-phase system can be assigned handler objects to run after
collision detection. These can perform tasks like handling collision response
or dispatching collision events to application handlers.

Note that broad-phase systems can return false positives, though they should
never return false negatives. Do not assume that all pairs returned by a
broad-phase system are actually in collision.
"""

__version__ = '$Id$'

from grease.geometry import Vec2d
from bisect import bisect_right


[docs]class Pair(tuple): """Pair of entities in collision. This is an ordered sequence of two entities, that compares and hashes unordered. Also stores additional collision point and normal vectors for each entity. Sets of ``Pair`` objects are exposed in the ``collision_pairs`` attribute of collision systems to indicate the entity pairs in collision. """ info = None """A sequence of (entity, collision point, collision normal) for each entity in the pair """ def __new__(cls, entity1, entity2, point=None, normal=None): pair = tuple.__new__(cls, (entity1, entity2)) return pair def __hash__(self): return hash(self[0]) ^ hash(self[1]) def __eq__(self, other): other = tuple(other) return tuple(self) == other or (self[1], self[0]) == other def __repr__(self): return '%s%r' % (self.__class__.__name__, tuple(self))
[docs] def set_point_normal(self, point0, normal0, point1, normal1): """Set the collision point and normal for both entities""" self.info = ( (self[0], point0, normal0), (self[1], point1, normal1), )
[docs]class BroadSweepAndPrune(object): """2D Broad-phase sweep and prune bounding box collision detector This algorithm is efficient for collision detection between many moving bodies. It has linear algorithmic complexity and takes advantage of temporal coherence between frames. It also does not suffer from bad worst-case performance (like RDC can). Unlike spacial hashing, it does not need to be optimized for specific space and body sizes. Other algorithms may be more efficient for collision detection with stationary bodies, bodies that are always evenly distributed, or ad-hoc queries. :param collision_component: Name of the collision component used by this system, defaults to 'collision'. This component supplies each entities' aabb and collision masks. :type collision_component: str """ world = None """|World| object this system belongs to""" collision_component = None """Name of world's collision component used by this system""" LEFT_ATTR = "left" RIGHT_ATTR = "right" TOP_ATTR = "top" BOTTOM_ATTR = "bottom" def __init__(self, collision_component='collision'): self.collision_component = collision_component self._by_x = None self._by_y = None self._collision_pairs = None
[docs] def set_world(self, world): """Bind the system to a world""" self.world = world
[docs] def step(self, dt): """Update the system for this time step, updates and sorts the axis arrays. """ component = getattr(self.world.components, self.collision_component) LEFT = self.LEFT_ATTR RIGHT = self.RIGHT_ATTR TOP = self.TOP_ATTR BOTTOM = self.BOTTOM_ATTR if self._by_x is None: # Build axis lists from scratch # Note we cache the box positions here # so that we can perform hit tests efficiently # it also isolates us from changes made to the # box positions after we run by_x = self._by_x = [] append_x = by_x.append by_y = self._by_y = [] append_y = by_y.append for data in component.itervalues(): append_x([data.aabb.left, LEFT, data]) append_x([data.aabb.right, RIGHT, data]) append_y([data.aabb.bottom, BOTTOM, data]) append_y([data.aabb.top, TOP, data]) else: by_x = self._by_x by_y = self._by_y removed = [] for entry in by_x: entry[0] = getattr(entry[2].aabb, entry[1]) for entry in by_y: entry[0] = getattr(entry[2].aabb, entry[1]) # Removing entities is inefficient, but expected to be rare if component.deleted_entities: deleted_entities = component.deleted_entities deleted_x = [] deleted_y = [] for i, (_, _, data) in enumerate(by_x): if data.entity in deleted_entities: deleted_x.append(i) deleted_x.reverse() for i in deleted_x: del by_x[i] for i, (_, _, data) in enumerate(by_y): if data.entity in deleted_entities: deleted_y.append(i) deleted_y.reverse() for i in deleted_y: del by_y[i] # Tack on new entities for entity in component.new_entities: data = component[entity] by_x.append([data.aabb.left, LEFT, data]) by_x.append([data.aabb.right, RIGHT, data]) by_y.append([data.aabb.bottom, BOTTOM, data]) by_y.append([data.aabb.top, TOP, data]) # Tim-sort is highly efficient with mostly sorted lists. # Because positions tend to change little each frame # we take advantage of this here. Obviously things are # less efficient with very fast moving, or teleporting entities by_x.sort() by_y.sort() self._collision_pairs = None
@property
[docs] def collision_pairs(self): """Set of candidate collision pairs for this timestep""" if self._collision_pairs is None: if self._by_x is None: # Axis arrays not ready return set() LEFT = self.LEFT_ATTR RIGHT = self.RIGHT_ATTR TOP = self.TOP_ATTR BOTTOM = self.BOTTOM_ATTR # Build candidates overlapping along the x-axis component = getattr(self.world.components, self.collision_component) xoverlaps = set() add_xoverlap = xoverlaps.add discard_xoverlap = xoverlaps.discard open = {} for _, side, data in self._by_x: if side is LEFT: for open_entity, (from_mask, into_mask) in open.iteritems(): if data.from_mask & into_mask or from_mask & data.into_mask: add_xoverlap(Pair(data.entity, open_entity)) open[data.entity] = (data.from_mask, data.into_mask) elif side is RIGHT: del open[data.entity] if len(xoverlaps) <= 10 and len(xoverlaps)*4 < len(self._by_y): # few candidates were found, so just scan the x overlap candidates # along y. This requires an additional sort, but it should # be cheaper than scanning everyone and its simpler # than a separate brute-force check entities = set([entity for entity, _ in xoverlaps] + [entity for _, entity in xoverlaps]) by_y = [] for entity in entities: data = component[entity] # We can use tuples here, which are cheaper to create by_y.append((data.aabb.bottom, BOTTOM, data)) by_y.append((data.aabb.top, TOP, data)) by_y.sort() else: by_y = self._by_y # Now check the candidates along the y-axis open = set() add_open = open.add discard_open = open.discard self._collision_pairs = set() add_pair = self._collision_pairs.add for _, side, data in by_y: if side is BOTTOM: for open_entity in open: pair = Pair(data.entity, open_entity) if pair in xoverlaps: discard_xoverlap(pair) add_pair(pair) if not xoverlaps: # No more candidates, bail return self._collision_pairs add_open(data.entity) elif side is TOP: discard_open(data.entity) return self._collision_pairs
[docs] def query_point(self, x_or_point, y=None, from_mask=0xffffffff): """Hit test at the point specified. :param x_or_point: x coordinate (float) or sequence of (x, y) floats. :param y: y coordinate (float) if x is not a sequence :param from_mask: Bit mask used to filter query results. This value is bit ANDed with candidate entities' ``collision.into_mask``. If the result is non-zero, then it is considered a hit. By default all entities colliding with the input point are returned. :return: A set of entities where the point is inside their bounding boxes as of the last time step. """ if self._by_x is None: # Axis arrays not ready return set() if y is None: x, y = x_or_point else: x = x_or_point LEFT = self.LEFT_ATTR RIGHT = self.RIGHT_ATTR TOP = self.TOP_ATTR BOTTOM = self.BOTTOM_ATTR x_index = bisect_right(self._by_x, [x]) x_hits = set() add_x_hit = x_hits.add discard_x_hit = x_hits.discard if x_index <= len(self._by_x) // 2: # closer to the left, scan from left to right while (x == self._by_x[x_index][0] and self._by_x[x_index][1] is LEFT and x_index < len(self._by_x)): # Ensure we hit on exact left edge matches x_index += 1 for _, side, data in self._by_x[:x_index]: if side is LEFT and from_mask & data.into_mask: add_x_hit(data.entity) else: discard_x_hit(data.entity) else: # closer to the right for _, side, data in reversed(self._by_x[x_index:]): if side is RIGHT and from_mask & data.into_mask: add_x_hit(data.entity) else: discard_x_hit(data.entity) if not x_hits: return x_hits y_index = bisect_right(self._by_y, [y]) y_hits = set() add_y_hit = y_hits.add discard_y_hit = y_hits.discard if y_index <= len(self._by_y) // 2: # closer to the bottom while (y == self._by_y[y_index][0] and self._by_y[y_index][1] is BOTTOM and y_index < len(self._by_y)): # Ensure we hit on exact bottom edge matches y_index += 1 for _, side, data in self._by_y[:y_index]: if side is BOTTOM: add_y_hit(data.entity) else: discard_y_hit(data.entity) else: # closer to the top for _, side, data in reversed(self._by_y[y_index:]): if side is TOP: add_y_hit(data.entity) else: discard_y_hit(data.entity) if y_hits: return x_hits & y_hits else: return y_hits
[docs]class Circular(object): """Basic narrow-phase collision detector which treats all entities as circles with their radius defined in the collision component. :param handlers: A sequence of collision handler functions that are invoked after collision detection. :type handlers: sequence of functions :param collision_component: Name of collision component for this system, defaults to 'collision'. This supplies each entity's collision radius and masks. :type collision_component: str :param position_component: Name of position component for this system, defaults to 'position'. This supplies each entity's position. :type position_component: str :param update_aabbs: If True (the default), then the entities' `collision.aabb` fields will be updated using their position and collision radius before invoking the broad phase system. Set this False if another system updates the aabbs. :type update_aabbs: bool :param broad_phase: A broad-phase collision system to use as a source for collision pairs. If not specified, a :class:`BroadSweepAndPrune` system will be created automatically. """ world = None """|World| object this system belongs to""" position_component = None """Name of world's position component used by this system""" collision_component = None """Name of world's collision component used by this system""" update_aabbs = True """Flag to indicate whether the system updates the entities' `collision.aabb` field before invoking the broad phase collision system """ handlers = None """A sequence of collision handler functions invoke after collision detection """ broad_phase = None """Broad phase collision system used as a source for collision pairs""" def __init__(self, handlers=(), position_component='position', collision_component='collision', update_aabbs=True, broad_phase=None): self.handlers = tuple(handlers) if broad_phase is None: broad_phase = BroadSweepAndPrune(collision_component) self.collision_component = collision_component self.position_component = position_component self.update_aabbs = bool(update_aabbs) self.broad_phase = broad_phase self._collision_pairs = None
[docs] def set_world(self, world): """Bind the system to a world""" self.world = world self.broad_phase.set_world(world) for handler in self.handlers: if hasattr(handler, 'set_world'): handler.set_world(world)
[docs] def step(self, dt): """Update the collision system for this time step and invoke the handlers """ if self.update_aabbs: for position, collision in self.world.components.join( self.position_component, self.collision_component): aabb = collision.aabb x, y = position.position radius = collision.radius aabb.left = x - radius aabb.right = x + radius aabb.bottom = y - radius aabb.top = y + radius self.broad_phase.step(dt) self._collision_pairs = None for handler in self.handlers: handler(self)
@property
[docs] def collision_pairs(self): """The set of entity pairs in collision in this timestep""" if self._collision_pairs is None: position = getattr(self.world.components, self.position_component) collision = getattr(self.world.components, self.collision_component) pairs = self._collision_pairs = set() for pair in self.broad_phase.collision_pairs: entity1, entity2 = pair position1 = position[entity1].position position2 = position[entity2].position radius1 = collision[entity1].radius radius2 = collision[entity2].radius separation = position2 - position1 if separation.get_length_sqrd() <= (radius1 + radius2)**2: normal = separation.normalized() pair.set_point_normal( normal * radius1 + position1, normal, normal * -radius2 + position2, -normal) pairs.add(pair) return self._collision_pairs
[docs] def query_point(self, x_or_point, y=None, from_mask=0xffffffff): """Hit test at the point specified. :param x_or_point: x coordinate (float) or sequence of (x, y) floats. :param y: y coordinate (float) if x is not a sequence :param from_mask: Bit mask used to filter query results. This value is bit ANDed with candidate entities' ``collision.into_mask``. If the result is non-zero, then it is considered a hit. By default all entities colliding with the input point are returned. :return: A set of entities where the point is inside their collision radii as of the last time step. """ if y is None: point = Vec2d(x_or_point) else: point = Vec2d(x_or_point, y) hits = set() position = getattr(self.world.components, self.position_component) collision = getattr(self.world.components, self.collision_component) for entity in self.broad_phase.query_point(x_or_point, y, from_mask): separation = point - position[entity].position if separation.get_length_sqrd() <= collision[entity].radius**2: hits.add(entity) return hits
[docs]def dispatch_events(collision_system): """Collision handler that dispatches `on_collide()` events to entities marked for collision by the specified collision system. The `on_collide()` event handler methods are defined by the application on the desired entity classes. These methods should have the following signature:: def on_collide(self, other_entity, collision_point, collision_normal): '''Handle A collision between this entity and `other_entity` - other_entity (Entity): The other entity in collision with `self` - collision_point (Vec2d): The point on this entity (`self`) where the collision occurred. Note this may be `None` for some collision systems that do not report it. - collision_normal (Vec2d): The normal vector at the point of collision. As with `collision_point`, this may be None for some collision systems. ''' Note the arguments to `on_collide()` are always passed positionally, so you can use different argument names than above if desired. If a pair of entities are in collision, then the event will be dispatched to both objects in arbitrary order if all of their collision masks align. """ collision = getattr(collision_system.world.components, collision_system.collision_component) for pair in collision_system.collision_pairs: entity1, entity2 = pair if pair.info is not None: args1, args2 = pair.info else: args1 = entity1, None, None args2 = entity2, None, None try: on_collide = entity1.on_collide masks_align = collision[entity2].from_mask & collision[entity1].into_mask except (AttributeError, KeyError): pass else: if masks_align: on_collide(*args2) try: on_collide = entity2.on_collide masks_align = collision[entity1].from_mask & collision[entity2].into_mask except (AttributeError, KeyError): pass else: if masks_align: on_collide(*args1)