-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgravity_controller.py
More file actions
326 lines (266 loc) · 11.3 KB
/
gravity_controller.py
File metadata and controls
326 lines (266 loc) · 11.3 KB
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
"""
This is a module that implements a funny simulation called 'Gravity Controller'
using Python and the Pygame and Pymunk libraries.
In this simulation, the user is allowed to use the computer mouse to change the
magnitude and direction of the gravity of the Pymunk Space. In the center of
the screen, there is a static box with some dynamic balls inside. These balls
will move attracted by the gravity. In other words, the balls will move towards
the mouse position at each time step. Move your mouse to guide the balls.
Pygame is a Python library that serves as a versatile framework for developing
2D games and multimedia applications. It provides a straightforward and
efficient way to manage graphics, sound, user input, and basic collision
detection.
PyMunk is a Python library that extends Pygame by adding robust 2D physics
simulation capabilities, allowing developers to create realistic physics
interactions within their 2D games and simulations. It provides functionality
for creating and managing physics objects such as rigid bodies, shapes,
constraints, and handling complex collision detection and response.
PyMunk's integration with Pygame simplifies the process of combining graphics
with dynamic physics, making it a valuable tool for those seeking to develop
games or simulations with compelling and lifelike physics behavior.
KEYWORDS: Pygame, Pymunk, YAML, Simulations, Physics using Python.
"""
import os
import random
from typing import Any, Dict, Tuple
import pygame
import pymunk
import yaml
class Ball:
"""
Ball class. Object that is attracted towards the direction of
gravity generated by the computer mouse (at the computer mouse position).
Dynamic pymunk body, circular pymunk shape. All balls will be contained in
a BoundingBox.
The mass, radius, and color will vary depending on the instance.
"""
def __init__(
self,
center: Tuple[int, int],
mass: int,
radius: int,
color: Tuple[int, int, int],
):
"""
Initialize a Ball instance.
:param center: (x,y) initial coordinates of the ball
:param mass: mass of the Ball
:param radius: radius of the Ball (circle)
:param color: color in RGB format
"""
self.color = color # in RGB format
# Create the Pymunk Body of the Ball
self.body = pymunk.Body(mass=mass, moment=1)
self.body.position = center
# Create the Pymunk Shape (Circle) of the Ball
self.shape = pymunk.Circle(self.body, radius=radius)
def draw(self, screen: pygame.surface) -> None:
"""
Display the Ball on the given 'screen' (pygame surface)
:param screen: pygame surface where the Ball is displayed
:return: None
"""
pygame.draw.circle(
surface=screen,
color=self.color,
center=self.body.position,
radius=self.shape.radius
)
class BoundingBox:
"""
BoundingBox class. Square and static Box that will serve as a container for
the balls. The BoundingBox will be placed in the center of the screen.
The balls within the BoundingBox will be moved in the direction of the
gravity generated by the computer mouse, but they will not be allowed to
leave the box.
"""
def __init__(
self,
center: Tuple[int, int],
width: int,
height: int,
radius: int,
line_color: Tuple[int, int, int],
background_color: Tuple[int, int, int]
) -> None:
"""
Initialize a BoundingBox instance.
:param center: (x,y) coordinates of the BoundingBox center
:param width: width of the BoundingBox
:param height: height of the BoundingBox
:param radius: line width (contour)
:line_color: color of the contour, in RGB format
:background_color: color that fills the box, in RGB format
"""
self.center = center
self.width = width
self.height = height
self.radius = radius
self.line_color = line_color
self.background_color = background_color
# Define the (x,y) coordinates of the corners of the BoundingBox
tl = (center[0] - width//2, center[1] - height//2) # Top Left
tr = (center[0] + width//2, center[1] - height//2) # Top Right
bl = (center[0] - width//2, center[1] + height//2) # Bottom Left
br = (center[0] + width//2, center[1] + height//2) # Bottom Right
# Create a body for each of the four Segments (lines that form the Box)
self.body = (
pymunk.Body(mass=1, moment=1, body_type=pymunk.Body.STATIC),
pymunk.Body(mass=1, moment=1, body_type=pymunk.Body.STATIC),
pymunk.Body(mass=1, moment=1, body_type=pymunk.Body.STATIC),
pymunk.Body(mass=1, moment=1, body_type=pymunk.Body.STATIC)
)
# Create four Segments (shapes) that form the BoundingBox
self.shape = (
pymunk.Segment(body=self.body[0], a=tl, b=tr, radius=self.radius),
pymunk.Segment(body=self.body[1], a=tl, b=bl, radius=self.radius),
pymunk.Segment(body=self.body[2], a=tr, b=br, radius=self.radius),
pymunk.Segment(body=self.body[3], a=bl, b=br, radius=self.radius)
)
# Define the background as a Rectangle (pygame surface)
self.background_rect = pygame.Rect(tl[0], tl[1], width, height)
# order of arguments in pygame.Rect: left, top, width, height
def draw(self, screen: pygame.surface) -> None:
"""
Displays the Box on the given 'screen' (pygame surface)
:return: None
"""
# First, fill the rectangle (background as a pygame Rect)
pygame.draw.rect(
surface=screen,
color=self.background_color,
rect=self.background_rect
)
# Then, display the four contours (lines)
for segment in self.shape:
pygame.draw.line(
surface=screen,
color=self.line_color,
start_pos=segment.a,
end_pos=segment.b,
width=self.radius
)
class GravityController:
"""
GravityController class. This class manages the logic of the simulation.
It creates the pymunk space, and initializes a set of Balls inside a
BoundingBox instance. It also updates the gravity based on the current
position of the mouse, and displays all the elements on a pygame surface.
"""
def __init__(self) -> None:
"""
Initialize a GravityController instance.
"""
# Read the configuration file
self._config = self._get_config()
# Initialize the Pygame elements
pygame.init()
self._screen = pygame.display.set_mode(
(self._config['screen_width'], self._config['screen_height'])
)
pygame.display.set_caption(self._config['screen_caption'])
self._clock = pygame.time.Clock()
# Create a Pymunk Space
self.space = pymunk.Space()
# Note that the gravity will depend on the mouse position
# Create the Box and fill it with Balls
screen_center = (
self._config['screen_width'] // 2,
self._config['screen_height'] // 2
)
self.bounding_box = BoundingBox(
center=screen_center,
width=self._config['box_width'],
height=self._config['box_height'],
radius=self._config['box_line_width'],
line_color=self._config['box_line_color'],
background_color=self._config['box_background_color']
)
# Add the box (its pymunk Segments) to the pymunk space
for body, shape in zip(self.bounding_box.body, self.bounding_box.shape):
self.space.add(body, shape)
# Create a set of Balls inside the BoundingBox
self.balls_list = [] # list of the Ball instances
for _ in range(self._config['n_balls_inside_box']):
ball = Ball(
center=self.bounding_box.center, # inside the BoundingBox
mass=random.randrange(
start=self._config['min_mass'],
stop=self._config['max_mass']
),
radius=random.randrange(
start=self._config['min_radius'],
stop=self._config['max_radius']
),
color=random.choices(population=range(255), k=3)
)
self.balls_list.append(ball)
# Add the Balls to the pymunk space
for ball in self.balls_list:
self.space.add(ball.body, ball.shape)
@staticmethod
def _get_config() -> Dict[str, Any]:
"""
Read the configuration file and return it as a python dictionary.
The configuration file is called 'gravity_controller/config.yml'
:return: configuration dictionary
"""
this_file_path = os.path.abspath(__file__)
this_project_dir_path = '/'.join(this_file_path.split('/')[:-1])
config_path = this_project_dir_path + '/config.yml'
with open(config_path, 'r') as yml_file:
config = yaml.safe_load(yml_file)[0]['config']
return config
@staticmethod
def process_events() -> bool:
"""
Process the actions of the user:
- if user wants to end the simulation: quit pygame
:return: whether the simulation is running
"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
return True
def run_logic(self) -> None:
"""
Run the logic of the simulation:
- Update the positions and velocities of the Ball instances
accordingly in discrete time step using the 'step' method.
- Updates the gravity based on the current position of the mouse.
:return: None
"""
self.space.step(1/60)
# The idea is to model the gravity so that the balls (Ball instances)
# follow the mouse, like if they were chasing the mouse but found
# the BoundingBox limits on their way. After some testing, this formula
# achieves the desired effect.
mouse_x, mouse_y = pygame.mouse.get_pos()
gravity_x = self._config['screen_width'] / 2 - mouse_x
gravity_y = self._config['screen_height'] / 2 - mouse_y
self.space.gravity = -3 * int(gravity_x), -3 * int(gravity_y)
def draw(self) -> None:
"""
Display the elements of the simulation on the 'screen' attribute.
:return: None
"""
self._screen.fill(self._config['background_color'])
self.bounding_box.draw(screen=self._screen)
for ball in self.balls_list:
ball.draw(screen=self._screen)
pygame.display.update()
def clock_tick(self) -> None:
"""
Updates the pygame clock (attribute '_clock')
:return: None
"""
self._clock.tick(self._config['pygame_clock_tick'])
if __name__ == '__main__':
gravity_controller = GravityController()
running = True
while running:
running = gravity_controller.process_events()
gravity_controller.run_logic()
gravity_controller.draw()
gravity_controller.clock_tick()
pygame.quit()