0
# Physics World Management (Space)
1
2
The `Space` class is the central physics world that contains and manages all bodies, shapes, and constraints. It handles collision detection, constraint solving, gravity, and time integration.
3
4
## Class Definition
5
6
```python { .api }
7
class Space:
8
"""
9
Spaces are the basic unit of simulation. You add rigid bodies, shapes
10
and joints to it and then step them all forward together through time.
11
12
A Space can be copied and pickled. Note that any post step callbacks are
13
not copied. Also note that some internal collision cache data is not copied,
14
which can make the simulation a bit unstable the first few steps of the
15
fresh copy.
16
"""
17
18
def __init__(self, threaded: bool = False) -> None:
19
"""
20
Create a new instance of the Space.
21
22
Args:
23
threaded: If True, enables multi-threaded simulation (not available on Windows).
24
Even with threaded=True, you must set Space.threads=2 to use more than one thread.
25
"""
26
```
27
28
## Core Properties
29
30
### Physics Configuration
31
32
```python { .api }
33
# Solver accuracy vs performance
34
space.iterations: int = 10
35
"""
36
Number of solver iterations per step. Higher values increase accuracy but use more CPU.
37
Default 10 is sufficient for most games.
38
"""
39
40
# Global forces
41
space.gravity: Vec2d = Vec2d(0, 0)
42
"""
43
Global gravity vector applied to all dynamic bodies.
44
Can be overridden per-body with custom velocity integration functions.
45
"""
46
47
space.damping: float = 1.0
48
"""
49
Global velocity damping factor. 0.9 means bodies lose 10% velocity per second.
50
1.0 = no damping. Can be overridden per-body.
51
"""
52
```
53
54
### Sleep Parameters
55
56
```python { .api }
57
space.idle_speed_threshold: float = 0
58
"""
59
Speed threshold for a body to be considered idle for sleeping.
60
Default 0 means space estimates based on gravity.
61
"""
62
63
space.sleep_time_threshold: float = float('inf')
64
"""
65
Time bodies must remain idle before falling asleep.
66
Default infinity disables sleeping. Set to enable sleeping optimization.
67
"""
68
```
69
70
### Collision Tuning
71
72
```python { .api }
73
space.collision_slop: float = 0.1
74
"""
75
Amount of overlap between shapes that is allowed for stability.
76
Set as high as possible without visible overlapping.
77
"""
78
79
space.collision_bias: float # ~0.002 (calculated)
80
"""
81
How fast overlapping shapes are pushed apart (0-1).
82
Controls percentage of overlap remaining after 1 second.
83
Rarely needs adjustment.
84
"""
85
86
space.collision_persistence: float = 3.0
87
"""
88
Number of frames collision solutions are cached to prevent jittering.
89
Rarely needs adjustment.
90
"""
91
```
92
93
### Threading
94
95
```python { .api }
96
space.threads: int = 1
97
"""
98
Number of threads for simulation (max 2, only if threaded=True).
99
Default 1 maintains determinism. Not supported on Windows.
100
"""
101
```
102
103
### Read-Only Properties
104
105
```python { .api }
106
space.current_time_step: float
107
"""Current or most recent timestep from Space.step()"""
108
109
space.static_body: Body
110
"""Built-in static body for attaching static shapes"""
111
112
space.shapes: KeysView[Shape]
113
"""View of all shapes in the space"""
114
115
space.bodies: KeysView[Body]
116
"""View of all bodies in the space"""
117
118
space.constraints: KeysView[Constraint]
119
"""View of all constraints in the space"""
120
```
121
122
## Object Management
123
124
### Adding Objects
125
126
```python { .api }
127
def add(self, *objs: Union[Body, Shape, Constraint]) -> None:
128
"""
129
Add one or many shapes, bodies or constraints to the space.
130
131
Objects can be added even from collision callbacks - they will be
132
added at the end of the current step.
133
134
Args:
135
*objs: Bodies, shapes, and/or constraints to add
136
137
Example:
138
space.add(body, shape, constraint)
139
space.add(body)
140
space.add(shape1, shape2, joint)
141
"""
142
```
143
144
### Removing Objects
145
146
```python { .api }
147
def remove(self, *objs: Union[Body, Shape, Constraint]) -> None:
148
"""
149
Remove one or many shapes, bodies or constraints from the space.
150
151
Objects can be removed from collision callbacks - removal happens
152
at the end of the current step or removal call.
153
154
Args:
155
*objs: Objects to remove
156
157
Note:
158
When removing bodies, also remove attached shapes and constraints
159
to avoid dangling references.
160
161
Example:
162
space.remove(body, shape)
163
space.remove(constraint)
164
"""
165
```
166
167
## Simulation Control
168
169
### Time Stepping
170
171
```python { .api }
172
def step(self, dt: float) -> None:
173
"""
174
Advance the simulation by the given time step.
175
176
Using fixed time steps is highly recommended for stability and
177
contact persistence efficiency.
178
179
Args:
180
dt: Time step in seconds
181
182
Example:
183
# 60 FPS with substeps for accuracy
184
steps = 5
185
for _ in range(steps):
186
space.step(1/60.0/steps)
187
"""
188
```
189
190
### Spatial Index Optimization
191
192
```python { .api }
193
def use_spatial_hash(self, dim: float, count: int) -> None:
194
"""
195
Switch from default bounding box tree to spatial hash indexing.
196
197
Spatial hash can be faster for large numbers (1000s) of same-sized objects
198
but requires tuning and is usually slower for varied object sizes.
199
200
Args:
201
dim: Hash cell size - should match average collision shape size
202
count: Minimum hash table size - try ~10x number of objects
203
204
Example:
205
# For 500 objects of ~50 pixel size
206
space.use_spatial_hash(50.0, 5000)
207
"""
208
209
def reindex_shape(self, shape: Shape) -> None:
210
"""Update spatial index for a single shape after manual position changes"""
211
212
def reindex_shapes_for_body(self, body: Body) -> None:
213
"""Update spatial index for all shapes on a body after position changes"""
214
215
def reindex_static(self) -> None:
216
"""Update spatial index for all static shapes after moving them"""
217
```
218
219
## Spatial Queries
220
221
### Point Queries
222
223
```python { .api }
224
def point_query(
225
self,
226
point: tuple[float, float],
227
max_distance: float,
228
shape_filter: ShapeFilter
229
) -> list[PointQueryInfo]:
230
"""
231
Query for shapes near a point within max_distance.
232
233
Args:
234
point: Query point (x, y)
235
max_distance: Maximum distance to search
236
- 0.0: point must be inside shapes
237
- negative: point must be certain depth inside shapes
238
shape_filter: Collision filter to apply
239
240
Returns:
241
List of PointQueryInfo with shape, point, distance, gradient
242
243
Example:
244
hits = space.point_query((100, 200), 50, pymunk.ShapeFilter())
245
for hit in hits:
246
print(f"Hit {hit.shape} at distance {hit.distance}")
247
"""
248
249
def point_query_nearest(
250
self,
251
point: tuple[float, float],
252
max_distance: float,
253
shape_filter: ShapeFilter
254
) -> Optional[PointQueryInfo]:
255
"""
256
Query for the single nearest shape to a point.
257
258
Returns None if no shapes found within max_distance.
259
Same parameters and behavior as point_query but returns only closest hit.
260
"""
261
```
262
263
### Line Segment Queries
264
265
```python { .api }
266
def segment_query(
267
self,
268
start: tuple[float, float],
269
end: tuple[float, float],
270
radius: float,
271
shape_filter: ShapeFilter
272
) -> list[SegmentQueryInfo]:
273
"""
274
Query for shapes intersecting a line segment with thickness.
275
276
Args:
277
start: Segment start point (x, y)
278
end: Segment end point (x, y)
279
radius: Segment thickness radius (0 for infinitely thin line)
280
shape_filter: Collision filter to apply
281
282
Returns:
283
List of SegmentQueryInfo with shape, point, normal, alpha
284
285
Example:
286
# Raycast from (0,0) to (100,100)
287
hits = space.segment_query((0, 0), (100, 100), 0, pymunk.ShapeFilter())
288
for hit in hits:
289
print(f"Hit {hit.shape} at {hit.point} with normal {hit.normal}")
290
"""
291
292
def segment_query_first(
293
self,
294
start: tuple[float, float],
295
end: tuple[float, float],
296
radius: float,
297
shape_filter: ShapeFilter
298
) -> Optional[SegmentQueryInfo]:
299
"""
300
Query for the first shape hit by a line segment.
301
302
Returns None if no shapes intersected.
303
Same parameters as segment_query but returns only the first/closest hit.
304
"""
305
```
306
307
### Area and Shape Queries
308
309
```python { .api }
310
def bb_query(self, bb: BB, shape_filter: ShapeFilter) -> list[Shape]:
311
"""
312
Query for shapes overlapping a bounding box.
313
314
Args:
315
bb: Bounding box to query
316
shape_filter: Collision filter to apply
317
318
Returns:
319
List of shapes overlapping the bounding box
320
321
Example:
322
bb = pymunk.BB(left=0, bottom=0, right=100, top=100)
323
shapes = space.bb_query(bb, pymunk.ShapeFilter())
324
"""
325
326
def shape_query(self, shape: Shape) -> list[ShapeQueryInfo]:
327
"""
328
Query for shapes overlapping the given shape.
329
330
Args:
331
shape: Shape to test overlaps with
332
333
Returns:
334
List of ShapeQueryInfo with overlapping shapes and contact info
335
336
Example:
337
test_shape = pymunk.Circle(None, 25, (100, 100))
338
overlaps = space.shape_query(test_shape)
339
for overlap in overlaps:
340
print(f"Overlapping with {overlap.shape}")
341
"""
342
```
343
344
## Collision Handling
345
346
### Collision Callbacks
347
348
```python { .api }
349
def on_collision(
350
self,
351
collision_type_a: Optional[int] = None,
352
collision_type_b: Optional[int] = None,
353
begin: Optional[Callable] = None,
354
pre_solve: Optional[Callable] = None,
355
post_solve: Optional[Callable] = None,
356
separate: Optional[Callable] = None,
357
data: Any = None
358
) -> None:
359
"""
360
Set callbacks for collision handling between specific collision types.
361
362
Args:
363
collision_type_a: First collision type (None matches any)
364
collision_type_b: Second collision type (None matches any)
365
begin: Called when shapes first touch
366
pre_solve: Called before collision resolution
367
post_solve: Called after collision resolution
368
separate: Called when shapes stop touching
369
data: User data passed to callbacks
370
371
Callback Signature:
372
def callback(arbiter: Arbiter, space: Space, data: Any) -> bool:
373
# Return True to process collision, False to ignore
374
return True
375
376
Example:
377
def player_enemy_collision(arbiter, space, data):
378
player_shape, enemy_shape = arbiter.shapes
379
print("Player hit enemy!")
380
return True # Process collision normally
381
382
space.on_collision(
383
collision_type_a=PLAYER_TYPE,
384
collision_type_b=ENEMY_TYPE,
385
begin=player_enemy_collision
386
)
387
"""
388
```
389
390
### Post-Step Callbacks
391
392
```python { .api }
393
def add_post_step_callback(
394
self,
395
func: Callable,
396
key: Hashable,
397
*args,
398
**kwargs
399
) -> None:
400
"""
401
Add a callback to be called after the current simulation step.
402
403
Useful for making changes that can't be done during collision callbacks,
404
like adding/removing objects or changing properties.
405
406
Args:
407
func: Function to call after step completes
408
key: Unique key for this callback (prevents duplicates)
409
*args, **kwargs: Arguments passed to func
410
411
Example:
412
def remove_object(space, obj):
413
space.remove(obj)
414
415
# Schedule removal after current step
416
space.add_post_step_callback(remove_object, "remove_ball", ball)
417
"""
418
```
419
420
## Debug Visualization
421
422
```python { .api }
423
def debug_draw(self, options: SpaceDebugDrawOptions) -> None:
424
"""
425
Draw debug visualization of the space using provided draw options.
426
427
Args:
428
options: Debug drawing implementation (pygame_util, pyglet_util, etc.)
429
430
Example:
431
import pygame
432
import pymunk.pygame_util
433
434
screen = pygame.display.set_mode((800, 600))
435
draw_options = pymunk.pygame_util.DrawOptions(screen)
436
437
# Customize colors
438
draw_options.shape_outline_color = (255, 0, 0, 255) # Red outlines
439
440
space.debug_draw(draw_options)
441
"""
442
```
443
444
## Usage Examples
445
446
### Basic Physics World
447
448
```python { .api }
449
import pymunk
450
451
# Create physics world
452
space = pymunk.Space()
453
space.gravity = (0, -982) # Earth gravity
454
space.iterations = 15 # Higher accuracy
455
456
# Create ground
457
ground = space.static_body
458
ground_shape = pymunk.Segment(ground, (0, 0), (800, 0), 5)
459
ground_shape.friction = 0.7
460
space.add(ground_shape)
461
462
# Create falling box
463
mass = 10
464
size = (50, 50)
465
moment = pymunk.moment_for_box(mass, size)
466
body = pymunk.Body(mass, moment)
467
body.position = 400, 300
468
469
shape = pymunk.Poly.create_box(body, size)
470
shape.friction = 0.7
471
shape.elasticity = 0.8
472
space.add(body, shape)
473
474
# Run simulation
475
dt = 1/60.0
476
for i in range(300): # 5 seconds
477
space.step(dt)
478
print(f"Position: {body.position}, Velocity: {body.velocity}")
479
```
480
481
### Collision Detection
482
483
```python { .api }
484
import pymunk
485
486
BALL_TYPE = 1
487
WALL_TYPE = 2
488
489
def ball_wall_collision(arbiter, space, data):
490
"""Handle ball hitting wall"""
491
impulse = arbiter.total_impulse
492
if abs(impulse) > 100: # Hard hit
493
print(f"Ball bounced hard! Impulse: {impulse}")
494
return True
495
496
space = pymunk.Space()
497
space.on_collision(BALL_TYPE, WALL_TYPE, begin=ball_wall_collision)
498
499
# Create ball with collision type
500
ball_body = pymunk.Body(1, pymunk.moment_for_circle(1, 0, 20))
501
ball_shape = pymunk.Circle(ball_body, 20)
502
ball_shape.collision_type = BALL_TYPE
503
ball_shape.elasticity = 0.9
504
505
# Create wall with collision type
506
wall_body = space.static_body
507
wall_shape = pymunk.Segment(wall_body, (0, 0), (800, 0), 5)
508
wall_shape.collision_type = WALL_TYPE
509
510
space.add(ball_body, ball_shape, wall_shape)
511
```
512
513
### Advanced Spatial Queries
514
515
```python { .api }
516
import pymunk
517
518
space = pymunk.Space()
519
# ... add objects to space ...
520
521
# Point query - find what's under mouse cursor
522
mouse_pos = (400, 300)
523
shapes_under_mouse = space.point_query(
524
mouse_pos, 0, pymunk.ShapeFilter()
525
)
526
527
if shapes_under_mouse:
528
print(f"Mouse over: {shapes_under_mouse[0].shape}")
529
530
# Raycast - line of sight check
531
start = (100, 100)
532
end = (700, 500)
533
line_of_sight = space.segment_query_first(
534
start, end, 0, pymunk.ShapeFilter()
535
)
536
537
if line_of_sight:
538
print(f"LOS blocked at {line_of_sight.point}")
539
else:
540
print("Clear line of sight")
541
542
# Area query - explosion radius
543
explosion_center = (400, 300)
544
explosion_radius = 100
545
explosion_bb = pymunk.BB.newForCircle(explosion_center, explosion_radius)
546
affected_shapes = space.bb_query(explosion_bb, pymunk.ShapeFilter())
547
548
for shape in affected_shapes:
549
# Apply explosion force
550
if shape.body.body_type == pymunk.Body.DYNAMIC:
551
direction = shape.body.position - explosion_center
552
force = direction.normalized() * 10000
553
shape.body.apply_impulse_at_world_point(force, explosion_center)
554
```
555
556
### Performance Optimization
557
558
```python { .api }
559
import pymunk
560
561
space = pymunk.Space(threaded=True) # Enable threading
562
space.threads = 2 # Use 2 threads
563
564
# For large numbers of similar-sized objects
565
if num_objects > 1000:
566
avg_object_size = 25 # Average collision shape size
567
space.use_spatial_hash(avg_object_size, num_objects * 10)
568
569
# Enable sleeping for better performance
570
space.sleep_time_threshold = 0.5 # Sleep after 0.5 seconds idle
571
572
# Optimize solver iterations vs accuracy
573
space.iterations = 8 # Reduce for better performance
574
575
# Use appropriate gravity and damping
576
space.gravity = (0, -981) # Realistic gravity
577
space.damping = 0.999 # Small amount of air resistance
578
```
579
580
The Space class provides complete control over physics simulation with efficient spatial queries, flexible collision handling, and performance optimization options suitable for games, simulations, and interactive applications.