miniworlds-physics 2.0.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.1
2
+ Name: miniworlds_physics
3
+ Version: 2.0.0.0
4
+ Summary: Physics engine for miniworlds
5
+ Home-page: https://github.com/asbl/miniworlds
6
+ Download-URL: https://github.com/asbl/miniworlds
7
+ Author: Andreas Siebel
8
+ Author-email: andreas.siebel@it-teaching.de
9
+ License: OSI Approved :: MIT License
10
+ Keywords: games,education,mini-worlds,pygame
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Topic :: Education
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: miniworlds
18
+
19
+ Physics World
20
+ ======================
21
+
22
+ physics engine for miniworlds based on pymunk
@@ -0,0 +1,4 @@
1
+ Physics World
2
+ ======================
3
+
4
+ physics engine for miniworlds based on pymunk
@@ -0,0 +1,15 @@
1
+ import inspect
2
+ import os
3
+ import sys
4
+ from miniworlds_physics.physics_world import PhysicsWorld
5
+
6
+ # __import__('pkg_resources').declare_namespace(__name__)
7
+ current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
8
+ parent_dir = os.path.dirname(current_dir)
9
+ sys.path.insert(0, parent_dir)
10
+
11
+ __all__ = []
12
+
13
+
14
+ __all__.append(PhysicsWorld.__name__)
15
+ __all__.append("ABC")
@@ -0,0 +1,580 @@
1
+ import math
2
+ import sys
3
+ from typing import Optional, Union
4
+
5
+ import pymunk as pymunk_engine
6
+ import pymunk.pygame_util
7
+ from miniworlds.actors import actor as actor
8
+ from miniworlds.actors.shapes import shapes
9
+
10
+
11
+ class ActorPhysics:
12
+ """Defines physics-properties of a actor, used as my_actor.physics.attribute or my_actor.physics.method
13
+
14
+ Can only be used for actors on a PhysicsWorld.
15
+
16
+ Examples:
17
+
18
+ .. code-block:: python
19
+
20
+ from miniworlds import *
21
+
22
+ world = PhysicsWorld((800, 600))
23
+
24
+ a = Circle()
25
+ a.position = (75, 200)
26
+ a.color = (255,0,0)
27
+ a.physics.simulation = "simulated"
28
+ a.direction = 180
29
+ a.physics.shape_type = "circle"
30
+ a.impulse(45, 1500)
31
+
32
+ world.run()
33
+ """
34
+
35
+ def __init__(self, actor, world):
36
+ self.started: bool = False
37
+ self._body_type = pymunk.Body.DYNAMIC
38
+ self.world = world
39
+ self.actor: actor.Actor = actor
40
+ self.simulation: str = "simulated"
41
+ self._gravity: bool = False
42
+ self._stable: bool = False
43
+ self._can_move: bool = True
44
+ self._density: float = 10
45
+ self.moment: Optional[float] = None
46
+ self.damping = 1
47
+ self.max_velocity_x = math.inf
48
+ self._friction: float = 0.5
49
+ self._velocity_x: float = 0
50
+ self._velocity_y: float = 0
51
+ self._elasticity: float = 0.5
52
+ self._shape_type: str = "rect"
53
+ self._correct_angle: float = 90
54
+ self._body: Union[pymunk_engine.Body, None] = None
55
+ self._shape: Union[pymunk_engine.Shape, None] = None
56
+ self.dirty: int = 1
57
+ self.has_physics: bool = False
58
+ self.size = (1, 1) # scale factor for physics box model
59
+ self.joints = []
60
+
61
+ @staticmethod
62
+ def velocity_function(body, gravity, damping, dt):
63
+ pymunk.Body.update_velocity(
64
+ body, gravity, body.physics_property.damping * damping, dt
65
+ )
66
+ if (
67
+ body.physics_property.max_velocity_x != math.inf
68
+ and body.velocity[0] > body.physics_property.max_velocity_x
69
+ ):
70
+ body.velocity = body.physics_property.max_velocity_x, body.velocity[1]
71
+ if (
72
+ body.physics_property.max_velocity_x != math.inf
73
+ and body.velocity[0] < -body.physics_property.max_velocity_x
74
+ ):
75
+ body.velocity = -body.physics_property.max_velocity_x, body.velocity[1]
76
+
77
+ def join(self, other: "actor.Actor", type="pin"):
78
+ """joins two actors at their center points"""
79
+ if not hasattr(other, "physics"):
80
+ raise TypeError("Other actor has no attribute physics")
81
+ my_body = self._body
82
+ other_body = other.physics._body
83
+ pj = pymunk.PinJoint(my_body, other_body, (0, 0), (0, 0))
84
+ self.joints.append(pj)
85
+ self.world.space.add(pj)
86
+ return self.actor.position_manager.get_position(), other.position
87
+
88
+ def remove_join(self, other: "actor.Actor"):
89
+ """Remove a joint between two actors.
90
+
91
+ Removes a joint between two actors, if a joint exists.
92
+
93
+ Examples:
94
+
95
+ Add and remove a joint on key_down:
96
+
97
+ .. code-block:: python
98
+
99
+ import random
100
+ from miniworlds import *
101
+ world = PhysicsWorld((400, 200))
102
+ connected = False
103
+ line = None
104
+ anchor = Rectangle()
105
+ anchor.size = (20,20)
106
+ anchor.center = (100, 20)
107
+ anchor.physics.simulation = "manual"
108
+ other_side = Line((250,100),(500,200))
109
+ def add_line(obj1, obj2):
110
+ l = Line(obj1.center, obj2.center)
111
+ l.physics.simulation = None
112
+ @l.register
113
+ def act(self):
114
+ self.start_position = obj1.center
115
+ self.end_position = obj2.center
116
+ return l
117
+ c = Circle()
118
+ @c.register
119
+ def on_key_down(self, key):
120
+ global connected
121
+ global line
122
+ if not connected:
123
+ print("not connected")
124
+ self.physics.join(anchor)
125
+ line = add_line(self, anchor)
126
+ connected = True
127
+ else:
128
+ print("connected")
129
+ self.physics.remove_join(anchor)
130
+ line.remove()
131
+
132
+ world.run()
133
+
134
+
135
+ .. raw:: html
136
+
137
+ <video loop autoplay muted width=400>
138
+ <source src="../_static/jointsremove1.webm" type="video/webm">
139
+ <source src="../_static/jointsremove1.mp4" type="video/mp4">
140
+ Your browser does not support the video tag.
141
+ </video>
142
+ """
143
+ for join in self.joints:
144
+ if other.physics._body == join.b:
145
+ self.world.space.remove(join)
146
+
147
+ def _start(self):
148
+ """Starts the physics-simulation
149
+
150
+ Called in world-connector
151
+ """
152
+ if not self.started:
153
+ self.started = True
154
+ self._setup_physics_model() # After on_setup
155
+
156
+ def _get_pymunk_shape(self):
157
+ if self.shape_type.lower() == "rect":
158
+ shape = pymunk.Poly.create_box(
159
+ self._body,
160
+ (self.size[0] * self.actor.width, self.size[1] * self.actor.height),
161
+ 1, # small radius
162
+ )
163
+ elif self.shape_type.lower() == "circle":
164
+ shape = pymunk.Circle(
165
+ self._body,
166
+ self.size[0] * self.actor.width / 2,
167
+ (0, 0),
168
+ )
169
+ elif isinstance(self.actor, shapes.Line):
170
+ start = pymunk.pygame_util.from_pygame(
171
+ (0, -self.actor._length / 2), self.actor.world.image
172
+ )
173
+ end = pymunk.pygame_util.from_pygame(
174
+ (0, self.actor._length / 2), self.actor.world.image
175
+ )
176
+ shape = pymunk.Segment(self._body, start, end, self.actor.thickness)
177
+ else:
178
+ raise AttributeError("No shape set!")
179
+ return shape
180
+
181
+ def _setup_physics_model(self):
182
+ if (
183
+ self.dirty and self.actor.position_manager.get_position()
184
+ ): # if actor is on the world
185
+ # create body
186
+ self.has_physics = False
187
+ self._body = pymunk_engine.Body(body_type=self.body_type)
188
+ self._body.physics_property = self
189
+ self._body.moment = math.inf
190
+ self._body.velocity_func = self.velocity_function
191
+ # self._body.damping = self.damping
192
+ self._set_pymunk_position()
193
+ self._set_pymunk_direction()
194
+ self._body.size = (
195
+ self.size[0] * self.actor.width,
196
+ self.size[1] * self.actor.height,
197
+ )
198
+
199
+ # disable velocity for actors if actor has no gravity
200
+ if self.simulation == "static":
201
+ self._body.velocity_func = lambda body, gravity, damping, dt: None
202
+ else:
203
+ self._body.velocity = self.velocity_x, self._velocity_y
204
+ # Adds object to space
205
+ if self._simulation:
206
+ self._shape = self._get_pymunk_shape()
207
+ self._shape.density = self.density
208
+ self._shape.friction = self.friction
209
+ self._shape.elasticity = self.elasticity
210
+ self._shape.actor = self.actor
211
+ self._shape.collision_type = hash(self.actor.__class__.__name__) % (
212
+ (sys.maxsize + 1) * 2
213
+ )
214
+ self.world.space.add(self._body, self._shape)
215
+ if self.moment is not None:
216
+ self._body.moment = self.moment
217
+ if self.simulation == "static":
218
+ self.world.space.reindex_static()
219
+ self.dirty = 1
220
+ self.has_physics = True
221
+
222
+ def _set_pymunk_position(self):
223
+ pymunk_position = self.actor.center[0], self.actor.center[1]
224
+ self._body.position = pymunk.pygame_util.from_pygame(
225
+ pymunk_position, self.actor.world.image
226
+ )
227
+
228
+ def _set_pymunk_direction(self):
229
+ self._body.angle = (
230
+ self.actor.position_manager.get_pymunk_direction_from_miniworlds()
231
+ )
232
+
233
+ def _set_mwm_actor_position(self):
234
+ if self._body:
235
+ self.actor.center = pymunk.pygame_util.from_pygame(
236
+ self._body.position, self.actor.world.image
237
+ )
238
+ self.dirty = 0
239
+
240
+ def _set_mwm_actor_direction(self):
241
+ self.actor.position_manager.set_mwm_direction_from_pymunk()
242
+ self.dirty = 0
243
+
244
+ def reload(self):
245
+ """Removes actor from space and reloads physics_model"""
246
+ if self.started:
247
+ self.dirty = 1
248
+ # Remove shape and body from space
249
+ self._remove_from_space()
250
+ # Set new properties and reset to space
251
+ self._setup_physics_model()
252
+ else:
253
+ self.dirty = 1
254
+
255
+ def _remove_from_space(self):
256
+ if self._body:
257
+ for shape in list(self._body.shapes):
258
+ if shape in self.world.space.shapes:
259
+ self.world.space.remove(shape)
260
+ if self._body in self.world.space.bodies:
261
+ self.world.space.remove(self._body)
262
+
263
+ def remove(self):
264
+ """Removes an object from physics-space"""
265
+ self._remove_from_space()
266
+
267
+ @property
268
+ def simulation(self):
269
+ """Sets simulation type for actor (`static`, `manual`, `simulated` or `None`)
270
+
271
+ Sets simulation type for actor:
272
+
273
+ * `simulated`: Actor is fully simulated by physics engine.
274
+ * `manual`: Actor is not affected by gravity.
275
+ * `static`: Actor is not moved by physics engine, but actors can collide with actor.
276
+ * `None`: Actor is not moved by physics engine and other actors can't collide with actor.
277
+ """
278
+ return self._simulation
279
+
280
+ @simulation.setter
281
+ def simulation(self, value: Union[str, None]):
282
+ # Sets the simulation type
283
+ self._simulation = value
284
+ if not value:
285
+ self._is_rotatable = False
286
+ self._gravity = False
287
+ self._can_move = False
288
+ self._stable = True
289
+ elif value.lower() == "static":
290
+ self._is_rotatable = False
291
+ self._gravity = False
292
+ self._can_move = False
293
+ self._stable = True
294
+ self.density = 0
295
+ elif value.lower() == "manual":
296
+ self._is_rotatable = True
297
+ self._gravity = False
298
+ self._can_move = True
299
+ self._stable = True
300
+ elif value.lower() == "simulated":
301
+ self._is_rotatable = True
302
+ self._gravity = True
303
+ self._can_move = True
304
+ self._stable = True
305
+ self.dirty = 1
306
+ self.reload()
307
+
308
+ @property
309
+ def body(self):
310
+ return self._body
311
+
312
+ @body.setter
313
+ def body(self, value):
314
+ self._body = value
315
+
316
+ @property
317
+ def body_type(self):
318
+ """Returns body type of actor
319
+
320
+ Must not be used from outside - Use property simulation instead.
321
+ """
322
+ if self.simulation is None or self.simulation == "static":
323
+ return pymunk.Body.STATIC
324
+ elif self.simulation == "manual":
325
+ return pymunk.Body.KINEMATIC
326
+ else:
327
+ return pymunk.Body.DYNAMIC
328
+
329
+ @property
330
+ def size(self):
331
+ """Sets size of physics_object in relation to object
332
+
333
+ * 1: Physics object size equals actor size
334
+ * < 1: Physics object is smaller than actor
335
+ * > 1: Physics object is larger than actor.
336
+
337
+ .. warning::
338
+
339
+ Actor is re-added to physics space after this operation - Velocity and impulses are lost.
340
+ """
341
+ return self._size
342
+
343
+ @size.setter
344
+ def size(self, value: tuple):
345
+ self._size = value
346
+ self.dirty = 1
347
+ self.reload()
348
+
349
+ @property
350
+ def shape_type(self):
351
+ """Sets shape type of object:
352
+
353
+ Shape Types:
354
+ * "rect": Rectangle
355
+ * "circle": Circle
356
+
357
+ .. warning::
358
+
359
+ Actor is re-added to physics space after this operation - Velocity and impulses are lost.
360
+
361
+ Examples:
362
+
363
+ Demonstrate different shape types:
364
+
365
+ .. code-block:: python
366
+
367
+ from miniworlds import *
368
+
369
+ world = PhysicsWorld(600,300)
370
+ Line((0,100),(100,150))
371
+ t = Actor((0,50))
372
+ t.physics.shape_type = "rect"
373
+ Line((200,100),(300,150))
374
+ t = Actor((200,50))
375
+ t.physics.shape_type = "circle"
376
+ world.run()
377
+
378
+ .. raw:: html
379
+
380
+ <video loop autoplay muted width=400>
381
+ <source src="../_static/shape_types.webm" type="video/webm">
382
+ <source src="../_static/shape_types.mp4" type="video/mp4">
383
+ Your browser does not support the video tag.
384
+ </video>
385
+ """
386
+ return self._shape_type
387
+
388
+ @shape_type.setter
389
+ def shape_type(self, value: str):
390
+ self._shape_type = value
391
+ self.dirty = 1
392
+ self.reload()
393
+
394
+ @property
395
+ def friction(self):
396
+ """Sets friction of actor
397
+
398
+ .. warning::
399
+
400
+ Actor is re-added to physics space after this operation - Velocity and impulses are lost.
401
+
402
+ """
403
+ return self._friction
404
+
405
+ @friction.setter
406
+ def friction(self, value: float):
407
+ self._friction = value
408
+ self.dirty = 1
409
+ self.reload()
410
+
411
+ @property
412
+ def elasticity(self):
413
+ """Sets elasticity of actor
414
+
415
+ .. warning::
416
+
417
+ Actor is re-added to physics space after this operation - Velocity and impulses are lost.
418
+
419
+ """
420
+ return self._elasticity
421
+
422
+ @elasticity.setter
423
+ def elasticity(self, value: float):
424
+ self._elasticity = value
425
+ self.dirty = 1
426
+ self.reload()
427
+
428
+ @property
429
+ def density(self):
430
+ """Sets density of actor
431
+
432
+ .. warning::
433
+
434
+ Actor is re-added to physics space after this operation - Velocity and impulses are lost.
435
+
436
+ """
437
+ return self._density
438
+
439
+ @density.setter
440
+ def density(self, value: float):
441
+ self._density = value
442
+ self.dirty = 1
443
+ self.reload()
444
+
445
+ def _simulation_preprocess_actor(self):
446
+ """
447
+ Updates the physics model in every frame
448
+
449
+ Returns:
450
+
451
+ """
452
+ if (
453
+ self._body and not self._body.body_type == pymunk_engine.Body.STATIC
454
+ ) and self.dirty:
455
+ self._set_pymunk_position()
456
+ self._set_pymunk_direction()
457
+ self.world.space.reindex_shapes_for_body(self._body)
458
+ self.dirty = 0
459
+
460
+ def _simulation_postprocess_actor(self):
461
+ """
462
+ Reloads physics model from pygame data
463
+ """
464
+ if self.simulation and not math.isnan(self._body.position[0]):
465
+ self._set_mwm_actor_position()
466
+ self._set_mwm_actor_direction()
467
+ if self._body and not self._body.body_type == pymunk_engine.Body.STATIC:
468
+ self.velocity_x = self._body.velocity[0]
469
+ self.velocity_y = self._body.velocity[1]
470
+ if self.world.debug:
471
+ options = pymunk.pygame_util.DrawOptions(self.actor.world.image)
472
+ options.collision_point_color = (255, 20, 30, 40)
473
+ self.world.space.debug_draw(options)
474
+
475
+ @property
476
+ def velocity_x(self):
477
+ """Sets velocity in x-direction. Can be positive or negative.
478
+
479
+ Examples:
480
+
481
+ Move a actor left or right.
482
+
483
+ .. code-block:: python
484
+
485
+ def on_key_pressed_d(self):
486
+ self.physics.velocity_x = 50
487
+
488
+ def on_key_pressed_a(self):
489
+ self.physics.velocity_x = - 50
490
+
491
+ """
492
+ return self._velocity_x
493
+
494
+ @velocity_x.setter
495
+ def velocity_x(self, value: float):
496
+ self._velocity_x = value
497
+ if self._body:
498
+ self._body.velocity = value, self._body.velocity[1]
499
+
500
+ @property
501
+ def velocity_y(self):
502
+ """Sets velocity in y-direction"""
503
+ return self._velocity_y
504
+
505
+ @velocity_y.setter
506
+ def velocity_y(self, value: float):
507
+ self._velocity_y = value
508
+ if self._body:
509
+ self._body.velocity = self._body.velocity[0], value
510
+
511
+ @property
512
+ def is_rotatable(self):
513
+ """defines, if actor will be rotated by physics-engine."""
514
+ return self._is_rotatable
515
+
516
+ @is_rotatable.setter
517
+ def is_rotatable(self, value: bool):
518
+ self._is_rotatable = value
519
+ self.dirty = 1
520
+ self.reload()
521
+
522
+ def impulse_in_direction(self, direction: float, power: float):
523
+ """
524
+ Adds an impulse in actor-direction
525
+
526
+ Examples:
527
+
528
+ .. code-block:: python
529
+
530
+ from miniworlds import *
531
+
532
+ world = PhysicsWorld(300, 200)
533
+
534
+ rect = Rectangle((280,120), 20, 80)
535
+ rect.physics.simulation = "manual"
536
+ ball = Circle((50,50),20)
537
+
538
+ @rect.register
539
+ def act(self):
540
+ rect.x -= 1
541
+ if rect.x == 0:
542
+ rect.x = 280
543
+
544
+ @ball.register
545
+ def on_key_down(self, key):
546
+ self.physics.impulse_in_direction(0, 5000)
547
+ world.run()
548
+
549
+
550
+ Args:
551
+ power: The power-value of the impulse.
552
+ direction: pymunk direction
553
+ """
554
+ impulse = pymunk.Vec2d(1, 0)
555
+ impulse = impulse.rotated_degrees(
556
+ 360
557
+ - self.actor.position_manager.dir_to_unit_circle(
558
+ direction - self.actor.direction
559
+ )
560
+ )
561
+ impulse = power * 1000 * impulse.normalized()
562
+ self._body.apply_impulse_at_local_point(impulse)
563
+
564
+ def force_in_direction(self, direction: float, power: float):
565
+ """
566
+ Adds a force in given direction
567
+
568
+ Args:
569
+ power: The power-value of the force.
570
+ direction: pymunk direction
571
+ """
572
+ force = pymunk.Vec2d(1, 0)
573
+ force = force.rotated_degrees(
574
+ 360
575
+ - self.actor.position_manager.dir_to_unit_circle(
576
+ direction - self.actor.direction
577
+ )
578
+ )
579
+ force = power * 10000 * force.normalized()
580
+ self._body.apply_force_at_local_point(force, (0, 0))
@@ -0,0 +1,227 @@
1
+ import pymunk as pymunk_engine
2
+ from miniworlds import Line, World
3
+ from miniworlds.tools import actor_inspection
4
+ import miniworlds_physics.physics_world_event_manager as event_manager
5
+ import miniworlds_physics.physics_world_connector as world_connector
6
+
7
+
8
+ class PhysicsWorld(World):
9
+ """
10
+ A PhysicsWorld is a playing field on which objects follow physical laws.
11
+
12
+ The PhysicsWorld itself defines some values with which the physics engine can be influenced, e.g.
13
+ the gravity in the world.
14
+
15
+ All actors on a PhysicsWorld have an attribute ``actor.physics``, with which you can change the physical properties
16
+ of the object.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ columns: int = 40,
22
+ rows: int = 40,
23
+ ):
24
+ super().__init__(columns, rows)
25
+ self.gravity_x: float = 0
26
+ self.gravity_y: float = 900
27
+ self.debug: bool = False
28
+ self._accuracy = 1
29
+ self.space = pymunk_engine.Space()
30
+ self.space.gravity = self.gravity_x, self.gravity_y
31
+ self.space.iterations = 35
32
+ self.space.damping = 0.9
33
+ self.space.collision_persistence = 10
34
+ self._damping = 0
35
+ self.physics_actors = list()
36
+ self.touching_methods = set() # filled in actor_manager
37
+ self.separate_methods = set() # filled in actor_manager
38
+
39
+ def _create_event_manager(self):
40
+ return event_manager.PhysicsWorldEventManager(self)
41
+
42
+ @property
43
+ def accuracy(self):
44
+ """Sets number of physics-steps performed in each frame.
45
+
46
+ Default: 1
47
+ """
48
+ return self._accuracy
49
+
50
+ @accuracy.setter
51
+ def accuracy(self, value: int):
52
+ self._accuracy = value
53
+
54
+ @staticmethod
55
+ def _get_world_connector_class():
56
+ """needed by get_world_connector in parent class"""
57
+ return world_connector.PhysicsWorldConnector
58
+
59
+
60
+ def get_physics_collision_methods_for_actor(self, actor):
61
+ """Gets all collision methods for actor
62
+
63
+ :meta private:
64
+ """
65
+ # gets all method names
66
+ methods = [
67
+ method_name
68
+ for method_name in dir(actor)
69
+ if method_name.startswith("on_touching_")
70
+ or method_name.startswith("on_separation_from_")
71
+ ]
72
+ # get methods from method names
73
+ return [
74
+ getattr(actor, method_name)
75
+ for method_name in methods
76
+ if hasattr(actor, method_name) and callable(getattr(actor, method_name))
77
+ ]
78
+
79
+ def on_new_actor(self, actor):
80
+ print("new actor", actor, actor.physics.simulation)
81
+ if not actor.physics.simulation:
82
+ connector = world_connector.PhysicsWorldConnector(self, actor)
83
+ connector.remove_actor_from_physics()
84
+
85
+ def _act_all(self):
86
+ """Handles acting of actors - Calls the physics-simulation in each frame.
87
+
88
+ :meta private:
89
+ """
90
+ super()._act_all()
91
+ self.simulate_all_physics_actors()
92
+
93
+ def simulate_all_physics_actors(self):
94
+ """Iterates over all actors and process physics-simulation
95
+
96
+ Processes phyisics-simulation in three steps
97
+
98
+ * Convert miniworlds-position/direction to pymunk position/direction
99
+ * Simulate a step in physics-engine
100
+ * Convert pymunk position/direction to miniworlds position/direction
101
+
102
+ :meta private:
103
+ """
104
+ if len(self.physics_actors) > 0:
105
+ # pre-process
106
+ [
107
+ actor.physics._simulation_preprocess_actor()
108
+ for actor in self.physics_actors
109
+ ]
110
+ # simulate
111
+ steps = self.accuracy
112
+ for _ in range(steps):
113
+ # if self.physics.space is not None: - can be removed
114
+ self.space.step(1 / (60 * steps))
115
+ # post-process
116
+ [
117
+ actor.physics._simulation_postprocess_actor()
118
+ for actor in self.physics_actors
119
+ ]
120
+
121
+ @property
122
+ def gravity(self) -> tuple:
123
+ """Defines gravity in physics world.
124
+
125
+ Gravity is a 2-tuple with gravy in x-direction and y-direction.
126
+
127
+ Default gravity: x=0, y=500
128
+
129
+ Examples:
130
+
131
+ Get all actors at mouse position:
132
+
133
+ .. code-block:: python
134
+
135
+ world = PhysicsWorld(400,400)
136
+ world.gravity = (0, 0)
137
+ """
138
+ return self.gravity_x, self.gravity_y
139
+
140
+ @gravity.setter
141
+ def gravity(self, value: tuple):
142
+ self.gravity_x = value[0]
143
+ self.gravity_y = value[1]
144
+ self.space.gravity = self.gravity_x, self.gravity_y
145
+
146
+ @property
147
+ def damping(self):
148
+ """Amount of simple damping to apply to the space.
149
+
150
+ A value of 0.9 means that each body will lose 10% of its velocity per second. Defaults to 1.
151
+ """
152
+ return self.gravity_x, self.gravity_y
153
+
154
+ @damping.setter
155
+ def damping(self, value: tuple):
156
+ self._damping = value
157
+ self.space.damping = self._damping
158
+
159
+ def pymunk_touching_collision_listener(self, arbiter, space, data):
160
+ """Handles collisions - Handled by pymunk engine
161
+
162
+ :meta private:
163
+ """
164
+ # Translate pymunk variables to miniworlds variables.
165
+ # Arbiter contains the two colliding actors.
166
+ t1 = arbiter.shapes[0].actor
167
+ t2 = arbiter.shapes[1].actor
168
+ collision = dict()
169
+ # get touching methods, e.g. `on_touching_circle`
170
+ for method in self.touching_methods:
171
+ # _cls_search_string = method.__name__[len("on_touching_"):].lower() # Get class by method name
172
+ # filter_class = actor_class_inspection.ActorClassInspection(self).find_actor_class_by_classname(
173
+ # _cls_search_string
174
+ # )
175
+ # sets parameter for method
176
+ if method.__self__ == t1:
177
+ other = t2
178
+ else:
179
+ other = t1
180
+ # call instance method with correct parameters
181
+ # if isinstance(other, filter_class):
182
+ actor_inspection.ActorInspection(method.__self__).get_and_call_method(
183
+ method.__name__, [other, collision]
184
+ )
185
+ return True
186
+
187
+ def pymunk_separation_collision_listener(self, arbiter, space, data):
188
+ """Handles collisions - Handled by pymunk engine
189
+
190
+ :meta private:
191
+ """
192
+ # Translate pymunk variables to miniworlds variables.
193
+ # Arbiter contains the two colliding actors.
194
+ t1 = arbiter.shapes[0].actor
195
+ t2 = arbiter.shapes[1].actor
196
+ collision = dict()
197
+ # get touching methods, e.g. `on_touching_circle`
198
+ for method in self.separate_methods:
199
+ # _cls_search_string = method.__name__[len("on_separation_from_"):].lower() # Get class by method name
200
+ # filter_class = actor_class_inspection.ActorClassInspection(self).find_actor_class_by_classname(
201
+ # _cls_search_string
202
+ # )
203
+ # sets parameter for method
204
+ if method.__self__ == t1:
205
+ other = t2
206
+ else:
207
+ other = t1
208
+ # call instance method with correct parameters
209
+ # if isinstance(other, filter_class):
210
+ actor_inspection.ActorInspection(method.__self__).get_and_call_method(
211
+ method.__name__, [other, collision]
212
+ )
213
+ return True
214
+
215
+ def connect(self, actor1, actor2) -> "Line":
216
+ line = Line(actor1.center, actor2.center)
217
+ line.physics.simulation = None
218
+ line.border = 1
219
+ line.fill = True
220
+ line.color = (255, 0, 0, 100)
221
+
222
+ @line.register
223
+ def act(self):
224
+ self.start_position = actor1.center
225
+ self.end_position = actor2.center
226
+
227
+ return line
@@ -0,0 +1,162 @@
1
+ import miniworlds.worlds.manager.world_connector as world_connector
2
+ import miniworlds.worlds.manager.sensor_manager as sensor_manager
3
+ import miniworlds.tools.actor_class_inspection as actor_class_inspection
4
+ import miniworlds_physics.physics_world as world_mod
5
+ import miniworlds_physics.actor_physics as actor_physics
6
+ import miniworlds_physics.physics_world_position_manager as physics_position_manager
7
+ import miniworlds.actors.actor as actor_mod
8
+ import sys
9
+
10
+
11
+ class PhysicsWorldConnector(world_connector.WorldConnector):
12
+ count_actors = 0
13
+
14
+ def __init__(self, world: "world_mod.PhysicsWorld", actor: "actor_mod.Actor"):
15
+ super().__init__(world, actor)
16
+ self.world: "world_mod.PhysicsWorld" = world
17
+ self.actor.register(self.impulse, force=True)
18
+ self.actor.register(self.force, force=True)
19
+ self.actor.register(self.set_simulation, force=True)
20
+ self.actor.register(self.set_velocity_x, force=True)
21
+ self.actor.register(self.set_velocity_y, force=True)
22
+ self.actor.register(self.set_velocity, force=True)
23
+
24
+
25
+ def impulse(self, direction: float, power: int):
26
+ """this method will be registered to an Actor-Instance
27
+ """
28
+ self.position_manager.impulse(direction, power)
29
+
30
+ def force(self, direction: float, power: int):
31
+ self.position_manager.force(direction, power)
32
+
33
+ def set_simulation(self, simulation_type: str):
34
+ self.position_manager.set_simulation(simulation_type)
35
+
36
+ def set_velocity_x(self, value):
37
+ self.position_manager.set_velocity_x(value)
38
+
39
+ def set_velocity_y(self, value):
40
+ self.position_manager.set_velocity_y(value)
41
+
42
+ def set_velocity(self, value):
43
+ self.position_manager.set_velocity(value)
44
+ @staticmethod
45
+ def get_position_manager_class():
46
+ return physics_position_manager.PhysicsWorldPositionManager
47
+
48
+ @staticmethod
49
+ def get_sensor_manager_class():
50
+ return sensor_manager.SensorManager
51
+
52
+ def add_to_world(self, position):
53
+ # add actor.physics attribute with physics properties to actor
54
+ self.actor.physics = actor_physics.ActorPhysics(self.actor, self.actor.world)
55
+ if hasattr(self.actor, "_set_physics"):
56
+ self.actor._set_physics()
57
+ super().add_to_world(position)
58
+ self.register_all_physics_collision_managers_for_actor()
59
+ self.actor.physics._start()
60
+ self.world.physics_actors.append(self.actor)
61
+ if hasattr(self.actor, "on_begin_simulation"):
62
+ self.actor.on_begin_simulation()
63
+
64
+ def remove_actor_from_world(self):
65
+ super().remove_actor_from_world()
66
+ self.actor.physics._remove_from_space()
67
+ if self in self.physics_actors:
68
+ self.physics_actors.remove(self)
69
+ try:
70
+ self.world.physics_actors.remove(self.actor)
71
+ except ValueError:
72
+ pass # actor not in physics actors
73
+
74
+ def remove_actor_from_physics(self):
75
+ print("remove from physics", self.actor)
76
+ self.actor.physics._remove_from_space()
77
+ if self in self.world.physics_actors:
78
+ self.world.physics_actors.remove(self.actor)
79
+ try:
80
+ self.world.physics_actors.remove(self.actor)
81
+ except ValueError:
82
+ pass # actor not in physics actors
83
+
84
+ def register_all_physics_collision_managers_for_actor(self):
85
+ """Registers on__touching and on_separation-Methods to actor.
86
+ If new_class is set, only methods with new class (e.g. on_touching_new_class are set)
87
+
88
+ :meta private:
89
+ """
90
+ collision_methods = self.world.get_physics_collision_methods_for_actor(self.actor)
91
+ for method in collision_methods:
92
+ if method.__name__.startswith("on_touching_"):
93
+ self.register_touching_method(method)
94
+ elif method.__name__.startswith("on_separation_from_"):
95
+ self.register_separate_method(method)
96
+
97
+ def register_touching_method(self, method):
98
+ """
99
+ Registers on_touching_[class] method
100
+
101
+ :meta private:
102
+ """
103
+ event = "begin"
104
+ other_cls_name = method.__name__[len("on_touching_"):].lower()
105
+ other_cls = actor_class_inspection.ActorClassInspection(self).find_actor_class_by_classname(other_cls_name)
106
+ if self._register_physics_listener_method(method, event, other_cls):
107
+ self.world.touching_methods.add(method)
108
+
109
+ def register_separate_method(self, method):
110
+ """
111
+ Registers on_separation_from_[class] method
112
+
113
+ :meta private:
114
+ """
115
+ event = "separate"
116
+ other_cls_name = method.__name__[len("on_separation_from_"):].lower()
117
+ other_cls = actor_class_inspection.ActorClassInspection(self).find_actor_class_by_classname(other_cls_name)
118
+ if self._register_physics_listener_method(method, event, other_cls):
119
+ self.world.separate_methods.add(method)
120
+
121
+ def _register_physics_listener_method(self, method, event, other_cls):
122
+ """Registers a physics listener method. (on touching or on_separation.)
123
+ Called from register_touching_method and register_separate_method
124
+
125
+ :meta private:
126
+ """
127
+ actor_class_inspect = actor_class_inspection.ActorClassInspection(self)
128
+ all_actor_classes = actor_class_inspect.get_all_actor_classes()
129
+ if other_cls not in all_actor_classes:
130
+ return False
131
+ else:
132
+ subclasses_of_other_actor = actor_class_inspection.ActorClassInspection(other_cls).get_subclasses_for_cls()
133
+ for other_subcls in set(subclasses_of_other_actor).union({other_cls}):
134
+ # If you register a Collision with a Actor, collisions with subclasses of the actor
135
+ # are also registered
136
+ self._pymunk_register_collision_manager(method.__self__, other_subcls, event, method)
137
+ return True
138
+
139
+ def _pymunk_register_collision_manager(self, actor, other_class, event, method):
140
+ """Adds pymunk collision handler, which is evaluated by pymunk engine.
141
+
142
+ The event (begin, end) and the method (on_touching...) are added as data to the handler
143
+
144
+ Args:
145
+ actor: The actor
146
+ other_class: The class which should be detected by collision handler
147
+ event: The pymunk-event (begin or separate)
148
+ method: The method, e.g. on_touching_actor or on_separation_from_actor. Last part is a class name
149
+
150
+ :meta private:
151
+ """
152
+
153
+ space = self.world.space
154
+ actor_id = hash(actor.__class__.__name__) % ((sys.maxsize + 1) * 2)
155
+ other_id = hash(other_class.__name__) % ((sys.maxsize + 1) * 2)
156
+ handler = space.add_collision_handler(actor_id, other_id)
157
+ handler.data["method"] = getattr(actor, method.__name__)
158
+ handler.data["type"] = event
159
+ if event == "begin":
160
+ handler.begin = self.world.pymunk_touching_collision_listener
161
+ if event == "separate":
162
+ handler.separate = self.world.pymunk_separation_collision_listener
@@ -0,0 +1,44 @@
1
+ import miniworlds.tools.inspection as inspection
2
+ import miniworlds.worlds.manager.event_manager as event_manager
3
+ from miniworlds.actors import actor as actor_mod
4
+ from miniworlds.tools import actor_class_inspection
5
+ from miniworlds_physics import (
6
+ physics_world_connector as physics_world_connector_mod,
7
+ )
8
+
9
+
10
+ class PhysicsWorldEventManager(event_manager.EventManager):
11
+ """Adds on_touching and on separation events"""
12
+
13
+ @classmethod
14
+ def setup_event_list(cls):
15
+ super().setup_event_list()
16
+ touching_actor_methods = []
17
+ separation_actor_methods = []
18
+ for actor_cls in actor_class_inspection.ActorClassInspection(
19
+ actor_mod.Actor
20
+ ).get_subclasses_for_cls():
21
+ touching_actor_methods.append("on_touching_" + actor_cls.__name__.lower())
22
+ for actor_cls in actor_class_inspection.ActorClassInspection(
23
+ actor_mod.Actor
24
+ ).get_subclasses_for_cls():
25
+ separation_actor_methods.append(
26
+ "on_separation_from_" + actor_cls.__name__.lower()
27
+ )
28
+ cls.actor_class_events["on_touching"] = touching_actor_methods
29
+ cls.actor_class_events["on_separation"] = separation_actor_methods
30
+ cls.fill_event_sets()
31
+
32
+ def register_event(self, member, instance):
33
+ super().register_event(member, instance)
34
+ method = inspection.Inspection(instance).get_instance_method(member)
35
+ if member.startswith("on_touching_"):
36
+ connector = physics_world_connector_mod.PhysicsWorldConnector(
37
+ self.world, instance
38
+ )
39
+ connector.register_touching_method(method)
40
+ elif member.startswith("on_separation_from_"):
41
+ connector = physics_world_connector_mod.PhysicsWorldConnector(
42
+ self.world, instance
43
+ )
44
+ connector.register_separate_method(method)
@@ -0,0 +1,90 @@
1
+ from math import radians, degrees
2
+ from typing import Tuple
3
+
4
+ import miniworlds.worlds.manager.position_manager as position_manager
5
+ from miniworlds.base.exceptions import (
6
+ PhysicsSimulationTypeError,
7
+ )
8
+ from miniworlds.actors import actor as actor_mod
9
+
10
+
11
+ class PhysicsWorldPositionManager(position_manager.Positionmanager):
12
+ def __init__(self, actor: "actor_mod.Actor", world, position):
13
+ super().__init__(actor, world, position)
14
+
15
+ def set_position(
16
+ self, value: Tuple[float, float]
17
+ ) -> Tuple[float, float]:
18
+ pos = super().set_position(value)
19
+ if hasattr(self.actor, "physics"):
20
+ self.actor.physics.dirty = 1
21
+ return pos
22
+
23
+ def set_center(self, value):
24
+ pos = super().set_center(value)
25
+ if hasattr(self.actor, "physics"):
26
+ self.actor.physics.dirty = 1
27
+ return pos
28
+
29
+ def set_size(self, value, scale=False):
30
+ super().set_size(value, scale)
31
+ if hasattr(self.actor, "physics"):
32
+ self.actor.physics.dirty = 1
33
+
34
+ def move_to(self, position: Tuple[float, float]):
35
+ super().move_to(position)
36
+ if hasattr(self.actor, "physics"):
37
+ self.actor.physics.reload()
38
+
39
+ def get_direction(self):
40
+ return self._direction
41
+
42
+ def set_direction(self, value):
43
+ if hasattr(self.actor, "physics") and self.actor.physics.body:
44
+ pymunk_direction = self.get_pymunk_direction(value)
45
+ self.actor.physics.body.angle = pymunk_direction
46
+ super().set_direction((value + 360) % 360)
47
+ else:
48
+ super().set_direction(value)
49
+
50
+ def get_pymunk_direction_from_miniworlds(self):
51
+ mwm_direction = self._direction
52
+ return self.get_pymunk_direction(mwm_direction)
53
+
54
+ def get_pymunk_direction(self, value):
55
+ mwm_direction = (value + 360) % 360
56
+ direction = radians(mwm_direction)
57
+ return direction
58
+
59
+ def set_mwm_direction_from_pymunk(self):
60
+ pymunk_dir_in_degrees = degrees(self.actor.physics.body.angle)
61
+ mwm_direction = (pymunk_dir_in_degrees + 360) % 360
62
+ super().set_direction(mwm_direction)
63
+
64
+ def impulse(self, direction: float, power: int):
65
+ self.actor.physics.impulse_in_direction(direction, power)
66
+
67
+ def force(self, direction: float, power: int):
68
+ self.actor.physics.force_in_direction(direction, power)
69
+
70
+ def set_simulation(self, simulation_type: str):
71
+ if simulation_type in ["simulated", "manual", "static", None]:
72
+ self.actor.physics.simulation = simulation_type
73
+ self.actor.physics.reload()
74
+ else:
75
+ raise PhysicsSimulationTypeError()
76
+
77
+ def set_velocity_y(self, value):
78
+ self.actor.physics.velocity_y = value
79
+
80
+ def set_velocity_x(self, value):
81
+ self.actor.physics.velocity_x = value
82
+
83
+ def set_velocity(self, value):
84
+ self.actor.physics.velocity_x, self.actor.physics.velocity_y = (
85
+ value[0],
86
+ value[1],
87
+ )
88
+
89
+ def self_remove(self):
90
+ self.actor.physics._remove_from_space()
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.1
2
+ Name: miniworlds_physics
3
+ Version: 2.0.0.0
4
+ Summary: Physics engine for miniworlds
5
+ Home-page: https://github.com/asbl/miniworlds
6
+ Download-URL: https://github.com/asbl/miniworlds
7
+ Author: Andreas Siebel
8
+ Author-email: andreas.siebel@it-teaching.de
9
+ License: OSI Approved :: MIT License
10
+ Keywords: games,education,mini-worlds,pygame
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Topic :: Education
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: miniworlds
18
+
19
+ Physics World
20
+ ======================
21
+
22
+ physics engine for miniworlds based on pymunk
@@ -0,0 +1,13 @@
1
+ README.md
2
+ setup.py
3
+ miniworlds_physics/__init__.py
4
+ miniworlds_physics/actor_physics.py
5
+ miniworlds_physics/physics_world.py
6
+ miniworlds_physics/physics_world_connector.py
7
+ miniworlds_physics/physics_world_event_manager.py
8
+ miniworlds_physics/physics_world_position_manager.py
9
+ miniworlds_physics.egg-info/PKG-INFO
10
+ miniworlds_physics.egg-info/SOURCES.txt
11
+ miniworlds_physics.egg-info/dependency_links.txt
12
+ miniworlds_physics.egg-info/requires.txt
13
+ miniworlds_physics.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ miniworlds_physics
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,37 @@
1
+ from codecs import open
2
+ from os import path
3
+
4
+ from setuptools import setup, find_packages
5
+
6
+ here = path.abspath(path.dirname(__file__))
7
+
8
+ # Get the long description from the README file
9
+ with open(path.join(here, "README.md"), encoding="utf-8") as f:
10
+ long_description = f.read()
11
+
12
+ setup(
13
+ name="miniworlds_physics",
14
+ version="2.0.0.0",
15
+ description="Physics engine for miniworlds",
16
+ long_description=long_description,
17
+ long_description_content_type="text/markdown",
18
+ keywords=["games", "education", "mini-worlds", "pygame"], # arbitrary keywords
19
+ author="Andreas Siebel",
20
+ author_email="andreas.siebel@it-teaching.de",
21
+ url="https://github.com/asbl/miniworlds",
22
+ download_url="https://github.com/asbl/miniworlds",
23
+ license="OSI Approved :: MIT License",
24
+ classifiers=[
25
+ "License :: OSI Approved :: MIT License",
26
+ "Programming Language :: Python",
27
+ "Development Status :: 4 - Beta",
28
+ "Intended Audience :: Education",
29
+ "Topic :: Education",
30
+ ],
31
+ packages=find_packages(
32
+ exclude=["contrib", "docs", "tests", "examples"]
33
+ ), # Required
34
+ package_dir={"miniworlds_physics": "miniworlds_physics"},
35
+ install_requires=["miniworlds"],
36
+ include_package_data=True,
37
+ )