This repository has been archived by the owner on Apr 20, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 20
/
main.py
225 lines (176 loc) · 8.35 KB
/
main.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
import sys
from dataclasses import dataclass
from pathlib import Path
from random import gauss, uniform, randint
from typing import List, Optional
import pygame
# This line tells python how to handle the relative imports
# when you run this file directly. Don't modify this line.
__package__ = "04-bouncing-bubbles." + Path(__file__).absolute().parent.name
# To import the modules in yourname/, you need to use relative imports,
# otherwise your project will not be compatible with the showcase.
from .utils import *
BACKGROUND = 0x0F1012
NB_BUBBLES = 42
class Bubble:
MAX_VELOCITY = 7
def __init__(self, position=None):
self.radius = int(gauss(25, 5))
if position is None:
# Default position is random.
self.position = pygame.Vector2(
randint(self.radius, SIZE[0] - self.radius),
randint(self.radius, SIZE[1] - self.radius),
)
else:
self.position = position
# Set a random direction and a speed of around 3.
self.velocity = pygame.Vector2()
self.velocity.from_polar((gauss(3, 0.5), uniform(0, 360)))
# Pick a random color with high saturation and value.
self.color = pygame.Color(0)
self.color.hsva = uniform(0, 360), 80, 80, 100
@property
def mass(self):
return self.radius ** 2
def draw(self, screen: pygame.Surface):
pygame.draw.circle(screen, self.color, self.position, self.radius)
def move_away_from_mouse(self, mouse_pos: pygame.Vector2):
"""Apply a force on the bubble to move away from the mouse."""
bubble_to_mouse = mouse_pos - self.position
distance_to_mouse = bubble_to_mouse.length()
if 0 < distance_to_mouse < 200:
strength = chrange(distance_to_mouse, (0, 200), (1, 0), power=2)
self.velocity -= bubble_to_mouse.normalize() * strength
def move(self):
"""Move the bubble according to its velocity."""
# We first limit the velocity to not get bubbles that go faster than what we can enjoy.
if self.velocity.length() > self.MAX_VELOCITY:
self.velocity.scale_to_length(self.MAX_VELOCITY)
self.position += self.velocity
debug.vector(self.velocity, self.position, scale=10)
def collide_borders(self):
# The first challenge is to make the bubbles bounce against the border.
# Hover that doesn't mean that a bubble must always be completely inside of the screen:
# If for instance it spawned on the edge, we don't want it to teleport so that it fits the screen,
# we want everything to be *smooooth*.
#
# To be sure it is smooth, a good rule is to never modify self.position directly,
# but instead modify self.velocity when needed.
#
# The second golden principle is to be lazy and not do anything if the collision will
# resolve itself naturally in a few frames, that is, if the bubble is already moving
# away from the wall.
# TODO: handle collisions for each of the four edges.
if not "self-resolving collision with left wall":
self.velocity.x *= -1
...
# Remove this. It is only a placeholder to keep the bubble inside the screen
self.position.x %= SIZE[0]
self.position.y %= SIZE[1]
def collide(self, other: "Bubble") -> Optional["Collision"]:
"""Get the collision data if there is a collision with the other Bubble"""
return None
# The second challenge contains two parts.
# The first one is to generate a list of all the collisions
# between bubbles.
# The data for a collision is stored into the Collision class below,
# and is generated by the Bubble.collide method above.
# The second part is then to process those collision data, and resolve them.
@dataclass
class Collision:
"""
The data of a collision consist of four attributes.
[first] and [second] are the the two objects that collided.
[center] is the collision point, that is, the point from which you
would like to push both circles away from. It corresponds to the center
of the overlapping area of the two moving circles, which is also the
midpoint between the two centers.
[normal] is the axis along which the two circles should bounce. That is,
if two bubbles move horizontally they bounce against the vertical axis,
so normal would be a vertical vector.
"""
first: "Bubble"
second: "Bubble"
center: pygame.Vector2
normal: pygame.Vector2
def resolve(self):
"""Apply a force on both colliding object to help them move out of collision."""
# The second part of the Ambitious challenge is to resolve the collisions that we have collected.
# (See below in World.logic for how all this is put together).
# TODO: Resolve the collision.
# Resolving a collision, here, means to modify the velocity of the two bubbles
# so that they move out of collision. Not necessarly in one frame, but if
# they move away from each other for say 2-5 frames, the collision will be resolved.
# To do so, add a force to the velocity of each bubble to help the two bubbles to separate.
# The separating force is perpendicular to the normal, similarly to how bubbles bounce
# against a wall: only the part of the velocity perpendicular to the wall is reflected.
# Keep in mind that one bubble an have multiple collisions at the same time.
# You may need to define extra methods.
# If you have troubles handling the mass of the particles, start by assuming they
# have a mass of 1, and then upgrade your code to take the mass into account.
...
# The world is a list of bubbles.
class World(List[Bubble]):
def __init__(self, nb):
super().__init__(Bubble() for _ in range(nb))
def logic(self, mouse_position: pygame.Vector2):
"""Handles the collision and evolution of all the objects."""
# Second part of the ambitious challenge is to make the algorithm that solves the collisions.
# A part of it is already provided so that you can focus on the important part.
# We start by moving the bubbles and do the collisions with the static objects, the walls.
for bubble in self:
bubble.move()
bubble.collide_borders()
bubble.move_away_from_mouse(mouse_position)
# Then we check each pair of bubbles to collect all collisions.
collisions = []
for i, b1 in enumerate(self):
for b2 in self[i + 1 :]:
collision = b1.collide(b2)
if collision:
collisions.append(collision)
# And finally we resolve them all at once, so that it doesn't impact the detection of collision.
for collision in collisions:
collision.resolve()
def draw(self, screen):
for bubble in self:
bubble.draw(screen)
def mainloop():
pygame.init()
world = World(NB_BUBBLES)
mouse_position = pygame.Vector2()
fps_counter = FpsCounter(60, Bubbles=world)
while True:
screen, events = yield
for event in events:
if event.type == pygame.QUIT:
return
elif event.type == pygame.MOUSEMOTION:
mouse_position.xy = event.pos
elif event.type == pygame.MOUSEBUTTONDOWN:
world.append(Bubble(event.pos))
debug.handle_event(event)
fps_counter.handle_event(event)
# Handle the collisions
world.logic(mouse_position)
fps_counter.logic()
# Drawing the screen
screen.fill(BACKGROUND)
world.draw(screen)
fps_counter.draw(screen)
debug.draw(screen)
# ---- Recommended: don't modify anything below this line ---- #
if __name__ == "__main__":
try:
# Note: your editor might say that this is an error, but it's not.
# Most editors can't understand that we are messing with the path.
import wclib
except ImportError:
# wclib may not be in the path because of the architecture
# of all the challenges and the fact that there are many
# way to run them (through the showcase, or on their own)
ROOT_FOLDER = Path(__file__).absolute().parent.parent.parent
sys.path.append(str(ROOT_FOLDER))
import wclib
wclib.run(mainloop())