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