Mercurial > parpg-source
comparison bGrease/collision.py @ 65:e856b604b650
Changed "import bGrease" to "import parpg.bGrease".
author | KarstenBock@gmx.net |
---|---|
date | Wed, 21 Sep 2011 16:10:14 +0200 |
parents | ff3e395abf91 |
children |
comparison
equal
deleted
inserted
replaced
64:b73050f98411 | 65:e856b604b650 |
---|---|
37 broad-phase system are actually in collision. | 37 broad-phase system are actually in collision. |
38 """ | 38 """ |
39 | 39 |
40 __version__ = '$Id$' | 40 __version__ = '$Id$' |
41 | 41 |
42 from bGrease.geometry import Vec2d | 42 from parpg.bGrease.geometry import Vec2d |
43 from bisect import bisect_right | 43 from bisect import bisect_right |
44 | 44 |
45 | 45 |
46 class Pair(tuple): | 46 class Pair(tuple): |
47 """Pair of entities in collision. This is an ordered sequence of two | 47 """Pair of entities in collision. This is an ordered sequence of two |
48 entities, that compares and hashes unordered. | 48 entities, that compares and hashes unordered. |
49 | 49 |
50 Also stores additional collision point and normal vectors | 50 Also stores additional collision point and normal vectors |
51 for each entity. | 51 for each entity. |
52 | 52 |
53 Sets of ``Pair`` objects are exposed in the ``collision_pairs`` | 53 Sets of ``Pair`` objects are exposed in the ``collision_pairs`` |
54 attribute of collision systems to indicate the entity pairs in | 54 attribute of collision systems to indicate the entity pairs in |
55 collision. | 55 collision. |
56 """ | 56 """ |
57 info = None | 57 info = None |
58 """A sequence of (entity, collision point, collision normal) | 58 """A sequence of (entity, collision point, collision normal) |
59 for each entity in the pair | 59 for each entity in the pair |
60 """ | 60 """ |
61 | 61 |
62 def __new__(cls, entity1, entity2, point=None, normal=None): | 62 def __new__(cls, entity1, entity2, point=None, normal=None): |
63 pair = tuple.__new__(cls, (entity1, entity2)) | 63 pair = tuple.__new__(cls, (entity1, entity2)) |
64 return pair | 64 return pair |
65 | 65 |
66 def __hash__(self): | 66 def __hash__(self): |
67 return hash(self[0]) ^ hash(self[1]) | 67 return hash(self[0]) ^ hash(self[1]) |
68 | 68 |
69 def __eq__(self, other): | 69 def __eq__(self, other): |
70 other = tuple(other) | 70 other = tuple(other) |
71 return tuple(self) == other or (self[1], self[0]) == other | 71 return tuple(self) == other or (self[1], self[0]) == other |
72 | 72 |
73 def __repr__(self): | 73 def __repr__(self): |
74 return '%s%r' % (self.__class__.__name__, tuple(self)) | 74 return '%s%r' % (self.__class__.__name__, tuple(self)) |
75 | 75 |
76 def set_point_normal(self, point0, normal0, point1, normal1): | 76 def set_point_normal(self, point0, normal0, point1, normal1): |
77 """Set the collision point and normal for both entities""" | 77 """Set the collision point and normal for both entities""" |
78 self.info = ( | 78 self.info = ( |
79 (self[0], point0, normal0), | 79 (self[0], point0, normal0), |
80 (self[1], point1, normal1), | 80 (self[1], point1, normal1), |
81 ) | 81 ) |
82 | 82 |
83 | 83 |
84 class BroadSweepAndPrune(object): | 84 class BroadSweepAndPrune(object): |
85 """2D Broad-phase sweep and prune bounding box collision detector | 85 """2D Broad-phase sweep and prune bounding box collision detector |
86 | 86 |
87 This algorithm is efficient for collision detection between many | 87 This algorithm is efficient for collision detection between many |
88 moving bodies. It has linear algorithmic complexity and takes | 88 moving bodies. It has linear algorithmic complexity and takes |
89 advantage of temporal coherence between frames. It also does | 89 advantage of temporal coherence between frames. It also does |
90 not suffer from bad worst-case performance (like RDC can). | 90 not suffer from bad worst-case performance (like RDC can). |
91 Unlike spacial hashing, it does not need to be optimized for | 91 Unlike spacial hashing, it does not need to be optimized for |
92 specific space and body sizes. | 92 specific space and body sizes. |
93 | 93 |
94 Other algorithms may be more efficient for collision detection with | 94 Other algorithms may be more efficient for collision detection with |
95 stationary bodies, bodies that are always evenly distributed, or ad-hoc | 95 stationary bodies, bodies that are always evenly distributed, or ad-hoc |
96 queries. | 96 queries. |
97 | 97 |
98 :param collision_component: Name of the collision component used by this | 98 :param collision_component: Name of the collision component used by this |
99 system, defaults to 'collision'. This component supplies each | 99 system, defaults to 'collision'. This component supplies each |
100 entities' aabb and collision masks. | 100 entities' aabb and collision masks. |
101 :type collision_component: str | 101 :type collision_component: str |
102 """ | 102 """ |
103 world = None | 103 world = None |
104 """|World| object this system belongs to""" | 104 """|World| object this system belongs to""" |
105 | 105 |
106 collision_component = None | 106 collision_component = None |
107 """Name of world's collision component used by this system""" | 107 """Name of world's collision component used by this system""" |
108 | 108 |
109 LEFT_ATTR = "left" | 109 LEFT_ATTR = "left" |
110 RIGHT_ATTR = "right" | 110 RIGHT_ATTR = "right" |
111 TOP_ATTR = "top" | 111 TOP_ATTR = "top" |
112 BOTTOM_ATTR = "bottom" | 112 BOTTOM_ATTR = "bottom" |
113 | 113 |
114 def __init__(self, collision_component='collision'): | 114 def __init__(self, collision_component='collision'): |
115 self.collision_component = collision_component | 115 self.collision_component = collision_component |
116 self._by_x = None | 116 self._by_x = None |
117 self._by_y = None | 117 self._by_y = None |
118 self._collision_pairs = None | 118 self._collision_pairs = None |
119 | 119 |
120 def set_world(self, world): | 120 def set_world(self, world): |
121 """Bind the system to a world""" | 121 """Bind the system to a world""" |
122 self.world = world | 122 self.world = world |
123 | 123 |
124 def step(self, dt): | 124 def step(self, dt): |
125 """Update the system for this time step, updates and sorts the | 125 """Update the system for this time step, updates and sorts the |
126 axis arrays. | 126 axis arrays. |
127 """ | 127 """ |
128 component = getattr(self.world.components, self.collision_component) | 128 component = getattr(self.world.components, self.collision_component) |
129 LEFT = self.LEFT_ATTR | 129 LEFT = self.LEFT_ATTR |
130 RIGHT = self.RIGHT_ATTR | 130 RIGHT = self.RIGHT_ATTR |
131 TOP = self.TOP_ATTR | 131 TOP = self.TOP_ATTR |
132 BOTTOM = self.BOTTOM_ATTR | 132 BOTTOM = self.BOTTOM_ATTR |
133 if self._by_x is None: | 133 if self._by_x is None: |
134 # Build axis lists from scratch | 134 # Build axis lists from scratch |
135 # Note we cache the box positions here | 135 # Note we cache the box positions here |
136 # so that we can perform hit tests efficiently | 136 # so that we can perform hit tests efficiently |
137 # it also isolates us from changes made to the | 137 # it also isolates us from changes made to the |
138 # box positions after we run | 138 # box positions after we run |
139 by_x = self._by_x = [] | 139 by_x = self._by_x = [] |
140 append_x = by_x.append | 140 append_x = by_x.append |
141 by_y = self._by_y = [] | 141 by_y = self._by_y = [] |
142 append_y = by_y.append | 142 append_y = by_y.append |
143 for data in component.itervalues(): | 143 for data in component.itervalues(): |
144 append_x([data.aabb.left, LEFT, data]) | 144 append_x([data.aabb.left, LEFT, data]) |
145 append_x([data.aabb.right, RIGHT, data]) | 145 append_x([data.aabb.right, RIGHT, data]) |
146 append_y([data.aabb.bottom, BOTTOM, data]) | 146 append_y([data.aabb.bottom, BOTTOM, data]) |
147 append_y([data.aabb.top, TOP, data]) | 147 append_y([data.aabb.top, TOP, data]) |
148 else: | 148 else: |
149 by_x = self._by_x | 149 by_x = self._by_x |
150 by_y = self._by_y | 150 by_y = self._by_y |
151 removed = [] | 151 removed = [] |
152 for entry in by_x: | 152 for entry in by_x: |
153 entry[0] = getattr(entry[2].aabb, entry[1]) | 153 entry[0] = getattr(entry[2].aabb, entry[1]) |
154 for entry in by_y: | 154 for entry in by_y: |
155 entry[0] = getattr(entry[2].aabb, entry[1]) | 155 entry[0] = getattr(entry[2].aabb, entry[1]) |
156 # Removing entities is inefficient, but expected to be rare | 156 # Removing entities is inefficient, but expected to be rare |
157 if component.deleted_entities: | 157 if component.deleted_entities: |
158 deleted_entities = component.deleted_entities | 158 deleted_entities = component.deleted_entities |
159 deleted_x = [] | 159 deleted_x = [] |
160 deleted_y = [] | 160 deleted_y = [] |
161 for i, (_, _, data) in enumerate(by_x): | 161 for i, (_, _, data) in enumerate(by_x): |
162 if data.entity in deleted_entities: | 162 if data.entity in deleted_entities: |
163 deleted_x.append(i) | 163 deleted_x.append(i) |
164 deleted_x.reverse() | 164 deleted_x.reverse() |
165 for i in deleted_x: | 165 for i in deleted_x: |
166 del by_x[i] | 166 del by_x[i] |
167 for i, (_, _, data) in enumerate(by_y): | 167 for i, (_, _, data) in enumerate(by_y): |
168 if data.entity in deleted_entities: | 168 if data.entity in deleted_entities: |
169 deleted_y.append(i) | 169 deleted_y.append(i) |
170 deleted_y.reverse() | 170 deleted_y.reverse() |
171 for i in deleted_y: | 171 for i in deleted_y: |
172 del by_y[i] | 172 del by_y[i] |
173 # Tack on new entities | 173 # Tack on new entities |
174 for entity in component.new_entities: | 174 for entity in component.new_entities: |
175 data = component[entity] | 175 data = component[entity] |
176 by_x.append([data.aabb.left, LEFT, data]) | 176 by_x.append([data.aabb.left, LEFT, data]) |
177 by_x.append([data.aabb.right, RIGHT, data]) | 177 by_x.append([data.aabb.right, RIGHT, data]) |
178 by_y.append([data.aabb.bottom, BOTTOM, data]) | 178 by_y.append([data.aabb.bottom, BOTTOM, data]) |
179 by_y.append([data.aabb.top, TOP, data]) | 179 by_y.append([data.aabb.top, TOP, data]) |
180 | 180 |
181 # Tim-sort is highly efficient with mostly sorted lists. | 181 # Tim-sort is highly efficient with mostly sorted lists. |
182 # Because positions tend to change little each frame | 182 # Because positions tend to change little each frame |
183 # we take advantage of this here. Obviously things are | 183 # we take advantage of this here. Obviously things are |
184 # less efficient with very fast moving, or teleporting entities | 184 # less efficient with very fast moving, or teleporting entities |
185 by_x.sort() | 185 by_x.sort() |
186 by_y.sort() | 186 by_y.sort() |
187 self._collision_pairs = None | 187 self._collision_pairs = None |
188 | 188 |
189 @property | 189 @property |
190 def collision_pairs(self): | 190 def collision_pairs(self): |
191 """Set of candidate collision pairs for this timestep""" | 191 """Set of candidate collision pairs for this timestep""" |
192 if self._collision_pairs is None: | 192 if self._collision_pairs is None: |
193 if self._by_x is None: | 193 if self._by_x is None: |
194 # Axis arrays not ready | 194 # Axis arrays not ready |
195 return set() | 195 return set() |
196 | 196 |
197 LEFT = self.LEFT_ATTR | 197 LEFT = self.LEFT_ATTR |
198 RIGHT = self.RIGHT_ATTR | 198 RIGHT = self.RIGHT_ATTR |
199 TOP = self.TOP_ATTR | 199 TOP = self.TOP_ATTR |
200 BOTTOM = self.BOTTOM_ATTR | 200 BOTTOM = self.BOTTOM_ATTR |
201 # Build candidates overlapping along the x-axis | 201 # Build candidates overlapping along the x-axis |
202 component = getattr(self.world.components, self.collision_component) | 202 component = getattr(self.world.components, self.collision_component) |
203 xoverlaps = set() | 203 xoverlaps = set() |
204 add_xoverlap = xoverlaps.add | 204 add_xoverlap = xoverlaps.add |
205 discard_xoverlap = xoverlaps.discard | 205 discard_xoverlap = xoverlaps.discard |
206 open = {} | 206 open = {} |
207 for _, side, data in self._by_x: | 207 for _, side, data in self._by_x: |
208 if side is LEFT: | 208 if side is LEFT: |
209 for open_entity, (from_mask, into_mask) in open.iteritems(): | 209 for open_entity, (from_mask, into_mask) in open.iteritems(): |
210 if data.from_mask & into_mask or from_mask & data.into_mask: | 210 if data.from_mask & into_mask or from_mask & data.into_mask: |
211 add_xoverlap(Pair(data.entity, open_entity)) | 211 add_xoverlap(Pair(data.entity, open_entity)) |
212 open[data.entity] = (data.from_mask, data.into_mask) | 212 open[data.entity] = (data.from_mask, data.into_mask) |
213 elif side is RIGHT: | 213 elif side is RIGHT: |
214 del open[data.entity] | 214 del open[data.entity] |
215 | 215 |
216 if len(xoverlaps) <= 10 and len(xoverlaps)*4 < len(self._by_y): | 216 if len(xoverlaps) <= 10 and len(xoverlaps)*4 < len(self._by_y): |
217 # few candidates were found, so just scan the x overlap candidates | 217 # few candidates were found, so just scan the x overlap candidates |
218 # along y. This requires an additional sort, but it should | 218 # along y. This requires an additional sort, but it should |
219 # be cheaper than scanning everyone and its simpler | 219 # be cheaper than scanning everyone and its simpler |
220 # than a separate brute-force check | 220 # than a separate brute-force check |
221 entities = set([entity for entity, _ in xoverlaps] | 221 entities = set([entity for entity, _ in xoverlaps] |
222 + [entity for _, entity in xoverlaps]) | 222 + [entity for _, entity in xoverlaps]) |
223 by_y = [] | 223 by_y = [] |
224 for entity in entities: | 224 for entity in entities: |
225 data = component[entity] | 225 data = component[entity] |
226 # We can use tuples here, which are cheaper to create | 226 # We can use tuples here, which are cheaper to create |
227 by_y.append((data.aabb.bottom, BOTTOM, data)) | 227 by_y.append((data.aabb.bottom, BOTTOM, data)) |
228 by_y.append((data.aabb.top, TOP, data)) | 228 by_y.append((data.aabb.top, TOP, data)) |
229 by_y.sort() | 229 by_y.sort() |
230 else: | 230 else: |
231 by_y = self._by_y | 231 by_y = self._by_y |
232 | 232 |
233 # Now check the candidates along the y-axis | 233 # Now check the candidates along the y-axis |
234 open = set() | 234 open = set() |
235 add_open = open.add | 235 add_open = open.add |
236 discard_open = open.discard | 236 discard_open = open.discard |
237 self._collision_pairs = set() | 237 self._collision_pairs = set() |
238 add_pair = self._collision_pairs.add | 238 add_pair = self._collision_pairs.add |
239 for _, side, data in by_y: | 239 for _, side, data in by_y: |
240 if side is BOTTOM: | 240 if side is BOTTOM: |
241 for open_entity in open: | 241 for open_entity in open: |
242 pair = Pair(data.entity, open_entity) | 242 pair = Pair(data.entity, open_entity) |
243 if pair in xoverlaps: | 243 if pair in xoverlaps: |
244 discard_xoverlap(pair) | 244 discard_xoverlap(pair) |
245 add_pair(pair) | 245 add_pair(pair) |
246 if not xoverlaps: | 246 if not xoverlaps: |
247 # No more candidates, bail | 247 # No more candidates, bail |
248 return self._collision_pairs | 248 return self._collision_pairs |
249 add_open(data.entity) | 249 add_open(data.entity) |
250 elif side is TOP: | 250 elif side is TOP: |
251 discard_open(data.entity) | 251 discard_open(data.entity) |
252 return self._collision_pairs | 252 return self._collision_pairs |
253 | 253 |
254 def query_point(self, x_or_point, y=None, from_mask=0xffffffff): | 254 def query_point(self, x_or_point, y=None, from_mask=0xffffffff): |
255 """Hit test at the point specified. | 255 """Hit test at the point specified. |
256 | 256 |
257 :param x_or_point: x coordinate (float) or sequence of (x, y) floats. | 257 :param x_or_point: x coordinate (float) or sequence of (x, y) floats. |
258 | 258 |
259 :param y: y coordinate (float) if x is not a sequence | 259 :param y: y coordinate (float) if x is not a sequence |
260 | 260 |
261 :param from_mask: Bit mask used to filter query results. This value | 261 :param from_mask: Bit mask used to filter query results. This value |
262 is bit ANDed with candidate entities' ``collision.into_mask``. | 262 is bit ANDed with candidate entities' ``collision.into_mask``. |
263 If the result is non-zero, then it is considered a hit. By | 263 If the result is non-zero, then it is considered a hit. By |
264 default all entities colliding with the input point are | 264 default all entities colliding with the input point are |
265 returned. | 265 returned. |
266 | 266 |
267 :return: A set of entities where the point is inside their bounding | 267 :return: A set of entities where the point is inside their bounding |
268 boxes as of the last time step. | 268 boxes as of the last time step. |
269 """ | 269 """ |
270 if self._by_x is None: | 270 if self._by_x is None: |
271 # Axis arrays not ready | 271 # Axis arrays not ready |
272 return set() | 272 return set() |
273 if y is None: | 273 if y is None: |
274 x, y = x_or_point | 274 x, y = x_or_point |
275 else: | 275 else: |
276 x = x_or_point | 276 x = x_or_point |
277 LEFT = self.LEFT_ATTR | 277 LEFT = self.LEFT_ATTR |
278 RIGHT = self.RIGHT_ATTR | 278 RIGHT = self.RIGHT_ATTR |
279 TOP = self.TOP_ATTR | 279 TOP = self.TOP_ATTR |
280 BOTTOM = self.BOTTOM_ATTR | 280 BOTTOM = self.BOTTOM_ATTR |
281 x_index = bisect_right(self._by_x, [x]) | 281 x_index = bisect_right(self._by_x, [x]) |
282 x_hits = set() | 282 x_hits = set() |
283 add_x_hit = x_hits.add | 283 add_x_hit = x_hits.add |
284 discard_x_hit = x_hits.discard | 284 discard_x_hit = x_hits.discard |
285 if x_index <= len(self._by_x) // 2: | 285 if x_index <= len(self._by_x) // 2: |
286 # closer to the left, scan from left to right | 286 # closer to the left, scan from left to right |
287 while (x == self._by_x[x_index][0] | 287 while (x == self._by_x[x_index][0] |
288 and self._by_x[x_index][1] is LEFT | 288 and self._by_x[x_index][1] is LEFT |
289 and x_index < len(self._by_x)): | 289 and x_index < len(self._by_x)): |
290 # Ensure we hit on exact left edge matches | 290 # Ensure we hit on exact left edge matches |
291 x_index += 1 | 291 x_index += 1 |
292 for _, side, data in self._by_x[:x_index]: | 292 for _, side, data in self._by_x[:x_index]: |
293 if side is LEFT and from_mask & data.into_mask: | 293 if side is LEFT and from_mask & data.into_mask: |
294 add_x_hit(data.entity) | 294 add_x_hit(data.entity) |
295 else: | 295 else: |
296 discard_x_hit(data.entity) | 296 discard_x_hit(data.entity) |
297 else: | 297 else: |
298 # closer to the right | 298 # closer to the right |
299 for _, side, data in reversed(self._by_x[x_index:]): | 299 for _, side, data in reversed(self._by_x[x_index:]): |
300 if side is RIGHT and from_mask & data.into_mask: | 300 if side is RIGHT and from_mask & data.into_mask: |
301 add_x_hit(data.entity) | 301 add_x_hit(data.entity) |
302 else: | 302 else: |
303 discard_x_hit(data.entity) | 303 discard_x_hit(data.entity) |
304 if not x_hits: | 304 if not x_hits: |
305 return x_hits | 305 return x_hits |
306 | 306 |
307 y_index = bisect_right(self._by_y, [y]) | 307 y_index = bisect_right(self._by_y, [y]) |
308 y_hits = set() | 308 y_hits = set() |
309 add_y_hit = y_hits.add | 309 add_y_hit = y_hits.add |
310 discard_y_hit = y_hits.discard | 310 discard_y_hit = y_hits.discard |
311 if y_index <= len(self._by_y) // 2: | 311 if y_index <= len(self._by_y) // 2: |
312 # closer to the bottom | 312 # closer to the bottom |
313 while (y == self._by_y[y_index][0] | 313 while (y == self._by_y[y_index][0] |
314 and self._by_y[y_index][1] is BOTTOM | 314 and self._by_y[y_index][1] is BOTTOM |
315 and y_index < len(self._by_y)): | 315 and y_index < len(self._by_y)): |
316 # Ensure we hit on exact bottom edge matches | 316 # Ensure we hit on exact bottom edge matches |
317 y_index += 1 | 317 y_index += 1 |
318 for _, side, data in self._by_y[:y_index]: | 318 for _, side, data in self._by_y[:y_index]: |
319 if side is BOTTOM: | 319 if side is BOTTOM: |
320 add_y_hit(data.entity) | 320 add_y_hit(data.entity) |
321 else: | 321 else: |
322 discard_y_hit(data.entity) | 322 discard_y_hit(data.entity) |
323 else: | 323 else: |
324 # closer to the top | 324 # closer to the top |
325 for _, side, data in reversed(self._by_y[y_index:]): | 325 for _, side, data in reversed(self._by_y[y_index:]): |
326 if side is TOP: | 326 if side is TOP: |
327 add_y_hit(data.entity) | 327 add_y_hit(data.entity) |
328 else: | 328 else: |
329 discard_y_hit(data.entity) | 329 discard_y_hit(data.entity) |
330 if y_hits: | 330 if y_hits: |
331 return x_hits & y_hits | 331 return x_hits & y_hits |
332 else: | 332 else: |
333 return y_hits | 333 return y_hits |
334 | 334 |
335 | 335 |
336 class Circular(object): | 336 class Circular(object): |
337 """Basic narrow-phase collision detector which treats all entities as | 337 """Basic narrow-phase collision detector which treats all entities as |
338 circles with their radius defined in the collision component. | 338 circles with their radius defined in the collision component. |
339 | 339 |
340 :param handlers: A sequence of collision handler functions that are invoked | 340 :param handlers: A sequence of collision handler functions that are invoked |
341 after collision detection. | 341 after collision detection. |
342 :type handlers: sequence of functions | 342 :type handlers: sequence of functions |
343 | 343 |
344 :param collision_component: Name of collision component for this system, | 344 :param collision_component: Name of collision component for this system, |
345 defaults to 'collision'. This supplies each entity's collision | 345 defaults to 'collision'. This supplies each entity's collision |
346 radius and masks. | 346 radius and masks. |
347 :type collision_component: str | 347 :type collision_component: str |
348 | 348 |
349 :param position_component: Name of position component for this system, | 349 :param position_component: Name of position component for this system, |
350 defaults to 'position'. This supplies each entity's position. | 350 defaults to 'position'. This supplies each entity's position. |
351 :type position_component: str | 351 :type position_component: str |
352 | 352 |
353 :param update_aabbs: If True (the default), then the entities' | 353 :param update_aabbs: If True (the default), then the entities' |
354 `collision.aabb` fields will be updated using their position | 354 `collision.aabb` fields will be updated using their position |
355 and collision radius before invoking the broad phase system. | 355 and collision radius before invoking the broad phase system. |
356 Set this False if another system updates the aabbs. | 356 Set this False if another system updates the aabbs. |
357 :type update_aabbs: bool | 357 :type update_aabbs: bool |
358 | 358 |
359 :param broad_phase: A broad-phase collision system to use as a source | 359 :param broad_phase: A broad-phase collision system to use as a source |
360 for collision pairs. If not specified, a :class:`BroadSweepAndPrune` | 360 for collision pairs. If not specified, a :class:`BroadSweepAndPrune` |
361 system will be created automatically. | 361 system will be created automatically. |
362 """ | 362 """ |
363 world = None | 363 world = None |
364 """|World| object this system belongs to""" | 364 """|World| object this system belongs to""" |
365 | 365 |
366 position_component = None | 366 position_component = None |
367 """Name of world's position component used by this system""" | 367 """Name of world's position component used by this system""" |
368 | 368 |
369 collision_component = None | 369 collision_component = None |
370 """Name of world's collision component used by this system""" | 370 """Name of world's collision component used by this system""" |
371 | 371 |
372 update_aabbs = True | 372 update_aabbs = True |
373 """Flag to indicate whether the system updates the entities' `collision.aabb` | 373 """Flag to indicate whether the system updates the entities' `collision.aabb` |
374 field before invoking the broad phase collision system | 374 field before invoking the broad phase collision system |
375 """ | 375 """ |
376 | 376 |
377 handlers = None | 377 handlers = None |
378 """A sequence of collision handler functions invoke after collision | 378 """A sequence of collision handler functions invoke after collision |
379 detection | 379 detection |
380 """ | 380 """ |
381 | 381 |
382 broad_phase = None | 382 broad_phase = None |
383 """Broad phase collision system used as a source for collision pairs""" | 383 """Broad phase collision system used as a source for collision pairs""" |
384 | 384 |
385 def __init__(self, handlers=(), position_component='position', | 385 def __init__(self, handlers=(), position_component='position', |
386 collision_component='collision', update_aabbs=True, broad_phase=None): | 386 collision_component='collision', update_aabbs=True, broad_phase=None): |
387 self.handlers = tuple(handlers) | 387 self.handlers = tuple(handlers) |
388 if broad_phase is None: | 388 if broad_phase is None: |
389 broad_phase = BroadSweepAndPrune(collision_component) | 389 broad_phase = BroadSweepAndPrune(collision_component) |
390 self.collision_component = collision_component | 390 self.collision_component = collision_component |
391 self.position_component = position_component | 391 self.position_component = position_component |
392 self.update_aabbs = bool(update_aabbs) | 392 self.update_aabbs = bool(update_aabbs) |
393 self.broad_phase = broad_phase | 393 self.broad_phase = broad_phase |
394 self._collision_pairs = None | 394 self._collision_pairs = None |
395 | 395 |
396 def set_world(self, world): | 396 def set_world(self, world): |
397 """Bind the system to a world""" | 397 """Bind the system to a world""" |
398 self.world = world | 398 self.world = world |
399 self.broad_phase.set_world(world) | 399 self.broad_phase.set_world(world) |
400 for handler in self.handlers: | 400 for handler in self.handlers: |
401 if hasattr(handler, 'set_world'): | 401 if hasattr(handler, 'set_world'): |
402 handler.set_world(world) | 402 handler.set_world(world) |
403 | 403 |
404 def step(self, dt): | 404 def step(self, dt): |
405 """Update the collision system for this time step and invoke | 405 """Update the collision system for this time step and invoke |
406 the handlers | 406 the handlers |
407 """ | 407 """ |
408 if self.update_aabbs: | 408 if self.update_aabbs: |
409 for position, collision in self.world.components.join( | 409 for position, collision in self.world.components.join( |
410 self.position_component, self.collision_component): | 410 self.position_component, self.collision_component): |
411 aabb = collision.aabb | 411 aabb = collision.aabb |
412 x, y = position.position | 412 x, y = position.position |
413 radius = collision.radius | 413 radius = collision.radius |
414 aabb.left = x - radius | 414 aabb.left = x - radius |
415 aabb.right = x + radius | 415 aabb.right = x + radius |
416 aabb.bottom = y - radius | 416 aabb.bottom = y - radius |
417 aabb.top = y + radius | 417 aabb.top = y + radius |
418 self.broad_phase.step(dt) | 418 self.broad_phase.step(dt) |
419 self._collision_pairs = None | 419 self._collision_pairs = None |
420 for handler in self.handlers: | 420 for handler in self.handlers: |
421 handler(self) | 421 handler(self) |
422 | 422 |
423 @property | 423 @property |
424 def collision_pairs(self): | 424 def collision_pairs(self): |
425 """The set of entity pairs in collision in this timestep""" | 425 """The set of entity pairs in collision in this timestep""" |
426 if self._collision_pairs is None: | 426 if self._collision_pairs is None: |
427 position = getattr(self.world.components, self.position_component) | 427 position = getattr(self.world.components, self.position_component) |
428 collision = getattr(self.world.components, self.collision_component) | 428 collision = getattr(self.world.components, self.collision_component) |
429 pairs = self._collision_pairs = set() | 429 pairs = self._collision_pairs = set() |
430 for pair in self.broad_phase.collision_pairs: | 430 for pair in self.broad_phase.collision_pairs: |
431 entity1, entity2 = pair | 431 entity1, entity2 = pair |
432 position1 = position[entity1].position | 432 position1 = position[entity1].position |
433 position2 = position[entity2].position | 433 position2 = position[entity2].position |
434 radius1 = collision[entity1].radius | 434 radius1 = collision[entity1].radius |
435 radius2 = collision[entity2].radius | 435 radius2 = collision[entity2].radius |
436 separation = position2 - position1 | 436 separation = position2 - position1 |
437 if separation.get_length_sqrd() <= (radius1 + radius2)**2: | 437 if separation.get_length_sqrd() <= (radius1 + radius2)**2: |
438 normal = separation.normalized() | 438 normal = separation.normalized() |
439 pair.set_point_normal( | 439 pair.set_point_normal( |
440 normal * radius1 + position1, normal, | 440 normal * radius1 + position1, normal, |
441 normal * -radius2 + position2, -normal) | 441 normal * -radius2 + position2, -normal) |
442 pairs.add(pair) | 442 pairs.add(pair) |
443 return self._collision_pairs | 443 return self._collision_pairs |
444 | 444 |
445 def query_point(self, x_or_point, y=None, from_mask=0xffffffff): | 445 def query_point(self, x_or_point, y=None, from_mask=0xffffffff): |
446 """Hit test at the point specified. | 446 """Hit test at the point specified. |
447 | 447 |
448 :param x_or_point: x coordinate (float) or sequence of (x, y) floats. | 448 :param x_or_point: x coordinate (float) or sequence of (x, y) floats. |
449 | 449 |
450 :param y: y coordinate (float) if x is not a sequence | 450 :param y: y coordinate (float) if x is not a sequence |
451 | 451 |
452 :param from_mask: Bit mask used to filter query results. This value | 452 :param from_mask: Bit mask used to filter query results. This value |
453 is bit ANDed with candidate entities' ``collision.into_mask``. | 453 is bit ANDed with candidate entities' ``collision.into_mask``. |
454 If the result is non-zero, then it is considered a hit. By | 454 If the result is non-zero, then it is considered a hit. By |
455 default all entities colliding with the input point are | 455 default all entities colliding with the input point are |
456 returned. | 456 returned. |
457 | 457 |
458 :return: A set of entities where the point is inside their collision | 458 :return: A set of entities where the point is inside their collision |
459 radii as of the last time step. | 459 radii as of the last time step. |
460 | 460 |
461 """ | 461 """ |
462 if y is None: | 462 if y is None: |
463 point = Vec2d(x_or_point) | 463 point = Vec2d(x_or_point) |
464 else: | 464 else: |
465 point = Vec2d(x_or_point, y) | 465 point = Vec2d(x_or_point, y) |
466 hits = set() | 466 hits = set() |
467 position = getattr(self.world.components, self.position_component) | 467 position = getattr(self.world.components, self.position_component) |
468 collision = getattr(self.world.components, self.collision_component) | 468 collision = getattr(self.world.components, self.collision_component) |
469 for entity in self.broad_phase.query_point(x_or_point, y, from_mask): | 469 for entity in self.broad_phase.query_point(x_or_point, y, from_mask): |
470 separation = point - position[entity].position | 470 separation = point - position[entity].position |
471 if separation.get_length_sqrd() <= collision[entity].radius**2: | 471 if separation.get_length_sqrd() <= collision[entity].radius**2: |
472 hits.add(entity) | 472 hits.add(entity) |
473 return hits | 473 return hits |
474 | 474 |
475 | 475 |
476 def dispatch_events(collision_system): | 476 def dispatch_events(collision_system): |
477 """Collision handler that dispatches `on_collide()` events to entities | 477 """Collision handler that dispatches `on_collide()` events to entities |
478 marked for collision by the specified collision system. The `on_collide()` | 478 marked for collision by the specified collision system. The `on_collide()` |
479 event handler methods are defined by the application on the desired entity | 479 event handler methods are defined by the application on the desired entity |
480 classes. These methods should have the following signature:: | 480 classes. These methods should have the following signature:: |
481 | 481 |
482 def on_collide(self, other_entity, collision_point, collision_normal): | 482 def on_collide(self, other_entity, collision_point, collision_normal): |
483 '''Handle A collision between this entity and `other_entity` | 483 '''Handle A collision between this entity and `other_entity` |
484 | 484 |
485 - other_entity (Entity): The other entity in collision with | 485 - other_entity (Entity): The other entity in collision with |
486 `self` | 486 `self` |
487 | 487 |
488 - collision_point (Vec2d): The point on this entity (`self`) | 488 - collision_point (Vec2d): The point on this entity (`self`) |
489 where the collision occurred. Note this may be `None` for | 489 where the collision occurred. Note this may be `None` for |
490 some collision systems that do not report it. | 490 some collision systems that do not report it. |
491 | 491 |
492 - collision_normal (Vec2d): The normal vector at the point of | 492 - collision_normal (Vec2d): The normal vector at the point of |
493 collision. As with `collision_point`, this may be None for | 493 collision. As with `collision_point`, this may be None for |
494 some collision systems. | 494 some collision systems. |
495 ''' | 495 ''' |
496 | 496 |
497 Note the arguments to `on_collide()` are always passed positionally, so you | 497 Note the arguments to `on_collide()` are always passed positionally, so you |
498 can use different argument names than above if desired. | 498 can use different argument names than above if desired. |
499 | 499 |
500 If a pair of entities are in collision, then the event will be dispatched | 500 If a pair of entities are in collision, then the event will be dispatched |
501 to both objects in arbitrary order if all of their collision masks align. | 501 to both objects in arbitrary order if all of their collision masks align. |
502 """ | 502 """ |
503 collision = getattr(collision_system.world.components, | 503 collision = getattr(collision_system.world.components, |
504 collision_system.collision_component) | 504 collision_system.collision_component) |
505 for pair in collision_system.collision_pairs: | 505 for pair in collision_system.collision_pairs: |
506 entity1, entity2 = pair | 506 entity1, entity2 = pair |
507 if pair.info is not None: | 507 if pair.info is not None: |
508 args1, args2 = pair.info | 508 args1, args2 = pair.info |
509 else: | 509 else: |
510 args1 = entity1, None, None | 510 args1 = entity1, None, None |
511 args2 = entity2, None, None | 511 args2 = entity2, None, None |
512 try: | 512 try: |
513 on_collide = entity1.on_collide | 513 on_collide = entity1.on_collide |
514 masks_align = collision[entity2].from_mask & collision[entity1].into_mask | 514 masks_align = collision[entity2].from_mask & collision[entity1].into_mask |
515 except (AttributeError, KeyError): | 515 except (AttributeError, KeyError): |
516 pass | 516 pass |
517 else: | 517 else: |
518 if masks_align: | 518 if masks_align: |
519 on_collide(*args2) | 519 on_collide(*args2) |
520 try: | 520 try: |
521 on_collide = entity2.on_collide | 521 on_collide = entity2.on_collide |
522 masks_align = collision[entity1].from_mask & collision[entity2].into_mask | 522 masks_align = collision[entity1].from_mask & collision[entity2].into_mask |
523 except (AttributeError, KeyError): | 523 except (AttributeError, KeyError): |
524 pass | 524 pass |
525 else: | 525 else: |
526 if masks_align: | 526 if masks_align: |
527 on_collide(*args1) | 527 on_collide(*args1) |
528 |