1
2 """Classes and algorithms for collision detection."""
3
4 from math import sqrt
5 import os
6 import struct
7
8 __author__ = "B. Henne, F. Ludwig, P. Tute"
9 __contact__ = "henne@dcsec.uni-hannover.de"
10 __copyright__ = "(c) 2010-2011, DCSec, Leibniz Universitaet Hannover, Germany"
11 __license__ = "GPLv3"
12
13
15 """A set of all walkable elements in the simulated world, e. g. streets and
16 public places.
17 @author: F. Ludwig
18 @author: P. Tute
19 @author: B. Henne"""
21 """Initialize collision grid.
22
23 grid_size determines maximum collision region. Collision
24 range/region/radius must be lower or equal the grid_size.
25 World.obj is the set of walkable elements.
26 calculate_grid() must be called after all objects have been loaded."""
27 self.grid_size = int(grid_size)
28 self.obj = set()
29 self.free_obj = set()
30 self.grid = {}
31
33 """Adds an object to the world."""
34 self.obj.add(obj)
35
37 """Updates an object of the world."""
38 self.obj.update(items)
39
41 """Calculates the collision grid of the World.
42
43 This method must be called after all world objects have been
44 added to the World. It calculate which objects are in which grid segment.
45 Calculation is done by colliding each object with each grid segment.
46 Determining a grid grid segment by coordinates is done by integer maths.
47 A grid segment is represented by its west and south border, e.g.
48 grid_size=50, p=(127.9; 33.2) => segment is x=100, y=0."""
49
50 if not hasattr(self, 'start_x'):
51
52 min_x = min_y = float('inf')
53 max_x = max_y = 0
54 for obj in self.obj:
55 for point in obj.get_points():
56 min_x = min(min_x, point[0])
57 max_x = max(max_x, point[0])
58 min_y = min(min_y, point[1])
59 max_y = max(max_y, point[1])
60
61 self.start_x = int(min_x) / self.grid_size * self.grid_size
62 self.start_y = int(min_y) / self.grid_size * self.grid_size
63 self.end_x = int(max_x) / self.grid_size * self.grid_size
64 self.end_y = int(max_y) / self.grid_size * self.grid_size
65
66
67 cache_path = None
68 if cache_base_path:
69 cache_path = cache_base_path + '.grid' + str(self.grid_size)
70
71
72 if cache_path and os.path.exists(cache_path):
73 objs = {}
74 for obj in self.obj:
75 objs[obj.id] = obj
76 rect_data_fmt = struct.Struct('!III')
77 id_fmt = struct.Struct('!I')
78 cache = open(cache_path)
79 data = cache.read(rect_data_fmt.size)
80 while data:
81 x, y, count = rect_data_fmt.unpack(data)
82 self.grid.setdefault(x, {})
83 rect = set()
84 for i in xrange(count):
85 rect.add(objs[id_fmt.unpack(cache.read(id_fmt.size))[0]])
86 self.grid[x][y] = rect
87 data = cache.read(rect_data_fmt.size)
88
89 else:
90
91 for x in xrange(self.start_x, self.end_x + 1, self.grid_size):
92 self.grid[x] = {}
93 for y in xrange(self.start_y, self.end_y + 1, self.grid_size):
94
95 rect = self.collide_rectangle(x, y, x + self.grid_size, y + self.grid_size)
96 self.grid[x][y] = rect
97
98 if cache_path:
99 cache = open(cache_path, 'w')
100 for x in self.grid:
101 for y in self.grid[x]:
102 data = ''
103 for obj in self.grid[x][y]:
104 data += struct.pack('!I', obj.id)
105 cache.write(struct.pack('!III', x, y, len(self.grid[x][y])) + data)
106 cache.close()
107
109 """Checks all registered walkable objects for a collision with a
110 given circle. Simplest implementation, least performance."""
111 re = set()
112 for obj in self.obj | self.free_obj:
113 if obj.collide_circle(x, y, radius):
114 re.add(obj)
115 return re
116
118 """Checks all registered walkable objects for collision that are
119 in grid segments which collide with the specified circle.
120
121 This method first determines all concerned grid segments and finally
122 only collides objects located in these segments. This implementation
123 uses sub/add/abs to determine neighbor segments by checking coordinates."""
124 grid_size = self.grid_size
125 rect = self.grid
126 assert radius <= grid_size
127
128
129
130 r_x = int(x) / grid_size * grid_size
131 r_y = int(y) / grid_size * grid_size
132
133
134 pos = rect.get(r_x, {}).get(r_y, set())
135 pos = pos | self.free_obj
136
137
138 top = bottom = left = right = False
139
140 if abs(r_y - y) < radius:
141
142 pos.update(rect.get(r_x, {}).get(r_y - grid_size, set()))
143 top = True
144
145 if abs(r_y - y + grid_size) < radius:
146
147 pos.update(rect.get(r_x, {}).get(r_y + grid_size, set()))
148 bottom = True
149
150 if abs(r_x - x) < radius:
151
152 pos.update(rect.get(r_x - grid_size, {}).get(r_y, set()))
153 left = True
154
155 if abs(r_x - x + grid_size) < radius:
156
157 pos.update(rect.get(r_x + grid_size, {}).get(r_y, set()))
158 right = True
159
160
161
162 if left and top:
163
164 pos.update(rect.get(r_x - grid_size, {}).get(r_y - grid_size, set()))
165
166 if top and right:
167
168 pos.update(rect.get(r_x + grid_size, {}).get(r_y - grid_size, set()))
169
170 if bottom and left:
171
172 pos.update(rect.get(r_x - grid_size, {}).get(r_y + grid_size, set()))
173
174 if bottom and right:
175
176 pos.update(rect.get(r_x + grid_size, {}).get(r_y + grid_size, set()))
177
178
179 re2 = set()
180 for obj in pos:
181 if obj.collide_circle(x, y, radius):
182 re2.add(obj)
183 return re2
184
186 """Checks all registered walkable objects for collision that are
187 in grid segments which collide with the specified circle.
188
189 This method first determines all concerned grid segments and finally
190 only collides objects located in these segments. This implementation
191 uses Line.collide to determine neighbor segments by colliding circle
192 with segment boundaries."""
193
194 grid_size = self.grid_size
195 rect = self.grid
196 assert radius <= grid_size
197
198
199
200 r_x = int(x) / grid_size * grid_size
201 r_y = int(y) / grid_size * grid_size
202
203
204 pos = rect.get(r_x, {}).get(r_y, set())
205 pos = pos | self.free_obj
206
207
208 top_line = Line(r_x, r_y, r_x + grid_size, r_y)
209 left_line = Line(r_x, r_y, r_x, r_y + grid_size)
210 right_line = Line(r_x + grid_size, r_y, r_x + grid_size, r_y + grid_size)
211 bottom_line = Line(r_x, r_y + grid_size, r_x + grid_size, r_y + grid_size)
212
213 top_line_col = top_line.collide_circle(x, y, radius)
214 left_line_col = left_line.collide_circle(x, y, radius)
215 right_line_col = right_line.collide_circle(x, y, radius)
216 bottom_line_col = bottom_line.collide_circle(x, y, radius)
217
218
219 if top_line_col and left_line_col:
220
221 pos.update(rect.get(r_x - grid_size, {}).get(r_y - grid_size, set()))
222
223 if top_line_col:
224
225 pos.update(rect.get(r_x, {}).get(r_y - grid_size, set()))
226
227 if top_line_col and right_line_col:
228
229 pos.update(rect.get(r_x + grid_size, {}).get(r_y - grid_size, set()))
230
231 if left_line_col:
232
233 pos.update(rect.get(r_x - grid_size, {}).get(r_y, set()))
234
235 if right_line_col:
236
237 pos.update(rect.get(r_x + grid_size, {}).get(r_y, set()))
238
239 if left_line_col and bottom_line_col:
240
241 pos.update(rect.get(r_x - grid_size, {}).get(r_y + grid_size, set()))
242
243 if bottom_line_col:
244
245 pos.update(rect.get(r_x, {}).get(r_y + grid_size, set()))
246
247 if right_line_col and bottom_line_col:
248
249 pos.update(rect.get(r_x + grid_size, {}).get(r_y + grid_size, set()))
250
251
252 re2 = set()
253 for obj in pos:
254 if obj.collide_circle(x, y, radius):
255 re2.add(obj)
256 return re2
257
258
259 collide_circle = collide_circle_impl2
260
261
263 """Checks all registered walkable objects for a collision with a given polygon.
264 @status: not implemented"""
265 raise NotImplementedError
266 re = set()
267 for obj in self:
268 if obj.collide_circle(corners):
269 re.add(obj)
270 return re
271
273 """Checks all registered walkable objects for a collision with a given rectangle."""
274 re = set()
275 for obj in self.obj | self.free_obj:
276 if obj.collide_rectangle(x_min, y_min, x_max, y_max):
277 re.add(obj)
278 return re
279
280
282 """A collidable rectangle. Not fully implemented.
283 @status: not completely implemented
284 @author: P. Tute"""
286 """initializes the collidable rectangle.
287
288 x0, y0 define south west point of rectangle,
289 x1, y1 define north east point of rectangle."""
290 self.x0 = x0
291 self.y0 = y0
292 self.x1 = x1
293 self.y1 = y1
294
296 """Not implemented! Checks if this rectangle collides with the given circle.
297 @status: not implemented"""
298 raise NotImplementedError
299
301 """Not implemented! Checks if this rectangle collides with the given rectangle.
302 @status: not implemented"""
303 raise NotImplementedError
304
306 """Not implemented! Checks if this rectangle collides with the given polygon.
307 @status: not implemented"""
308 raise NotImplementedError
309
310
312 """A collidable line.
313 @author: P. Tute"""
314 - def __init__(self, x_start, y_start, x_end, y_end):
315 """initializes the collidable Line.
316
317 x_start, y_start define starting point,
318 x_end, y_end define ending point."""
319 self.x_start = x_start
320 self.y_start = y_start
321 self.x_end = x_end
322 self.y_end = y_end
323
325 """Yields the start and end point coordinates (x, y) of the Line."""
326 yield self.x_start, self.y_start
327 yield self.x_end, self.y_end
328
330 """Finds the point on this line closest to a given point.
331
332 Based on http://www.alecjacobson.com/weblog/?p=1486"""
333
334 x1 = self.x_end - self.x_start
335 y1 = self.y_end - self.y_start
336
337 squared_dist = x1 ** 2 + y1 ** 2
338
339
340 if squared_dist == 0:
341 return self.x_start, self.y_start
342
343
344 start_to_point_x = x - self.x_start
345 start_to_point_y = y - self.y_start
346
347
348 t = (start_to_point_x * x1 + start_to_point_y * y1) / float(squared_dist)
349 if t < 0.0:
350
351 return self.x_start, self.y_start
352 elif t > 1.0:
353
354 return self.x_end, self.y_end
355 else:
356
357 re_x = self.x_start + t * x1
358 re_y = self.y_start + t * y1
359 return re_x, re_y
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
397 """Checks if this line collides with a vertical line.
398
399 Returns a boolean and the intersection-coordinates."""
400
401 if self.x_start == self.x_end:
402 return False, 0, 0
403
404 if (self.x_start < x and self.x_end < x or
405 self.x_start > x and self.x_end > x):
406 return False, 0, 0
407
408 bottom_y = min(y1, y2)
409 top_y = max(y1, y2)
410 if (self.y_start > top_y and self.y_end > top_y or
411 self.y_start < bottom_y and self.y_end < bottom_y):
412 return False, 0, 0
413
414 if self.y_start == self.y_end:
415
416 return True, x, self.y_start
417 elif self.x_start < self.x_end:
418 p1 = [self.x_start, self.y_start]
419 p2 = [self.x_end, self.y_end]
420 else:
421 p1 = [self.x_end, self.y_end]
422 p2 = [self.x_start, self.y_start]
423 m = (p2[1] - p1[1]) / (p2[0] - p1[0])
424 b = p1[1] - m * p1[0]
425 y = m * x + b
426 return True, x, y
427
429 """Checks if this line collides with a horizontal line.
430
431 Returns a boolean and the intersection-coordinates."""
432
433 if self.y_start == self.y_end:
434 return False, 0, 0
435
436 if (self.y_start < y and self.y_end < y or
437 self.y_start > y and self.y_end > y):
438 return False, 0, 0
439
440 left_x = min(x1, x2)
441 right_x = max(x1, x2)
442 if (self.x_start < left_x and self.x_end < left_x or
443 self.x_start > right_x and self.x_end > right_x):
444 return False, 0, 0
445
446
447 if self.x_start == self.x_end:
448
449 return True, self.x_start, y
450 elif self.x_start < self.x_end:
451 p1 = [self.x_start, self.y_start]
452 p2 = [self.x_end, self.y_end]
453 else:
454 p1 = [self.x_end, self.y_end]
455 p2 = [self.x_start, self.y_start]
456 m = (p2[1] - p1[1]) / (p2[0] - p1[0])
457 b = p1[1] - m * p1[0]
458 x = (y - b) / m
459 return True, x, y
460
462 """Checks if this line collides with the given circle."""
463
464
465 close_x, close_y = self.closest_to_point(x, y)
466 return sqrt((close_x - x) ** 2 + (close_y - y) ** 2) <= radius
467
469 """Checks if this line collides with the given rectangle.
470
471 The algorithm by Cohen & Sutherland is used.
472 It only works for axis-aligned rectangles.
473 http://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland"""
474 INSIDE = 0
475 LEFT = 1
476 RIGHT = 2
477 BOTTOM = 4
478 TOP = 8
479
480 def calculate_code(x, y):
481 code = INSIDE
482
483 if x < x_min:
484 code |= LEFT
485 elif x > x_max:
486 code |= RIGHT
487 if y < y_min:
488 code |= BOTTOM
489 elif y > y_max:
490 code |= TOP
491
492 return code
493
494 x0 = self.x_start
495 y0 = self.y_start
496 x1 = self.x_end
497 y1 = self.y_end
498
499 code0 = calculate_code(x0, y0)
500 code1 = calculate_code(x1, y1)
501
502 while True:
503 if not code0 | code1:
504 return True
505 elif code0 & code1:
506 return False
507 else:
508 code = code0 if code0 else code1
509 if code & TOP:
510 x = (x0 + (x1 - x0) *
511 (y_max - y0) / (y1 - y0))
512 y = y_max
513 elif code & BOTTOM:
514 x = (x0 + (x1 - x0) *
515 (y_min - y0) / (y1 - y0))
516 y = y_min
517 elif code & RIGHT:
518 y = (y0 + (y1 - y0) *
519 (x_max - x0) / (x1 - x0))
520 x = x_max
521 elif code & LEFT:
522 y = (y0 + (y1 - y0) *
523 (x_min - x0) / (x1 - x0))
524 x = x_min
525
526 if code == code0:
527 x0 = x
528 y0 = y
529 code0 = calculate_code(x0, y0)
530 elif code == code1:
531 x1 = x
532 y1 = y
533 code1 = calculate_code(x1, y1)
534
535
537 """Nothing Implemented! Checks if this line collides with the given polygon.
538 @status: not implemented"""
539 raise NotImplementedErrorss
540
542 return "<Line (%i,%i) to (%i, %i)>" % (self.x_start, self.y_start, self.x_end, self.y_end)
543
544
546 """A collidable polygon. Nothing implemented yet.
547 @status: not implemented
548 @author: P. Tute"""
550 """Checks if this polygon collides with the given circle.
551 @status: not implemented"""
552 raise NotImplementedError
553
555 """Checks if this polygon collides with the given polygon.
556 @status: not implemented"""
557 raise NotImplementedError
558
559
561 """A collidable point.
562 @author: P. Tute"""
564 """initializes the collidable point.
565
566 x and y are its coordinates."""
567 self.x = x
568 self.y = y
569
571 """Checks if this point collides with the given circle."""
572 x_dist = self.x - x
573 y_dist = self.y - y
574 dist = sqrt(x_dist ** 2 + y_dist ** 2)
575 return dist <= radius
576
578 """Checks if this point collides with the given rectangle."""
579 return (x_min <= self.x <= x_max and
580 y_min <= self.y <= y_max)
581
583 """Checks if this point collides with the given polygon.
584 @status: not implemented"""
585 raise NotImplementedError
586
588 """Yields the coordinates (x, y) of the point."""
589 yield self.x, self.y
590
591
593 """Empty, nothing implemented.
594 @status: not implemented
595 @author: F. Ludwig"""
597 raise NotImplementedError
598