source: skaapsteker/physics.py

Last change on this file was 629:59556235dec7, checked in by Jeremy Thurgood <firxen@…>, 12 years ago

Clean up some leftover mess from collision handling work.

File size: 12.9 KB
Line 
1"""Model of gravity, acceleration, velocities and collisions.
2
3 Works very closely with sprites/base.py.
4 """
5
6import time
7
8import pygame
9import pygame.draw
10import pygame.sprite
11from pygame.mask import from_surface
12
13from . import options
14from .constants import EPSILON
15from .utils import (cadd, csub, cmul, cdiv, cclamp, cabsmax, cint, cneg, cabs,
16 rect_projection)
17
18class Sprite(pygame.sprite.Sprite):
19
20 # physics attributes
21 mobile = True # whether the velocity may be non-zero
22 gravitates = True # whether gravity applies to the sprite
23 terminal_velocity = (900.0, 500.0) # maximum horizontal and vertial speeds (pixels / s)
24 bounce_factor = (0.95, 0.95) # bounce factor
25 mass = 1.0 # used for shared collisions and applying forces
26 friction_coeff = (0.99, 0.99) # friction factor
27
28 # collision attributes
29 # Sprite X collides with Y iff (X.collision_layer in Y.collides_with) and X.check_collides(Y)
30 # Collisions result in the colliding movement being partially backed out, a call to X.bounce(frac) and a call to X.collided(Y)
31 # X.bounce(frac) is only called for the first (as determined by backing out distance) collision in a multi-collision event
32 collision_layer = None # never collides with anything
33 collides_with = set() # nothing collides with this
34
35 # set to True to have .update() called once per tick (and have .collision_group set)
36 wants_updates = False
37
38 floor = False # We special case collisions with ground objects
39 block = False
40
41 def __init__(self, *args, **kwargs):
42 super(Sprite, self).__init__(*args, **kwargs)
43 self.on_solid = False
44 self.velocity = (0.0, 0.0)
45 self.rect = pygame.Rect(0, 0, 10, 10) # sub-classes should override
46 self.collide_rect = pygame.Rect(0, 0, 10, 10) # rectangle we use for collisions
47 self.floor_rect = self.collide_rect
48 self.image = pygame.Surface((10, 10))
49 self.image.fill((0, 0, 200))
50 self.collision_group = None
51 self._mask_cache = {} # image id -> collision bit mask
52
53 def init_pos(self):
54 self._float_pos = cadd(self.rect.center, 0.0) # To make it a float
55
56 def deltav(self, dv):
57 self.velocity = cadd(self.velocity, dv)
58
59 def get_vectors(self):
60 self.velocity = cclamp(self.velocity, self.terminal_velocity)
61 return (self.velocity, self._float_pos)
62
63 def update_position(self, float_pos):
64 self._float_pos = float_pos
65 old_pos = self.rect.center
66 self.rect.center = cint(float_pos)
67 displacement = csub(self.rect.center, old_pos)
68 self.collide_rect.move_ip(displacement)
69 self.floor_rect.move_ip(displacement)
70
71 def apply_velocity(self, dt):
72 velocity, pos = self.get_vectors()
73 new_pos = cadd(pos, cmul(velocity, dt))
74 self.update_position(new_pos)
75
76 def _check_mask(self):
77 mask = self._mask_cache.get(id(self.image))
78 if mask is None:
79 mask = self._mask_cache[id(self.image)] = from_surface(self.image)
80 self.mask = mask
81
82 def check_collides(self, other):
83 # check bitmasks for collision
84 self._check_mask()
85 other._check_mask()
86 return pygame.sprite.collide_mask(self, other)
87
88 def collided(self, other):
89 pass
90
91 def check_floors(self, floors):
92 """Trigger of the current set of floors"""
93 pass
94
95 def apply_friction(self):
96 self.velocity = cmul(self.velocity, self.friction_coeff)
97
98 def bounce(self, other, normal):
99 """Alter velocity after a collision.
100
101 other: sprite collided with
102 normal: unit vector (tuple) normal to the collision
103 surface.
104 """
105 bounce_factor = cadd(cmul(self.bounce_factor, other.bounce_factor), 1)
106 deltav = cmul(cneg(normal), cmul(self.velocity, bounce_factor))
107
108 if normal == (0, 1) and (other.floor or other.block) and self.velocity[1] > 0 and self.collide_rect.top < other.collide_rect.top:
109 # Colliding with the ground from above is special
110 self.on_solid = True
111 deltav = (deltav[0], -self.velocity[1])
112
113 if other.mobile:
114 total_mass = self.mass + other.mass
115 f_self = self.mass / total_mass
116 f_other = other.mass / total_mass
117
118 self.deltav(cmul(deltav, f_self))
119 self.deltav(cmul(cneg(deltav), f_other))
120 else:
121 self.deltav(deltav) # oof
122
123 def update(self):
124 pass # only called in wants_update = True
125
126 def check_collide_rect(self, new_collide_rect, new_rect, new_image):
127 if self.collision_group is None:
128 return True
129
130 # TODO: decide whether to throw out checking of existing
131 # collisions. Doesn't seem needed at the moment and takes
132 # time.
133 old_image = self.image
134 old_rect = self.rect
135 #rect_collides = self.collide_rect.colliderect
136 old_collisions = set()
137 #for other in self.collision_group:
138 # if rect_collides(other.collide_rect) \
139 # and self.check_collides(other):
140 # old_collisions.add(other)
141
142 self.image = new_image
143 self.rect = new_rect
144 new_rect_collides = new_collide_rect.colliderect
145 new_collisions = set()
146 for other in self.collision_group:
147 if new_rect_collides(other.collide_rect) \
148 and self.check_collides(other):
149 new_collisions.add(other)
150
151 self.image = old_image
152 self.rect = old_rect
153 return not bool(new_collisions - old_collisions)
154
155 def fix_bounds(self, bounds):
156 self.kill()
157
158
159class World(object):
160
161 GRAVITY = cmul((0.0, 9.8), 80.0) # pixels / s^2
162
163 def __init__(self, bounds):
164 self._all = pygame.sprite.LayeredUpdates()
165 self._mobiles = pygame.sprite.Group()
166 self._gravitators = pygame.sprite.Group()
167 self._updaters = pygame.sprite.Group()
168 self._actionables = pygame.sprite.Group()
169 self._actors = pygame.sprite.Group()
170 self._collision_groups = { None: pygame.sprite.Group() }
171 self._last_time = None
172 self._bounds = bounds
173
174 def freeze(self):
175 self._last_time = None
176
177 def thaw(self):
178 self._last_time = time.time()
179
180 def add(self, sprite):
181 sprite.init_pos()
182 self._all.add(sprite)
183 if sprite.mobile:
184 self._mobiles.add(sprite)
185 if sprite.gravitates:
186 self._gravitators.add(sprite)
187 if sprite.wants_updates:
188 self._updaters.add(sprite)
189 self._add_collision_group(sprite.collision_layer)
190 for layer in sprite.collides_with:
191 self._add_collision_group(layer)
192 self._collision_groups[layer].add(sprite)
193 if sprite.wants_updates:
194 self._updaters.add(sprite)
195 sprite.collision_group = self._collision_groups[sprite.collision_layer]
196 if getattr(sprite, 'player_action', None) is not None:
197 self._actionables.add(sprite)
198 if getattr(sprite, 'add_actionable', None) is not None:
199 self._actors.add(sprite)
200
201 def _add_collision_group(self, layer):
202 if layer in self._collision_groups:
203 return
204 self._collision_groups[layer] = pygame.sprite.Group()
205
206 def _backout_collisions(self, sprite, others, dt):
207 frac, normal, idx = 0.0, None, None
208 abs_v_x, abs_v_y = cabs(sprite.velocity)
209
210 # We only backout of "solide" collisions
211 if sprite.block:
212 for i, other in enumerate(others):
213 if other.block or other.floor:
214 clip = sprite.collide_rect.clip(other.collide_rect)
215 # TODO: avoid continual "if abs_v_? > EPSILON"
216 frac_x = clip.width / abs_v_x if abs_v_x > EPSILON else dt
217 frac_y = clip.height / abs_v_y if abs_v_y > EPSILON else dt
218 if frac_x > frac_y:
219 if frac_y > frac:
220 frac, normal, idx = frac_y, (0, 1), i
221 else:
222 if frac_x > frac:
223 frac, normal, idx = frac_x, (1, 0), i
224
225 if idx is not None:
226 # We can see no solide collisions now
227 sprite.apply_velocity(max(-1.1 * frac, -dt))
228 sprite.bounce(others[idx], normal)
229
230 for other in others:
231 sprite.collided(other)
232
233
234 def apply_gravity(self, dt):
235 dv = cmul(self.GRAVITY, dt)
236 for sprite in self._gravitators:
237 if not sprite.on_solid:
238 sprite.deltav(dv)
239
240 def apply_friction(self):
241 for sprite in self._mobiles:
242 sprite.apply_friction()
243
244 def handle_escaped_sprites(self):
245 inbound = self._bounds.colliderect
246 for sprite in self._mobiles:
247 if not inbound(sprite):
248 sprite.fix_bounds(self._bounds)
249
250 def get_dt(self):
251 now = time.time()
252 dt = now - self._last_time
253 self._last_time = now
254 return dt
255
256
257 # def collide_sprite(self, dt, sprite):
258 # sprite.apply_velocity(dt)
259 # sprite_collides = sprite.collide_rect.colliderect
260 # collisions = []
261 # for other in self._collision_groups[sprite.collision_layer]:
262 # if sprite_collides(other.collide_rect) \
263 # and sprite.check_collides(other):
264 # collisions.append(other)
265 # if collisions:
266 # self._backout_collisions(sprite, collisions, dt)
267 # contact_rect = pygame.Rect(
268 # (sprite.collide_rect.left, sprite.collide_rect.bottom),
269 # (sprite.collide_rect.width, 1))
270 # return contact_rect.colliderect
271
272
273 def get_sprite_collisions(self, dt, sprite):
274 sprite.apply_velocity(dt)
275 sprite_collides = sprite.collide_rect.colliderect
276 collisions = []
277 for other in self._collision_groups[sprite.collision_layer]:
278 if (sprite_collides(other.collide_rect)
279 and sprite.check_collides(other)):
280 collisions.append(other)
281 return collisions
282
283
284 def path_collide(self, dt, sprite):
285 dts = [dt/10] * 9
286 dts.append(dt - sum(dts))
287 dtf_acc = 0
288 collisions = []
289 for dtf in dts:
290 dtf_acc += dtf
291 collisions = self.get_sprite_collisions(dtf, sprite)
292 for col in collisions:
293 if sprite.block and (col.floor or col.block):
294 return collisions, dtf_acc
295 return collisions, dt
296
297
298 def collide_sprite(self, dt, sprite):
299 initial_pos = sprite._float_pos
300 collisions = self.get_sprite_collisions(dt, sprite)
301 escape_vector = (0, 0)
302 if collisions:
303 # If we've collided, reset and try again with smaller time increments
304 sprite.update_position(initial_pos)
305 collisions, dtf = self.path_collide(dt, sprite)
306 for col in collisions:
307 if sprite.block and (col.floor or col.block):
308 escape_vector = cabsmax(escape_vector, rect_projection(sprite.collide_rect, col.collide_rect))
309 sprite.collided(col)
310 sprite.update_position(cadd(sprite._float_pos, escape_vector))
311
312
313 def update_sprite_positions(self, dt):
314 # position update and collision check (do last)
315 for sprite in self._mobiles:
316 self.collide_sprite(dt, sprite)
317
318 # Are we currently in contact with the ground?
319 if not sprite.block:
320 continue
321 contact_rect = pygame.Rect(
322 cadd(sprite.collide_rect.bottomleft, (4, 0)),
323 (sprite.collide_rect.width - 8, 1))
324 floors = []
325 sprite.on_solid = False
326 for other in self._collision_groups[sprite.collision_layer]:
327 if (other.floor or other.block) and contact_rect.colliderect(other.floor_rect):
328 sprite.on_solid = True
329 if sprite.velocity[1] > 0:
330 sprite.velocity = (sprite.velocity[0], 0)
331 floors.append(other)
332 sprite.check_floors(floors)
333
334 def update(self):
335 if self._last_time is None:
336 self._last_time = time.time()
337 return
338
339 dt = self.get_dt()
340
341 self.apply_gravity(dt)
342 self.apply_friction()
343 self.handle_escaped_sprites()
344 self.update_sprite_positions(dt)
345 self._updaters.update()
346 self.handle_actions()
347
348 def handle_actions(self):
349 for sprite in self._actors:
350 actor_collide_rect = sprite.collide_rect.inflate((4, 4))
351 for other in self._actionables:
352 other_actor_collide_rect = other.collide_rect.inflate((4, 4))
353 if actor_collide_rect.colliderect(other_actor_collide_rect):
354 sprite.add_actionable(other)
355
356
357 def draw(self, surface):
358 self._all.draw(surface)
359 if options['debug_rects']:
360 for sprite in self._all:
361 sprite.draw_debug(surface)
Note: See TracBrowser for help on using the repository browser.