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.
- miniworlds_physics-2.0.0.0/PKG-INFO +22 -0
- miniworlds_physics-2.0.0.0/README.md +4 -0
- miniworlds_physics-2.0.0.0/miniworlds_physics/__init__.py +15 -0
- miniworlds_physics-2.0.0.0/miniworlds_physics/actor_physics.py +580 -0
- miniworlds_physics-2.0.0.0/miniworlds_physics/physics_world.py +227 -0
- miniworlds_physics-2.0.0.0/miniworlds_physics/physics_world_connector.py +162 -0
- miniworlds_physics-2.0.0.0/miniworlds_physics/physics_world_event_manager.py +44 -0
- miniworlds_physics-2.0.0.0/miniworlds_physics/physics_world_position_manager.py +90 -0
- miniworlds_physics-2.0.0.0/miniworlds_physics.egg-info/PKG-INFO +22 -0
- miniworlds_physics-2.0.0.0/miniworlds_physics.egg-info/SOURCES.txt +13 -0
- miniworlds_physics-2.0.0.0/miniworlds_physics.egg-info/dependency_links.txt +1 -0
- miniworlds_physics-2.0.0.0/miniworlds_physics.egg-info/requires.txt +1 -0
- miniworlds_physics-2.0.0.0/miniworlds_physics.egg-info/top_level.txt +1 -0
- miniworlds_physics-2.0.0.0/setup.cfg +4 -0
- miniworlds_physics-2.0.0.0/setup.py +37 -0
|
@@ -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,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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
miniworlds
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
miniworlds_physics
|
|
@@ -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
|
+
)
|