collision
View Source
import math from collections import namedtuple from renderable import TileRenderable from renderable_line import CircleCollisionRenderable, BoxCollisionRenderable, TileBoxCollisionRenderable # collision shape types CST_NONE = 0 "Don't use a CollisionShape" CST_CIRCLE = 1 "Use a CircleCollisionShap" CST_AABB = 2 "Use an AABBCollisionShape" CST_TILE = 3 """ Tile-based collision: generate multiple AABBCollisionShapes to approximate all non-blank (character index 0) tiles of our GameObject's default Art's "collision layer", whose string name is defined in GO.col_layer_name. """ # collision types CT_NONE = 0 CT_PLAYER = 1 CT_GENERIC_STATIC = 2 CT_GENERIC_DYNAMIC = 3 # collision type groups, eg static and dynamic CTG_STATIC = [CT_GENERIC_STATIC] '"Collision type group", collections of CT_* values for more convenient checks.' CTG_DYNAMIC = [CT_GENERIC_DYNAMIC, CT_PLAYER] '"Collision type group", collections of CT_* values for more convenient checks.' __pdoc__ = {} # named tuples for collision structs that don't merit a class Contact = namedtuple('Contact', ['overlap', 'timestamp']) __pdoc__['Contact'] = "Represents a contact between two objects." ShapeOverlap = namedtuple('ShapeOverlap', ['x', 'y', 'dist', 'area', 'other']) __pdoc__['ShapeOverlap'] = "Represents a CollisionShape's overlap with another." class CollisionShape: """ Abstract class for a shape that can overlap and collide with other shapes. Shapes are part of a Collideable which in turn is part of a GameObject. """ def resolve_overlaps_with_shapes(self, shapes): "Resolve this shape's overlap(s) with given list of shapes." overlaps = [] for other in shapes: if other is self: continue overlap = self.get_overlap(other) if overlap.dist < 0: overlaps.append(overlap) if len(overlaps) == 0: return # resolve collisions in order of largest -> smallest overlap overlaps.sort(key=lambda item: item.area, reverse=True) for i,old_overlap in enumerate(overlaps): # resolve first overlap without recalculating overlap = self.get_overlap(old_overlap.other) if i > 0 else overlaps[0] self.resolve_overlap(overlap) def resolve_overlap(self, overlap): "Resolve this shape's given overlap." other = overlap.other # tell objects they're overlapping, pass penetration vector a_coll_b, a_started_b = self.go.overlapped(other.go, overlap) b_coll_a, b_started_a = other.go.overlapped(self.go, overlap) # if either object says it shouldn't collide with other, don't if not a_coll_b or not b_coll_a: return # push shapes apart according to mass total_mass = max(0, self.go.mass) + max(0, other.go.mass) if self.go.is_dynamic(): if not other.go.is_dynamic() or other.go.mass < 0: a_push = overlap.dist else: a_push = (self.go.mass / total_mass) * overlap.dist # move parent object, not shape self.go.x += a_push * overlap.x self.go.y += a_push * overlap.y # update all shapes based on object's new position self.go.collision.update_transform_from_object() if other.go.is_dynamic(): if not self.go.is_dynamic() or self.go.mass < 0: b_push = overlap.dist else: b_push = (other.go.mass / total_mass) * overlap.dist other.go.x -= b_push * overlap.x other.go.y -= b_push * overlap.y other.go.collision.update_transform_from_object() # call objs' started_colliding once collisions have been resolved world = self.go.world if a_started_b: world.try_object_method(self.go, self.go.started_colliding, [other.go]) if b_started_a: world.try_object_method(other.go, other.go.started_colliding, [self.go]) def get_overlapping_static_shapes(self): "Return a list of static shapes that overlap with this shape." overlapping_shapes = [] shape_left, shape_top, shape_right, shape_bottom = self.get_box() # add padding to overlapping tiles check if False: padding = 0.01 shape_left -= padding shape_top -= padding shape_right += padding shape_bottom += padding for obj in self.go.world.objects.values(): if obj is self.go or not obj.should_collide() or obj.is_dynamic(): continue # always check non-tile-based static shapes if obj.collision_shape_type != CST_TILE: overlapping_shapes += obj.collision.shapes else: # skip if even bounds don't overlap obj_left, obj_top, obj_right, obj_bottom = obj.get_edges() if not boxes_overlap(shape_left, shape_top, shape_right, shape_bottom, obj_left, obj_top, obj_right, obj_bottom): continue overlapping_shapes += obj.collision.get_shapes_overlapping_box(shape_left, shape_top, shape_right, shape_bottom) return overlapping_shapes class CircleCollisionShape(CollisionShape): "CollisionShape using a circle area." def __init__(self, loc_x, loc_y, radius, game_object): self.x, self.y = loc_x, loc_y self.radius = radius self.go = game_object def get_box(self): "Return world coordinates of our bounds (left, top, right, bottom)" return self.x - self.radius, self.y - self.radius, self.x + self.radius, self.y + self.radius def is_point_inside(self, x, y): "Return True if given point is inside this shape." return (self.x - x) ** 2 + (self.y - y) ** 2 <= self.radius ** 2 def overlaps_line(self, x1, y1, x2, y2): "Return True if this circle overlaps given line segment." return circle_overlaps_line(self.x, self.y, self.radius, x1, y1, x2, y2) def get_overlap(self, other): "Return ShapeOverlap data for this shape's overlap with given other." if type(other) is CircleCollisionShape: px, py, pdist1, pdist2 = point_circle_penetration(self.x, self.y, other.x, other.y, self.radius + other.radius) elif type(other) is AABBCollisionShape: px, py, pdist1, pdist2 = circle_box_penetration(self.x, self.y, other.x, other.y, self.radius, other.halfwidth, other.halfheight) area = abs(pdist1 * pdist2) if pdist1 < 0 else 0 return ShapeOverlap(x=px, y=py, dist=pdist1, area=area, other=other) class AABBCollisionShape(CollisionShape): "CollisionShape using an axis-aligned bounding box area." def __init__(self, loc_x, loc_y, halfwidth, halfheight, game_object): self.x, self.y = loc_x, loc_y self.halfwidth, self.halfheight = halfwidth, halfheight self.go = game_object # for CST_TILE objects, lists of tile(s) we cover self.tiles = [] def get_box(self): return self.x - self.halfwidth, self.y - self.halfheight, self.x + self.halfwidth, self.y + self.halfheight def is_point_inside(self, x, y): "Return True if given point is inside this shape." return point_in_box(x, y, *self.get_box()) def overlaps_line(self, x1, y1, x2, y2): "Return True if this box overlaps given line segment." left, top, right, bottom = self.get_box() return box_overlaps_line(left, top, right, bottom, x1, y1, x2, y2) def get_overlap(self, other): "Return ShapeOverlap data for this shape's overlap with given other." if type(other) is AABBCollisionShape: px, py, pdist1, pdist2 = box_penetration(self.x, self.y, other.x, other.y, self.halfwidth, self.halfheight, other.halfwidth, other.halfheight) elif type(other) is CircleCollisionShape: px, py, pdist1, pdist2 = circle_box_penetration(other.x, other.y, self.x, self.y, other.radius, self.halfwidth, self.halfheight) # reverse result if we're shape B px, py = -px, -py area = abs(pdist1 * pdist2) if pdist1 < 0 else 0 return ShapeOverlap(x=px, y=py, dist=pdist1, area=area, other=other) class Collideable: "Collision component for GameObjects. Contains a list of shapes." use_art_offset = False "use game object's art_off_pct values" def __init__(self, obj): "Create new Collideable for given GameObject." self.go = obj self.cl = self.go.world.cl self.renderables, self.shapes = [], [] self.tile_shapes = {} "Dict of shapes accessible by (x,y) tile coordinates" self.contacts = {} "Dict of contacts with other objects, by object name" self.create_shapes() def create_shapes(self): """ Create collision shape(s) appropriate to our game object's collision_shape_type value. """ self._clear_shapes() if self.go.collision_shape_type == CST_NONE: return elif self.go.collision_shape_type == CST_CIRCLE: self._create_circle() elif self.go.collision_shape_type == CST_AABB: self._create_box() elif self.go.collision_shape_type == CST_TILE: self.tile_shapes.clear() self._create_merged_tile_boxes() # update renderables once if static if not self.go.is_dynamic(): self.update_renderables() def _clear_shapes(self): for r in self.renderables: r.destroy() self.renderables = [] for shape in self.shapes: self.cl._remove_shape(shape) self.shapes = [] "List of CollisionShapes" def _create_circle(self): x = self.go.x + self.go.col_offset_x y = self.go.y + self.go.col_offset_y shape = self.cl._add_circle_shape(x, y, self.go.col_radius, self.go) self.shapes = [shape] self.renderables = [CircleCollisionRenderable(shape)] def _create_box(self): x = self.go.x # + self.go.col_offset_x y = self.go.y # + self.go.col_offset_y shape = self.cl._add_box_shape(x, y, self.go.col_width / 2, self.go.col_height / 2, self.go) self.shapes = [shape] self.renderables = [BoxCollisionRenderable(shape)] def _create_merged_tile_boxes(self): "Create AABB shapes for a CST_TILE object" # generate fewer, larger boxes! frame = self.go.renderable.frame if not self.go.col_layer_name in self.go.art.layer_names: self.go.app.dev_log("%s: Couldn't find collision layer with name '%s'" % (self.go.name, self.go.col_layer_name)) return layer = self.go.art.layer_names.index(self.go.col_layer_name) # tile is available if it's not empty and not already covered by a shape def tile_available(tile_x, tile_y): return self.go.art.get_char_index_at(frame, layer, tile_x, tile_y) != 0 and not (tile_x, tile_y) in self.tile_shapes def tile_range_available(start_x, end_x, start_y, end_y): for y in range(start_y, end_y + 1): for x in range(start_x, end_x + 1): if not tile_available(x, y): return False return True for y in range(self.go.art.height): for x in range(self.go.art.width): if not tile_available(x, y): continue # determine how big we can make this box # first fill left to right end_x = x while end_x < self.go.art.width - 1 and tile_available(end_x + 1, y): end_x += 1 # then fill top to bottom end_y = y while end_y < self.go.art.height - 1 and tile_range_available(x, end_x, y, end_y + 1): end_y += 1 # compute origin and halfsizes of box covering tile range wx1, wy1 = self.go.get_tile_loc(x, y, tile_center=True) wx2, wy2 = self.go.get_tile_loc(end_x, end_y, tile_center=True) wx = (wx1 + wx2) / 2 halfwidth = (end_x - x) * self.go.art.quad_width halfwidth /= 2 halfwidth += self.go.art.quad_width / 2 wy = (wy1 + wy2) / 2 halfheight = (end_y - y) * self.go.art.quad_height halfheight /= 2 halfheight += self.go.art.quad_height / 2 shape = self.cl._add_box_shape(wx, wy, halfwidth, halfheight, self.go) # fill in cell(s) in our tile collision dict, # write list of tiles shape covers to shape.tiles for tile_y in range(y, end_y + 1): for tile_x in range(x, end_x + 1): self.tile_shapes[(tile_x, tile_y)] = shape shape.tiles.append((tile_x, tile_y)) r = TileBoxCollisionRenderable(shape) # update renderable once to set location correctly r.update() self.shapes.append(shape) self.renderables.append(r) def get_shape_overlapping_point(self, x, y): "Return shape if it's overlapping given point, None if no overlap." tile_x, tile_y = self.go.get_tile_at_point(x, y) return self.tile_shapes.get((tile_x, tile_y), None) def get_shapes_overlapping_box(self, left, top, right, bottom): "Return a list of our shapes that overlap given box." shapes = [] tiles = self.go.get_tiles_overlapping_box(left, top, right, bottom) for (x, y) in tiles: shape = self.tile_shapes.get((x, y), None) if shape and not shape in shapes: shapes.append(shape) return shapes def update(self): if self.go and self.go.is_dynamic(): self.update_transform_from_object() def update_transform_from_object(self, obj=None): "Snap our shapes to location of given object (if unspecified, our GO)." obj = obj or self.go # CST_TILE shouldn't run here, it's static-only if obj.collision_shape_type == CST_TILE: return for shape in self.shapes: shape.x = obj.x + obj.col_offset_x shape.y = obj.y + obj.col_offset_y def set_shape_color(self, shape, new_color): "Set the color of a given shape's debug LineRenderable." try: shape_index = self.shapes.index(shape) except ValueError: return self.renderables[shape_index].color = new_color self.renderables[shape_index].build_geo() self.renderables[shape_index].rebind_buffers() def update_renderables(self): for r in self.renderables: r.update() def render(self): for r in self.renderables: r.render() def destroy(self): for r in self.renderables: r.destroy() # remove our shapes from CollisionLord's shape list for shape in self.shapes: self.cl._remove_shape(shape) class CollisionLord: """ Collision manager object, tracks Collideables, detects overlaps and resolves collisions. """ iterations = 7 """ Number of times to resolve collisions per update. Lower at own risk; multi-object collisions require multiple iterations to settle correctly. """ def __init__(self, world): self.world = world self.ticks = 0 # list of objects processed for collision this frame self.collisions_this_frame = [] self.reset() def report(self): print('%s: %s dynamic shapes, %s static shapes' % (self, len(self.dynamic_shapes), len(self.static_shapes))) def reset(self): self.dynamic_shapes, self.static_shapes = [], [] def _add_circle_shape(self, x, y, radius, game_object): shape = CircleCollisionShape(x, y, radius, game_object) if game_object.is_dynamic(): self.dynamic_shapes.append(shape) else: self.static_shapes.append(shape) return shape def _add_box_shape(self, x, y, halfwidth, halfheight, game_object): shape = AABBCollisionShape(x, y, halfwidth, halfheight, game_object) if game_object.is_dynamic(): self.dynamic_shapes.append(shape) else: self.static_shapes.append(shape) return shape def _remove_shape(self, shape): if shape in self.dynamic_shapes: self.dynamic_shapes.remove(shape) elif shape in self.static_shapes: self.static_shapes.remove(shape) def update(self): "Resolve overlaps between all relevant world objects." for i in range(self.iterations): # filter shape lists for anything out of room etc valid_dynamic_shapes = [] for shape in self.dynamic_shapes: if shape.go.should_collide(): valid_dynamic_shapes.append(shape) for shape in valid_dynamic_shapes: shape.resolve_overlaps_with_shapes(valid_dynamic_shapes) for shape in valid_dynamic_shapes: static_shapes = shape.get_overlapping_static_shapes() shape.resolve_overlaps_with_shapes(static_shapes) # check which objects stopped colliding for obj in self.world.objects.values(): obj.check_finished_contacts() self.ticks += 1 self.collisions_this_frame = [] # collision handling def point_in_box(x, y, box_left, box_top, box_right, box_bottom): "Return True if given point lies within box with given corners." return box_left <= x <= box_right and box_bottom <= y <= box_top def boxes_overlap(left_a, top_a, right_a, bottom_a, left_b, top_b, right_b, bottom_b): "Return True if given boxes A and B overlap." for (x, y) in ((left_a, top_a), (right_a, top_a), (right_a, bottom_a), (left_a, bottom_a)): if left_b <= x <= right_b and bottom_b <= y <= top_b: return True return False def lines_intersect(x1, y1, x2, y2, x3, y3, x4, y4): "Return True if given lines intersect." denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) numer = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3) numer2 = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3) if denom == 0: if numer == 0 and numer2 == 0: # coincident return False # parallel return False ua = numer / denom ub = numer2 / denom return ua >= 0 and ua <= 1 and ub >= 0 and ub <= 1 def line_point_closest_to_point(point_x, point_y, x1, y1, x2, y2): "Return point on given line that's closest to given point." wx, wy = point_x - x1, point_y - y1 dir_x, dir_y = x2 - x1, y2 - y1 proj = wx * dir_x + wy * dir_y if proj <= 0: # line point 1 is closest return x1, y1 vsq = dir_x ** 2 + dir_y ** 2 if proj >= vsq: # line point 2 is closest return x2, y2 else: # closest point is between 1 and 2 return x1 + (proj / vsq) * dir_x, y1 + (proj / vsq) * dir_y def circle_overlaps_line(circle_x, circle_y, radius, x1, y1, x2, y2): "Return True if given circle overlaps given line." # get closest point on line to circle center closest_x, closest_y = line_point_closest_to_point(circle_x, circle_y, x1, y1, x2, y2) dist_x, dist_y = closest_x - circle_x, closest_y - circle_y return dist_x ** 2 + dist_y ** 2 <= radius ** 2 def box_overlaps_line(left, top, right, bottom, x1, y1, x2, y2): "Return True if given box overlaps given line." # TODO: determine if this is less efficient than slab method below if point_in_box(x1, y1, left, top, right, bottom) and \ point_in_box(x2, y2, left, top, right, bottom): return True # check left/top/right/bottoms edges return lines_intersect(left, top, left, bottom, x1, y1, x2, y2) or \ lines_intersect(left, top, right, top, x1, y1, x2, y2) or \ lines_intersect(right, top, right, bottom, x1, y1, x2, y2) or \ lines_intersect(left, bottom, right, bottom, x1, y1, x2, y2) def box_overlaps_ray(left, top, right, bottom, x1, y1, x2, y2): "Return True if given box overlaps given ray." # TODO: determine if this can be adapted for line segments # (just a matter of setting tmin/tmax properly?) tmin, tmax = -math.inf, math.inf dir_x, dir_y = x2 - x1, y2 - y1 if abs(dir_x) > 0: tx1 = (left - x1) / dir_x tx2 = (right - x1) / dir_x tmin = max(tmin, min(tx1, tx2)) tmax = min(tmax, max(tx1, tx2)) if abs(dir_y) > 0: ty1 = (top - y1) / dir_y ty2 = (bottom - y1) / dir_y tmin = max(tmin, min(ty1, ty2)) tmax = min(tmax, max(ty1, ty2)) return tmax >= tmin def point_circle_penetration(point_x, point_y, circle_x, circle_y, radius): "Return normalized penetration x, y, and distance for given circles." dx, dy = circle_x - point_x, circle_y - point_y pdist = math.sqrt(dx ** 2 + dy ** 2) # point is center of circle, arbitrarily project out in +X if pdist == 0: return 1, 0, -radius, -radius # TODO: calculate other axis of intersection for area? return dx / pdist, dy / pdist, pdist - radius, pdist - radius def box_penetration(ax, ay, bx, by, ahw, ahh, bhw, bhh): "Return penetration vector and magnitude for given boxes." left_a, right_a = ax - ahw, ax + ahw top_a, bottom_a = ay + ahh, ay - ahh left_b, right_b = bx - bhw, bx + bhw top_b, bottom_b = by + bhh, by - bhh # A to left or right of B? px = right_a - left_b if ax <= bx else right_b - left_a # A above or below B? py = top_b - bottom_a if ay >= by else top_a - bottom_b dx, dy = bx - ax, by - ay widths, heights = ahw + bhw, ahh + bhh # return separating axis + penetration depth (+ other axis for area calc) if widths + px - abs(dx) < heights + py - abs(dy): if dx >= 0: return 1, 0, -px, -py elif dx < 0: return -1, 0, -px, -py else: if dy >= 0: return 0, 1, -py, -px elif dy < 0: return 0, -1, -py, -px def circle_box_penetration(circle_x, circle_y, box_x, box_y, circle_radius, box_hw, box_hh): "Return penetration vector and magnitude for given circle and box." box_left, box_right = box_x - box_hw, box_x + box_hw box_top, box_bottom = box_y + box_hh, box_y - box_hh # if circle center inside box, use box-on-box penetration vector + distance if point_in_box(circle_x, circle_y, box_left, box_top, box_right, box_bottom): return box_penetration(circle_x, circle_y, box_x, box_y, circle_radius, circle_radius, box_hw, box_hh) # find point on AABB edges closest to center of circle # clamp = min(highest, max(lowest, val)) px = min(box_right, max(box_left, circle_x)) py = min(box_top, max(box_bottom, circle_y)) closest_x = circle_x - px closest_y = circle_y - py d = math.sqrt(closest_x ** 2 + closest_y ** 2) pdist = circle_radius - d if d == 0: return 1, 0, -pdist, -pdist # TODO: calculate other axis of intersection for area? return -closest_x / d, -closest_y / d, -pdist, -pdist
Don't use a CollisionShape
Use a CircleCollisionShap
Use an AABBCollisionShape
Tile-based collision: generate multiple AABBCollisionShapes to approximate all non-blank (character index 0) tiles of our GameObject's default Art's "collision layer", whose string name is defined in GO.col_layer_name.
"Collision type group", collections of CT_* values for more convenient checks.
"Collision type group", collections of CT_* values for more convenient checks.
Contact(overlap, timestamp)
Return first index of value.
Raises ValueError if the value is not present.
Return number of occurrences of value.
ShapeOverlap(x, y, dist, area, other)
Return first index of value.
Raises ValueError if the value is not present.
Return number of occurrences of value.
View Source
class CollisionShape: """ Abstract class for a shape that can overlap and collide with other shapes. Shapes are part of a Collideable which in turn is part of a GameObject. """ def resolve_overlaps_with_shapes(self, shapes): "Resolve this shape's overlap(s) with given list of shapes." overlaps = [] for other in shapes: if other is self: continue overlap = self.get_overlap(other) if overlap.dist < 0: overlaps.append(overlap) if len(overlaps) == 0: return # resolve collisions in order of largest -> smallest overlap overlaps.sort(key=lambda item: item.area, reverse=True) for i,old_overlap in enumerate(overlaps): # resolve first overlap without recalculating overlap = self.get_overlap(old_overlap.other) if i > 0 else overlaps[0] self.resolve_overlap(overlap) def resolve_overlap(self, overlap): "Resolve this shape's given overlap." other = overlap.other # tell objects they're overlapping, pass penetration vector a_coll_b, a_started_b = self.go.overlapped(other.go, overlap) b_coll_a, b_started_a = other.go.overlapped(self.go, overlap) # if either object says it shouldn't collide with other, don't if not a_coll_b or not b_coll_a: return # push shapes apart according to mass total_mass = max(0, self.go.mass) + max(0, other.go.mass) if self.go.is_dynamic(): if not other.go.is_dynamic() or other.go.mass < 0: a_push = overlap.dist else: a_push = (self.go.mass / total_mass) * overlap.dist # move parent object, not shape self.go.x += a_push * overlap.x self.go.y += a_push * overlap.y # update all shapes based on object's new position self.go.collision.update_transform_from_object() if other.go.is_dynamic(): if not self.go.is_dynamic() or self.go.mass < 0: b_push = overlap.dist else: b_push = (other.go.mass / total_mass) * overlap.dist other.go.x -= b_push * overlap.x other.go.y -= b_push * overlap.y other.go.collision.update_transform_from_object() # call objs' started_colliding once collisions have been resolved world = self.go.world if a_started_b: world.try_object_method(self.go, self.go.started_colliding, [other.go]) if b_started_a: world.try_object_method(other.go, other.go.started_colliding, [self.go]) def get_overlapping_static_shapes(self): "Return a list of static shapes that overlap with this shape." overlapping_shapes = [] shape_left, shape_top, shape_right, shape_bottom = self.get_box() # add padding to overlapping tiles check if False: padding = 0.01 shape_left -= padding shape_top -= padding shape_right += padding shape_bottom += padding for obj in self.go.world.objects.values(): if obj is self.go or not obj.should_collide() or obj.is_dynamic(): continue # always check non-tile-based static shapes if obj.collision_shape_type != CST_TILE: overlapping_shapes += obj.collision.shapes else: # skip if even bounds don't overlap obj_left, obj_top, obj_right, obj_bottom = obj.get_edges() if not boxes_overlap(shape_left, shape_top, shape_right, shape_bottom, obj_left, obj_top, obj_right, obj_bottom): continue overlapping_shapes += obj.collision.get_shapes_overlapping_box(shape_left, shape_top, shape_right, shape_bottom) return overlapping_shapes
Abstract class for a shape that can overlap and collide with other shapes. Shapes are part of a Collideable which in turn is part of a GameObject.
View Source
def resolve_overlaps_with_shapes(self, shapes): "Resolve this shape's overlap(s) with given list of shapes." overlaps = [] for other in shapes: if other is self: continue overlap = self.get_overlap(other) if overlap.dist < 0: overlaps.append(overlap) if len(overlaps) == 0: return # resolve collisions in order of largest -> smallest overlap overlaps.sort(key=lambda item: item.area, reverse=True) for i,old_overlap in enumerate(overlaps): # resolve first overlap without recalculating overlap = self.get_overlap(old_overlap.other) if i > 0 else overlaps[0] self.resolve_overlap(overlap)
Resolve this shape's overlap(s) with given list of shapes.
View Source
def resolve_overlap(self, overlap): "Resolve this shape's given overlap." other = overlap.other # tell objects they're overlapping, pass penetration vector a_coll_b, a_started_b = self.go.overlapped(other.go, overlap) b_coll_a, b_started_a = other.go.overlapped(self.go, overlap) # if either object says it shouldn't collide with other, don't if not a_coll_b or not b_coll_a: return # push shapes apart according to mass total_mass = max(0, self.go.mass) + max(0, other.go.mass) if self.go.is_dynamic(): if not other.go.is_dynamic() or other.go.mass < 0: a_push = overlap.dist else: a_push = (self.go.mass / total_mass) * overlap.dist # move parent object, not shape self.go.x += a_push * overlap.x self.go.y += a_push * overlap.y # update all shapes based on object's new position self.go.collision.update_transform_from_object() if other.go.is_dynamic(): if not self.go.is_dynamic() or self.go.mass < 0: b_push = overlap.dist else: b_push = (other.go.mass / total_mass) * overlap.dist other.go.x -= b_push * overlap.x other.go.y -= b_push * overlap.y other.go.collision.update_transform_from_object() # call objs' started_colliding once collisions have been resolved world = self.go.world if a_started_b: world.try_object_method(self.go, self.go.started_colliding, [other.go]) if b_started_a: world.try_object_method(other.go, other.go.started_colliding, [self.go])
Resolve this shape's given overlap.
View Source
def get_overlapping_static_shapes(self): "Return a list of static shapes that overlap with this shape." overlapping_shapes = [] shape_left, shape_top, shape_right, shape_bottom = self.get_box() # add padding to overlapping tiles check if False: padding = 0.01 shape_left -= padding shape_top -= padding shape_right += padding shape_bottom += padding for obj in self.go.world.objects.values(): if obj is self.go or not obj.should_collide() or obj.is_dynamic(): continue # always check non-tile-based static shapes if obj.collision_shape_type != CST_TILE: overlapping_shapes += obj.collision.shapes else: # skip if even bounds don't overlap obj_left, obj_top, obj_right, obj_bottom = obj.get_edges() if not boxes_overlap(shape_left, shape_top, shape_right, shape_bottom, obj_left, obj_top, obj_right, obj_bottom): continue overlapping_shapes += obj.collision.get_shapes_overlapping_box(shape_left, shape_top, shape_right, shape_bottom) return overlapping_shapes
Return a list of static shapes that overlap with this shape.
View Source
class CircleCollisionShape(CollisionShape): "CollisionShape using a circle area." def __init__(self, loc_x, loc_y, radius, game_object): self.x, self.y = loc_x, loc_y self.radius = radius self.go = game_object def get_box(self): "Return world coordinates of our bounds (left, top, right, bottom)" return self.x - self.radius, self.y - self.radius, self.x + self.radius, self.y + self.radius def is_point_inside(self, x, y): "Return True if given point is inside this shape." return (self.x - x) ** 2 + (self.y - y) ** 2 <= self.radius ** 2 def overlaps_line(self, x1, y1, x2, y2): "Return True if this circle overlaps given line segment." return circle_overlaps_line(self.x, self.y, self.radius, x1, y1, x2, y2) def get_overlap(self, other): "Return ShapeOverlap data for this shape's overlap with given other." if type(other) is CircleCollisionShape: px, py, pdist1, pdist2 = point_circle_penetration(self.x, self.y, other.x, other.y, self.radius + other.radius) elif type(other) is AABBCollisionShape: px, py, pdist1, pdist2 = circle_box_penetration(self.x, self.y, other.x, other.y, self.radius, other.halfwidth, other.halfheight) area = abs(pdist1 * pdist2) if pdist1 < 0 else 0 return ShapeOverlap(x=px, y=py, dist=pdist1, area=area, other=other)
CollisionShape using a circle area.
View Source
def __init__(self, loc_x, loc_y, radius, game_object): self.x, self.y = loc_x, loc_y self.radius = radius self.go = game_object
View Source
def get_box(self): "Return world coordinates of our bounds (left, top, right, bottom)" return self.x - self.radius, self.y - self.radius, self.x + self.radius, self.y + self.radius
Return world coordinates of our bounds (left, top, right, bottom)
View Source
def is_point_inside(self, x, y): "Return True if given point is inside this shape." return (self.x - x) ** 2 + (self.y - y) ** 2 <= self.radius ** 2
Return True if given point is inside this shape.
View Source
def overlaps_line(self, x1, y1, x2, y2): "Return True if this circle overlaps given line segment." return circle_overlaps_line(self.x, self.y, self.radius, x1, y1, x2, y2)
Return True if this circle overlaps given line segment.
View Source
def get_overlap(self, other): "Return ShapeOverlap data for this shape's overlap with given other." if type(other) is CircleCollisionShape: px, py, pdist1, pdist2 = point_circle_penetration(self.x, self.y, other.x, other.y, self.radius + other.radius) elif type(other) is AABBCollisionShape: px, py, pdist1, pdist2 = circle_box_penetration(self.x, self.y, other.x, other.y, self.radius, other.halfwidth, other.halfheight) area = abs(pdist1 * pdist2) if pdist1 < 0 else 0 return ShapeOverlap(x=px, y=py, dist=pdist1, area=area, other=other)
Return ShapeOverlap data for this shape's overlap with given other.
View Source
class AABBCollisionShape(CollisionShape): "CollisionShape using an axis-aligned bounding box area." def __init__(self, loc_x, loc_y, halfwidth, halfheight, game_object): self.x, self.y = loc_x, loc_y self.halfwidth, self.halfheight = halfwidth, halfheight self.go = game_object # for CST_TILE objects, lists of tile(s) we cover self.tiles = [] def get_box(self): return self.x - self.halfwidth, self.y - self.halfheight, self.x + self.halfwidth, self.y + self.halfheight def is_point_inside(self, x, y): "Return True if given point is inside this shape." return point_in_box(x, y, *self.get_box()) def overlaps_line(self, x1, y1, x2, y2): "Return True if this box overlaps given line segment." left, top, right, bottom = self.get_box() return box_overlaps_line(left, top, right, bottom, x1, y1, x2, y2) def get_overlap(self, other): "Return ShapeOverlap data for this shape's overlap with given other." if type(other) is AABBCollisionShape: px, py, pdist1, pdist2 = box_penetration(self.x, self.y, other.x, other.y, self.halfwidth, self.halfheight, other.halfwidth, other.halfheight) elif type(other) is CircleCollisionShape: px, py, pdist1, pdist2 = circle_box_penetration(other.x, other.y, self.x, self.y, other.radius, self.halfwidth, self.halfheight) # reverse result if we're shape B px, py = -px, -py area = abs(pdist1 * pdist2) if pdist1 < 0 else 0 return ShapeOverlap(x=px, y=py, dist=pdist1, area=area, other=other)
CollisionShape using an axis-aligned bounding box area.
View Source
def __init__(self, loc_x, loc_y, halfwidth, halfheight, game_object): self.x, self.y = loc_x, loc_y self.halfwidth, self.halfheight = halfwidth, halfheight self.go = game_object # for CST_TILE objects, lists of tile(s) we cover self.tiles = []
View Source
def get_box(self): return self.x - self.halfwidth, self.y - self.halfheight, self.x + self.halfwidth, self.y + self.halfheight
View Source
def is_point_inside(self, x, y): "Return True if given point is inside this shape." return point_in_box(x, y, *self.get_box())
Return True if given point is inside this shape.
View Source
def overlaps_line(self, x1, y1, x2, y2): "Return True if this box overlaps given line segment." left, top, right, bottom = self.get_box() return box_overlaps_line(left, top, right, bottom, x1, y1, x2, y2)
Return True if this box overlaps given line segment.
View Source
def get_overlap(self, other): "Return ShapeOverlap data for this shape's overlap with given other." if type(other) is AABBCollisionShape: px, py, pdist1, pdist2 = box_penetration(self.x, self.y, other.x, other.y, self.halfwidth, self.halfheight, other.halfwidth, other.halfheight) elif type(other) is CircleCollisionShape: px, py, pdist1, pdist2 = circle_box_penetration(other.x, other.y, self.x, self.y, other.radius, self.halfwidth, self.halfheight) # reverse result if we're shape B px, py = -px, -py area = abs(pdist1 * pdist2) if pdist1 < 0 else 0 return ShapeOverlap(x=px, y=py, dist=pdist1, area=area, other=other)
Return ShapeOverlap data for this shape's overlap with given other.
View Source
class Collideable: "Collision component for GameObjects. Contains a list of shapes." use_art_offset = False "use game object's art_off_pct values" def __init__(self, obj): "Create new Collideable for given GameObject." self.go = obj self.cl = self.go.world.cl self.renderables, self.shapes = [], [] self.tile_shapes = {} "Dict of shapes accessible by (x,y) tile coordinates" self.contacts = {} "Dict of contacts with other objects, by object name" self.create_shapes() def create_shapes(self): """ Create collision shape(s) appropriate to our game object's collision_shape_type value. """ self._clear_shapes() if self.go.collision_shape_type == CST_NONE: return elif self.go.collision_shape_type == CST_CIRCLE: self._create_circle() elif self.go.collision_shape_type == CST_AABB: self._create_box() elif self.go.collision_shape_type == CST_TILE: self.tile_shapes.clear() self._create_merged_tile_boxes() # update renderables once if static if not self.go.is_dynamic(): self.update_renderables() def _clear_shapes(self): for r in self.renderables: r.destroy() self.renderables = [] for shape in self.shapes: self.cl._remove_shape(shape) self.shapes = [] "List of CollisionShapes" def _create_circle(self): x = self.go.x + self.go.col_offset_x y = self.go.y + self.go.col_offset_y shape = self.cl._add_circle_shape(x, y, self.go.col_radius, self.go) self.shapes = [shape] self.renderables = [CircleCollisionRenderable(shape)] def _create_box(self): x = self.go.x # + self.go.col_offset_x y = self.go.y # + self.go.col_offset_y shape = self.cl._add_box_shape(x, y, self.go.col_width / 2, self.go.col_height / 2, self.go) self.shapes = [shape] self.renderables = [BoxCollisionRenderable(shape)] def _create_merged_tile_boxes(self): "Create AABB shapes for a CST_TILE object" # generate fewer, larger boxes! frame = self.go.renderable.frame if not self.go.col_layer_name in self.go.art.layer_names: self.go.app.dev_log("%s: Couldn't find collision layer with name '%s'" % (self.go.name, self.go.col_layer_name)) return layer = self.go.art.layer_names.index(self.go.col_layer_name) # tile is available if it's not empty and not already covered by a shape def tile_available(tile_x, tile_y): return self.go.art.get_char_index_at(frame, layer, tile_x, tile_y) != 0 and not (tile_x, tile_y) in self.tile_shapes def tile_range_available(start_x, end_x, start_y, end_y): for y in range(start_y, end_y + 1): for x in range(start_x, end_x + 1): if not tile_available(x, y): return False return True for y in range(self.go.art.height): for x in range(self.go.art.width): if not tile_available(x, y): continue # determine how big we can make this box # first fill left to right end_x = x while end_x < self.go.art.width - 1 and tile_available(end_x + 1, y): end_x += 1 # then fill top to bottom end_y = y while end_y < self.go.art.height - 1 and tile_range_available(x, end_x, y, end_y + 1): end_y += 1 # compute origin and halfsizes of box covering tile range wx1, wy1 = self.go.get_tile_loc(x, y, tile_center=True) wx2, wy2 = self.go.get_tile_loc(end_x, end_y, tile_center=True) wx = (wx1 + wx2) / 2 halfwidth = (end_x - x) * self.go.art.quad_width halfwidth /= 2 halfwidth += self.go.art.quad_width / 2 wy = (wy1 + wy2) / 2 halfheight = (end_y - y) * self.go.art.quad_height halfheight /= 2 halfheight += self.go.art.quad_height / 2 shape = self.cl._add_box_shape(wx, wy, halfwidth, halfheight, self.go) # fill in cell(s) in our tile collision dict, # write list of tiles shape covers to shape.tiles for tile_y in range(y, end_y + 1): for tile_x in range(x, end_x + 1): self.tile_shapes[(tile_x, tile_y)] = shape shape.tiles.append((tile_x, tile_y)) r = TileBoxCollisionRenderable(shape) # update renderable once to set location correctly r.update() self.shapes.append(shape) self.renderables.append(r) def get_shape_overlapping_point(self, x, y): "Return shape if it's overlapping given point, None if no overlap." tile_x, tile_y = self.go.get_tile_at_point(x, y) return self.tile_shapes.get((tile_x, tile_y), None) def get_shapes_overlapping_box(self, left, top, right, bottom): "Return a list of our shapes that overlap given box." shapes = [] tiles = self.go.get_tiles_overlapping_box(left, top, right, bottom) for (x, y) in tiles: shape = self.tile_shapes.get((x, y), None) if shape and not shape in shapes: shapes.append(shape) return shapes def update(self): if self.go and self.go.is_dynamic(): self.update_transform_from_object() def update_transform_from_object(self, obj=None): "Snap our shapes to location of given object (if unspecified, our GO)." obj = obj or self.go # CST_TILE shouldn't run here, it's static-only if obj.collision_shape_type == CST_TILE: return for shape in self.shapes: shape.x = obj.x + obj.col_offset_x shape.y = obj.y + obj.col_offset_y def set_shape_color(self, shape, new_color): "Set the color of a given shape's debug LineRenderable." try: shape_index = self.shapes.index(shape) except ValueError: return self.renderables[shape_index].color = new_color self.renderables[shape_index].build_geo() self.renderables[shape_index].rebind_buffers() def update_renderables(self): for r in self.renderables: r.update() def render(self): for r in self.renderables: r.render() def destroy(self): for r in self.renderables: r.destroy() # remove our shapes from CollisionLord's shape list for shape in self.shapes: self.cl._remove_shape(shape)
Collision component for GameObjects. Contains a list of shapes.
View Source
def __init__(self, obj): "Create new Collideable for given GameObject." self.go = obj self.cl = self.go.world.cl self.renderables, self.shapes = [], [] self.tile_shapes = {} "Dict of shapes accessible by (x,y) tile coordinates" self.contacts = {} "Dict of contacts with other objects, by object name" self.create_shapes()
Create new Collideable for given GameObject.
use game object's art_off_pct values
Dict of shapes accessible by (x,y) tile coordinates
Dict of contacts with other objects, by object name
View Source
def create_shapes(self): """ Create collision shape(s) appropriate to our game object's collision_shape_type value. """ self._clear_shapes() if self.go.collision_shape_type == CST_NONE: return elif self.go.collision_shape_type == CST_CIRCLE: self._create_circle() elif self.go.collision_shape_type == CST_AABB: self._create_box() elif self.go.collision_shape_type == CST_TILE: self.tile_shapes.clear() self._create_merged_tile_boxes() # update renderables once if static if not self.go.is_dynamic(): self.update_renderables()
Create collision shape(s) appropriate to our game object's collision_shape_type value.
View Source
def get_shape_overlapping_point(self, x, y): "Return shape if it's overlapping given point, None if no overlap." tile_x, tile_y = self.go.get_tile_at_point(x, y) return self.tile_shapes.get((tile_x, tile_y), None)
Return shape if it's overlapping given point, None if no overlap.
View Source
def get_shapes_overlapping_box(self, left, top, right, bottom): "Return a list of our shapes that overlap given box." shapes = [] tiles = self.go.get_tiles_overlapping_box(left, top, right, bottom) for (x, y) in tiles: shape = self.tile_shapes.get((x, y), None) if shape and not shape in shapes: shapes.append(shape) return shapes
Return a list of our shapes that overlap given box.
View Source
def update(self): if self.go and self.go.is_dynamic(): self.update_transform_from_object()
View Source
def update_transform_from_object(self, obj=None): "Snap our shapes to location of given object (if unspecified, our GO)." obj = obj or self.go # CST_TILE shouldn't run here, it's static-only if obj.collision_shape_type == CST_TILE: return for shape in self.shapes: shape.x = obj.x + obj.col_offset_x shape.y = obj.y + obj.col_offset_y
Snap our shapes to location of given object (if unspecified, our GO).
View Source
def set_shape_color(self, shape, new_color): "Set the color of a given shape's debug LineRenderable." try: shape_index = self.shapes.index(shape) except ValueError: return self.renderables[shape_index].color = new_color self.renderables[shape_index].build_geo() self.renderables[shape_index].rebind_buffers()
Set the color of a given shape's debug LineRenderable.
View Source
def update_renderables(self): for r in self.renderables: r.update()
View Source
def render(self): for r in self.renderables: r.render()
View Source
def destroy(self): for r in self.renderables: r.destroy() # remove our shapes from CollisionLord's shape list for shape in self.shapes: self.cl._remove_shape(shape)
View Source
class CollisionLord: """ Collision manager object, tracks Collideables, detects overlaps and resolves collisions. """ iterations = 7 """ Number of times to resolve collisions per update. Lower at own risk; multi-object collisions require multiple iterations to settle correctly. """ def __init__(self, world): self.world = world self.ticks = 0 # list of objects processed for collision this frame self.collisions_this_frame = [] self.reset() def report(self): print('%s: %s dynamic shapes, %s static shapes' % (self, len(self.dynamic_shapes), len(self.static_shapes))) def reset(self): self.dynamic_shapes, self.static_shapes = [], [] def _add_circle_shape(self, x, y, radius, game_object): shape = CircleCollisionShape(x, y, radius, game_object) if game_object.is_dynamic(): self.dynamic_shapes.append(shape) else: self.static_shapes.append(shape) return shape def _add_box_shape(self, x, y, halfwidth, halfheight, game_object): shape = AABBCollisionShape(x, y, halfwidth, halfheight, game_object) if game_object.is_dynamic(): self.dynamic_shapes.append(shape) else: self.static_shapes.append(shape) return shape def _remove_shape(self, shape): if shape in self.dynamic_shapes: self.dynamic_shapes.remove(shape) elif shape in self.static_shapes: self.static_shapes.remove(shape) def update(self): "Resolve overlaps between all relevant world objects." for i in range(self.iterations): # filter shape lists for anything out of room etc valid_dynamic_shapes = [] for shape in self.dynamic_shapes: if shape.go.should_collide(): valid_dynamic_shapes.append(shape) for shape in valid_dynamic_shapes: shape.resolve_overlaps_with_shapes(valid_dynamic_shapes) for shape in valid_dynamic_shapes: static_shapes = shape.get_overlapping_static_shapes() shape.resolve_overlaps_with_shapes(static_shapes) # check which objects stopped colliding for obj in self.world.objects.values(): obj.check_finished_contacts() self.ticks += 1 self.collisions_this_frame = []
Collision manager object, tracks Collideables, detects overlaps and resolves collisions.
View Source
def __init__(self, world): self.world = world self.ticks = 0 # list of objects processed for collision this frame self.collisions_this_frame = [] self.reset()
Number of times to resolve collisions per update. Lower at own risk; multi-object collisions require multiple iterations to settle correctly.
View Source
def report(self): print('%s: %s dynamic shapes, %s static shapes' % (self, len(self.dynamic_shapes), len(self.static_shapes)))
View Source
def reset(self): self.dynamic_shapes, self.static_shapes = [], []
View Source
def update(self): "Resolve overlaps between all relevant world objects." for i in range(self.iterations): # filter shape lists for anything out of room etc valid_dynamic_shapes = [] for shape in self.dynamic_shapes: if shape.go.should_collide(): valid_dynamic_shapes.append(shape) for shape in valid_dynamic_shapes: shape.resolve_overlaps_with_shapes(valid_dynamic_shapes) for shape in valid_dynamic_shapes: static_shapes = shape.get_overlapping_static_shapes() shape.resolve_overlaps_with_shapes(static_shapes) # check which objects stopped colliding for obj in self.world.objects.values(): obj.check_finished_contacts() self.ticks += 1 self.collisions_this_frame = []
Resolve overlaps between all relevant world objects.
View Source
def point_in_box(x, y, box_left, box_top, box_right, box_bottom): "Return True if given point lies within box with given corners." return box_left <= x <= box_right and box_bottom <= y <= box_top
Return True if given point lies within box with given corners.
View Source
def boxes_overlap(left_a, top_a, right_a, bottom_a, left_b, top_b, right_b, bottom_b): "Return True if given boxes A and B overlap." for (x, y) in ((left_a, top_a), (right_a, top_a), (right_a, bottom_a), (left_a, bottom_a)): if left_b <= x <= right_b and bottom_b <= y <= top_b: return True return False
Return True if given boxes A and B overlap.
View Source
def lines_intersect(x1, y1, x2, y2, x3, y3, x4, y4): "Return True if given lines intersect." denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) numer = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3) numer2 = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3) if denom == 0: if numer == 0 and numer2 == 0: # coincident return False # parallel return False ua = numer / denom ub = numer2 / denom return ua >= 0 and ua <= 1 and ub >= 0 and ub <= 1
Return True if given lines intersect.
View Source
def line_point_closest_to_point(point_x, point_y, x1, y1, x2, y2): "Return point on given line that's closest to given point." wx, wy = point_x - x1, point_y - y1 dir_x, dir_y = x2 - x1, y2 - y1 proj = wx * dir_x + wy * dir_y if proj <= 0: # line point 1 is closest return x1, y1 vsq = dir_x ** 2 + dir_y ** 2 if proj >= vsq: # line point 2 is closest return x2, y2 else: # closest point is between 1 and 2 return x1 + (proj / vsq) * dir_x, y1 + (proj / vsq) * dir_y
Return point on given line that's closest to given point.
View Source
def circle_overlaps_line(circle_x, circle_y, radius, x1, y1, x2, y2): "Return True if given circle overlaps given line." # get closest point on line to circle center closest_x, closest_y = line_point_closest_to_point(circle_x, circle_y, x1, y1, x2, y2) dist_x, dist_y = closest_x - circle_x, closest_y - circle_y return dist_x ** 2 + dist_y ** 2 <= radius ** 2
Return True if given circle overlaps given line.
View Source
def box_overlaps_line(left, top, right, bottom, x1, y1, x2, y2): "Return True if given box overlaps given line." # TODO: determine if this is less efficient than slab method below if point_in_box(x1, y1, left, top, right, bottom) and \ point_in_box(x2, y2, left, top, right, bottom): return True # check left/top/right/bottoms edges return lines_intersect(left, top, left, bottom, x1, y1, x2, y2) or \ lines_intersect(left, top, right, top, x1, y1, x2, y2) or \ lines_intersect(right, top, right, bottom, x1, y1, x2, y2) or \ lines_intersect(left, bottom, right, bottom, x1, y1, x2, y2)
Return True if given box overlaps given line.
View Source
def box_overlaps_ray(left, top, right, bottom, x1, y1, x2, y2): "Return True if given box overlaps given ray." # TODO: determine if this can be adapted for line segments # (just a matter of setting tmin/tmax properly?) tmin, tmax = -math.inf, math.inf dir_x, dir_y = x2 - x1, y2 - y1 if abs(dir_x) > 0: tx1 = (left - x1) / dir_x tx2 = (right - x1) / dir_x tmin = max(tmin, min(tx1, tx2)) tmax = min(tmax, max(tx1, tx2)) if abs(dir_y) > 0: ty1 = (top - y1) / dir_y ty2 = (bottom - y1) / dir_y tmin = max(tmin, min(ty1, ty2)) tmax = min(tmax, max(ty1, ty2)) return tmax >= tmin
Return True if given box overlaps given ray.
View Source
def point_circle_penetration(point_x, point_y, circle_x, circle_y, radius): "Return normalized penetration x, y, and distance for given circles." dx, dy = circle_x - point_x, circle_y - point_y pdist = math.sqrt(dx ** 2 + dy ** 2) # point is center of circle, arbitrarily project out in +X if pdist == 0: return 1, 0, -radius, -radius # TODO: calculate other axis of intersection for area? return dx / pdist, dy / pdist, pdist - radius, pdist - radius
Return normalized penetration x, y, and distance for given circles.
View Source
def box_penetration(ax, ay, bx, by, ahw, ahh, bhw, bhh): "Return penetration vector and magnitude for given boxes." left_a, right_a = ax - ahw, ax + ahw top_a, bottom_a = ay + ahh, ay - ahh left_b, right_b = bx - bhw, bx + bhw top_b, bottom_b = by + bhh, by - bhh # A to left or right of B? px = right_a - left_b if ax <= bx else right_b - left_a # A above or below B? py = top_b - bottom_a if ay >= by else top_a - bottom_b dx, dy = bx - ax, by - ay widths, heights = ahw + bhw, ahh + bhh # return separating axis + penetration depth (+ other axis for area calc) if widths + px - abs(dx) < heights + py - abs(dy): if dx >= 0: return 1, 0, -px, -py elif dx < 0: return -1, 0, -px, -py else: if dy >= 0: return 0, 1, -py, -px elif dy < 0: return 0, -1, -py, -px
Return penetration vector and magnitude for given boxes.
View Source
def circle_box_penetration(circle_x, circle_y, box_x, box_y, circle_radius, box_hw, box_hh): "Return penetration vector and magnitude for given circle and box." box_left, box_right = box_x - box_hw, box_x + box_hw box_top, box_bottom = box_y + box_hh, box_y - box_hh # if circle center inside box, use box-on-box penetration vector + distance if point_in_box(circle_x, circle_y, box_left, box_top, box_right, box_bottom): return box_penetration(circle_x, circle_y, box_x, box_y, circle_radius, circle_radius, box_hw, box_hh) # find point on AABB edges closest to center of circle # clamp = min(highest, max(lowest, val)) px = min(box_right, max(box_left, circle_x)) py = min(box_top, max(box_bottom, circle_y)) closest_x = circle_x - px closest_y = circle_y - py d = math.sqrt(closest_x ** 2 + closest_y ** 2) pdist = circle_radius - d if d == 0: return 1, 0, -pdist, -pdist # TODO: calculate other axis of intersection for area? return -closest_x / d, -closest_y / d, -pdist, -pdist
Return penetration vector and magnitude for given circle and box.