-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathphysics.py
287 lines (220 loc) · 7.49 KB
/
physics.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
"""
Implements gravity and collision detection.
The World class implements the main logic.
The World contains a list of entities. All entities are rectangles that
cannot be rotated (axis-aligned bounding boxes).
There are 2 kinds of entities:
Static
Does not move. Does not have velocity. Does not collide with other static
bodies.
Dynamic:
Moves according to its velocity. Collides with static bodies. Does not
collide with other dynamic bodies.
Collisions are detected by sweeping the object's path a factor of its size
at a time; this should catch most collisions, but won't catch all of them,
especially for larger objects. When a collision happens, the sweep is
repeated/resolved with a sub-unit step.
Collisions are resolved first on the vertical axis, then on the horizontal one.
Known issues:
1. Objects' corners might tunnel through one another.
"""
from math import ceil
from dataclasses import dataclass, field
import pyxel
import click
@dataclass
class Vec2:
x: float = 0
y: float = 0
@dataclass
class Rect:
x: float = 0
y: float = 0
w: float = 0
h: float = 0
@property
def r(self):
return self.x + self.w
@property
def b(self):
return self.y + self.h
@property
def position(self):
return Vec2(self.x, self.y)
@position.setter
def position(self, value):
self.x, self.y = value.x, value.y
class Static(Rect): pass
@dataclass
class Dynamic(Rect):
velocity: Vec2 = field(default_factory=Vec2)
old_position: Vec2 = field(default_factory=Vec2)
had_collision: bool = False
@dataclass
class World:
things: list = field(default_factory=list)
gravity: Vec2 = field(default_factory=lambda: Vec2(0, 1))
def __post_init__(self):
self.things = list(self.things)
@property
def static_things(self):
for one in self.things:
if not hasattr(one, 'velocity'):
yield one
@property
def dynamic_things(self):
for one in self.things:
if hasattr(one, 'velocity'):
yield one
@classmethod
def check_collision(cls, one, two):
h_collision = one.x < two.r and one.r > two.x
v_collision = one.y < two.b and one.b > two.y
if h_collision and v_collision:
return True
return False
def check_dynamic_static_collision(self, one):
assert hasattr(one, 'velocity')
for two in self.static_things:
if self.check_collision(one, two):
return True
return False
def simulate(self, steps_per_frame=1, sweep=True):
for one in self.dynamic_things:
self.simulate_one(one, steps_per_frame, sweep)
def simulate_one(self, one, steps_per_frame, sweep):
sweep_steps = 1
if sweep:
new_x = one.x + one.velocity.x + self.gravity.x
new_y = one.y + one.velocity.y + self.gravity.y
length = ((one.x - new_x) ** 2 + (one.y - new_y) ** 2) ** .5
# hardcoded to half of the smallest dimension, could be better
sweep_length = min(one.w, one.h) / 2
sweep_steps = ceil(length / sweep_length)
dummy = Dynamic(one.x, one.y, one.w, one.h, Vec2(one.velocity.x, one.velocity.y))
self.simulate_one_step(dummy, steps_per_frame * sweep_steps)
if sweep and dummy.had_collision:
# hardcoded to half a pixel, could be better
sweep_length = .5
sweep_steps = ceil(length / sweep_length)
self.simulate_one_step(one, steps_per_frame * sweep_steps)
else:
one.position = dummy.position
one.velocity = dummy.velocity
one.had_collision = dummy.had_collision
def simulate_one_step(self, one, steps_per_frame):
had_collision = False
for _ in range(steps_per_frame):
had_collision = self.simulate_one_substep(one, 1 / steps_per_frame)
if had_collision:
one.had_collision = True
def simulate_one_substep(self, one, steps):
one.old_position = one.position
had_collision = False
one.velocity.x += self.gravity.x * steps
one.velocity.y += self.gravity.y * steps
one.y += one.velocity.y * steps
if self.check_dynamic_static_collision(one):
had_collision = True
one.y = one.old_position.y
one.velocity.y = 0
one.x += one.velocity.x * steps
if self.check_dynamic_static_collision(one):
had_collision = True
one.x = one.old_position.x
one.velocity.x = 0
return had_collision
@dataclass
class Scene:
name: str
offset: Vec2
world: World
SCENES = [
Scene('normal', Vec2(4, 4), World([
Dynamic(4, 10, 3, 3),
Static(0, 30, 16, 3),
])),
Scene('tunnel', Vec2(34, 4), World([
Dynamic(4, 0, 3, 3, velocity=Vec2(0, 2)),
Static(0, 30, 16, 3)
])),
Scene('hslide', Vec2(64, 4), World([
Dynamic(-2, 20, 3, 3, velocity=Vec2(2, 0)),
Static(0, 30, 16, 3),
])),
Scene('vslide', Vec2(94, 4), World([
Dynamic(0, 0, 3, 3, velocity=Vec2(2, 0)),
Static(12, 12, 3, 20),
])),
Scene('vsxvel', Vec2(124, 4), World([
Dynamic(0, 0, 3, 3),
Static(12, 12, 3, 20),
], Vec2(1, 1))),
Scene('tnlbig', Vec2(34, 56), World([
Dynamic(4, -4, 8, 8, velocity=Vec2(0, 20)),
Static(0, 30, 16, 3)
])),
]
DO_CLS = True
DO_CLIP = True
DO_SCENE_FRAME = False
STEPS_PER_FRAME = 1
SWEEP = True
def update():
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
for scene in SCENES:
scene.world.simulate(STEPS_PER_FRAME, SWEEP)
def draw():
if DO_CLS:
pyxel.cls(0)
status = ' '.join([
'--steps-per-frame {}'.format(STEPS_PER_FRAME),
'--sweep' if SWEEP else '--no-sweep',
])
pyxel.text(4, 108, status, 5)
for scene in SCENES:
pyxel.clip()
pyxel.text(scene.offset.x, scene.offset.y, scene.name, 5)
if DO_SCENE_FRAME:
pyxel.rectb(
scene.offset.x - 1,
scene.offset.y - 1,
24 + 1 - 1,
48 + 1 - 1,
1)
if DO_CLIP:
pyxel.clip(
scene.offset.x - 0,
scene.offset.y - 0,
24 + 0 - 1,
48 + 0 - 1,
)
for thing in scene.world.things:
if hasattr(thing, 'velocity'):
color = 2 # purple
else:
color = 1 # blue
if getattr(thing, 'had_collision', False):
color = 8 # red
pyxel.rectb(scene.offset.x + round(thing.x),
scene.offset.y + round(thing.y),
round(thing.w),
round(thing.h),
color)
@click.command()
@click.option('-f', '--fps', type=int, default=4, show_default=True)
@click.option('-s', '--steps-per-frame', type=int, default=1, show_default=True)
@click.option('--sweep/--no-sweep', default=True, show_default=True)
@click.option('--cls/--no-cls', default=False, show_default=True)
@click.option('--clip/--no-clip', default=True, show_default=True)
def main(fps, cls, clip, steps_per_frame, sweep):
global DO_CLS, DO_CLIP, STEPS_PER_FRAME, SWEEP
DO_CLS = cls
DO_CLIP = clip
STEPS_PER_FRAME = steps_per_frame
SWEEP = sweep
pyxel.init(160, 120, fps=fps)
pyxel.run(update, draw)
if __name__ == '__main__':
main()