Skip to content

Commit b6a9d82

Browse files
authored
Merge pull request #117 from orionrobots/particle-systems
Particle systems
2 parents 3d4e1c6 + c571f42 commit b6a9d82

File tree

5 files changed

+467
-0
lines changed

5 files changed

+467
-0
lines changed
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
---
2+
title: How to make particle systems for fun and robotics
3+
date: 2024-11-24
4+
tags: ["robotics", "programming", "python", "particle systems"," robotics at home"]
5+
thumbnail: content/2024/11/24-making-particle-systems-for-fun-and-robotics/raindrops.png
6+
---
7+
I have a lifelong fascination with Particle systems, complementing my robotics. However, it was only a few years ago that I found out how to bring the two together in a Monte Carlo Localization particle filter.
8+
9+
Today I am going to express my love of particle systems in Python. This might be a long ride, with a few programs in a series of posts. I hope you'll join me, with some demonstrations in how much fun these can be. It would be helpful if you've done a little Python before, but this aims to be a beginner-friendly course.
10+
11+
This series will focus on programming with lots of visuals. You'll write it using PyGame letting us get a lot on the screen. Beyond being handy in robotics, this is fun for some simple visual effects, games and simulations.
12+
13+
## Getting prepared
14+
15+
Depending on your experience, you will need an environment to run this in.
16+
17+
For absolute beginners, I recommend starting in the Mu editor, as this comes with a Python environment and most of what you'll need to follow along. However, if you are more experienced, you can use your own IDE.
18+
19+
If you are using your own IDE you will need to ensure you have at least Python 3.8 with PyGame installed. You can install PyGame using pip or poetry:
20+
21+
```bash
22+
pip install pygame
23+
```
24+
25+
## One dot
26+
27+
The simplest particle system is one dot. You'll use this to get up and running. This is the least interesting particle system, but you can build on it to something more fun.
28+
29+
```python
30+
import pygame
31+
32+
WIDTH = 800
33+
HEIGHT = 800
34+
FRAME_RATE = 60
35+
BG_COLOUR = pygame.Color("darkslategray1")
36+
DOT_COLOUR = pygame.Color("deepskyblue4")
37+
DOT_SIZE = 2
38+
39+
one_dot = [400, 400]
40+
41+
def draw(surface):
42+
pygame.draw.circle(surface, DOT_COLOUR, one_dot, DOT_SIZE)
43+
44+
45+
pygame.init()
46+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
47+
clock = pygame.time.Clock()
48+
49+
running = True
50+
while running:
51+
for event in pygame.event.get():
52+
if event.type == pygame.QUIT:
53+
running = False
54+
55+
screen.fill(BG_COLOUR)
56+
draw(screen)
57+
pygame.display.flip()
58+
clock.tick(FRAME_RATE)
59+
pygame.quit()
60+
```
61+
62+
Run it and you should see this:
63+
64+
![One dot on the screen](/2024/11/24-making-particle-systems-for-fun-and-robotics/one-dot.png)
65+
66+
The code starts with importing [PyGame](https://www.pygame.org/), a Python gaming library for drawing 2D games. It then sets up some parameters for your program. It defines the display size with `WIDTH` and `HEIGHT`, and uses `FRAME_RATE` so the program runs at a consistent speed.
67+
68+
You then set up some colours. A background colour, and a colour for your dot, followed by a dot size. putting these things in parameters makes them easy to change later.
69+
70+
Your code then defines a list with two numbers, the x and y position of the dot. This is your particle, followed by a function to draw our particle, a circle on the screen. In PyGame, the top left corner is 0,0, so the bottom of the screen is HEIGHT.
71+
72+
![PyGame Coordinate System](/2024/11/24-making-particle-systems-for-fun-and-robotics/pygame-coordinates.png)
73+
74+
After this you initialise pygame, with a screen and a clock and enter a main loop. The main loop starts with a running variable, so the system can be told when to exit. The code looks for a QUIT event, triggered when you close the window to ensure it shuts down. This is a common pattern in PyGame.
75+
76+
The next part of the loop fills the screen with background colour. You then use the `draw` function to draw the dot. You must call `pygame.display.flip` as pygame draws everything on a hidden buffer, which you swap with the visible screen.
77+
78+
Finally you tick the clock to keep the framerate the same. The last line of the program is `pygame.quit` to ensure everything is cleaned up.
79+
80+
This is drawn at 400, 400 which is the middle of the screen. I suggest you try a few values between 0 and the WIDTH to see how it changes. I guarantee you won't like my colour choices, so you can also try different colour names, or even RGB values (like `(255, 0, 0)` for red).
81+
82+
## Making it a bit random
83+
84+
A key concept in a particle system is randomness. You can make the one dot less boring by making it random.
85+
86+
At the top of the file, lets import the random module above pygame:
87+
88+
```python
89+
import random
90+
import pygame
91+
```
92+
93+
Now you can make it show the dot at a random place every time you run it. Update the one_dot line to be:
94+
95+
```python
96+
one_dot = [random.randint(0, WIDTH), random.randint(0, HEIGHT)]
97+
```
98+
99+
## Movement
100+
101+
You have a particle, but it's not doing much. You can add a little movement to our particle.
102+
103+
Particles have a lifecycle:
104+
105+
- They are created
106+
- They update and change over time
107+
- They may also die
108+
109+
Let's introduce a speed constant and update our particle with it. In the constants add the following:
110+
111+
```python
112+
SPEED = 2
113+
```
114+
115+
You then add an `update` function, which can be called in the main loop:
116+
117+
```python
118+
def update():
119+
one_dot[1] += SPEED
120+
if one_dot[1] >= HEIGHT:
121+
one_dot[1] = 0
122+
```
123+
124+
This will add the speed to the second element (the Y coordinate) of the one_dot list. If the dot reaches the bottom of the screen, you reset it to the top.
125+
126+
You then call `update` in the main loop before you start drawing things:
127+
128+
```python
129+
running = True
130+
while running:
131+
for event in pygame.event.get():
132+
if event.type == pygame.QUIT:
133+
running = False
134+
135+
update()
136+
screen.fill(BG_COLOUR)
137+
draw(screen)
138+
pygame.display.flip()
139+
clock.tick(FRAME_RATE)
140+
```
141+
142+
Run this. You can adjust the speed by changing the SPEED constant, or increasing the FRAME_RATE, however, note that above a certain frame rate value your frames may be being slowed by the speed of the program. With high SPEED values, you may see the dot jump on the screen.
143+
144+
This dot has a lifecycle:
145+
146+
- It is created at a random position
147+
- It moves down the screen
148+
- When it reaches the bottom, it is moved back to the top
149+
150+
What happens if you make the SPEED constant negative?
151+
152+
## Multiple dots
153+
154+
You can make this more interesting again by having multiple dots, like a rain storm. You can do this by having a list of dots.
155+
156+
Swap our one dot for this:
157+
158+
```python
159+
POPULATION_SIZE = 200
160+
161+
raindrops = []
162+
163+
def populate():
164+
for n in range(POPULATION_SIZE):
165+
raindrops.append(
166+
[
167+
random.randint(0, WIDTH),
168+
random.randint(0, HEIGHT),
169+
]
170+
)
171+
```
172+
173+
You then need to update the draw function to draw all the dots:
174+
175+
```python
176+
def draw(surface):
177+
for raindrop in raindrops:
178+
pygame.draw.circle(surface, DOT_COLOUR, raindrop, DOT_SIZE)
179+
```
180+
181+
And modify the update function to move all the dots:
182+
183+
```python
184+
def update():
185+
for raindrop in raindrops:
186+
raindrop[1] += SPEED
187+
if raindrop[1] >= HEIGHT:
188+
raindrop[1] = 0
189+
```
190+
191+
Finally, you need to call the populate function to create the dots while you initialise the program:
192+
193+
```python
194+
pygame.init()
195+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
196+
clock = pygame.time.Clock()
197+
populate()
198+
```
199+
200+
Run this, and you should now see dots falling down the screen. I've also used some more rain-like colours.
201+
202+
![Raindrops on the screen](/2024/11/24-making-particle-systems-for-fun-and-robotics/raindrops.png)
203+
204+
The colour names I've used are `darkslategray1` for the background and `deepskyblue4` for the dots. You can find a list of colour names in the [PyGame documentation](https://www.pygame.org/docs/ref/color.html).
205+
206+
You can modify the number of dots, but be aware that too large a number might make it slower. You can still adjust the speed and size of the dots.
207+
208+
These dot's all have the same lifecycle as our one dot!
209+
210+
## Adjusting the lifecycle
211+
212+
You can change this particle system in a few interesting ways. You might have noticed the raindrops loop around in a repeating pattern.
213+
214+
You can fix this by changing the lifecycle. Instead of just wrapping the raindrop, you can pretend this raindrop has reached the end of the lifecycle and that your are creating a new one. However, you can do something sneaky and reset the x to a random value when you do this. You only need to modify the `update` function:
215+
216+
```python
217+
def update():
218+
for raindrop in raindrops:
219+
raindrop[1] += SPEED
220+
if raindrop[1] >= HEIGHT:
221+
raindrop[1] = 0
222+
raindrop[0] = random.randint(0, WIDTH)
223+
```
224+
225+
You shouldn't be able to see a repeating pattern any more.
226+
227+
## Random speeds
228+
229+
You can add a little depth by adding a further parameter to our raindrops. For this you will change two parts of the lifecycle - the add function and the update function.
230+
You'll also adjust the parameters above. Extend the constants after the POPULATION_SIZE:
231+
232+
```python
233+
POPULATION_SIZE = 200
234+
MIN_SPEED = 2
235+
MAX_SPEED = 6
236+
```
237+
238+
You can then modify the populate function to generate a random speed:
239+
240+
```python
241+
def populate():
242+
for n in range(POPULATION_SIZE):
243+
raindrops.append(
244+
[
245+
random.randint(0, WIDTH),
246+
random.randint(0, HEIGHT),
247+
random.randint(MIN_SPEED, MAX_SPEED),
248+
]
249+
)
250+
```
251+
252+
You can then update using this stored speed:
253+
254+
```python
255+
def update():
256+
for raindrop in raindrops:
257+
raindrop[1] += raindrop[2]
258+
if raindrop[1] >= HEIGHT:
259+
raindrop[1] = 0
260+
raindrop[0] = random.randint(0, WIDTH)
261+
```
262+
263+
However, the code made an assumption in draw that the raindrop was only 2 numbers - the coordinates of the drop. With 3, you need to filter them:
264+
265+
```python
266+
def draw(surface):
267+
for raindrop in raindrops:
268+
pygame.draw.circle(surface, DOT_COLOUR, raindrop[:2], DOT_SIZE)
269+
```
270+
271+
If you run this, you can now see raindrops falling at different speeds.
272+
273+
## Checkpoint - raindrops
274+
275+
You've built a small particle system, transforming a single static dot into a rainstorm with raindrops at different speeds. Here's the full code:
276+
277+
```python
278+
import random
279+
import pygame
280+
281+
WIDTH = 800
282+
HEIGHT = 800
283+
FRAME_RATE = 60
284+
BG_COLOUR = pygame.Color("darkslategray1")
285+
DOT_COLOUR = pygame.Color("deepskyblue4")
286+
DOT_SIZE = 2
287+
SPEED = 2
288+
POPULATION_SIZE = 200
289+
MIN_SPEED = 2
290+
MAX_SPEED = 6
291+
292+
raindrops = []
293+
294+
def populate():
295+
for n in range(POPULATION_SIZE):
296+
raindrops.append(
297+
[
298+
random.randint(0, WIDTH),
299+
random.randint(0, HEIGHT),
300+
random.randint(MIN_SPEED, MAX_SPEED),
301+
]
302+
)
303+
304+
def draw(surface):
305+
for raindrop in raindrops:
306+
pygame.draw.circle(surface, DOT_COLOUR, raindrop[:2], DOT_SIZE)
307+
308+
def update():
309+
for raindrop in raindrops:
310+
raindrop[1] += raindrop[2]
311+
if raindrop[1] >= HEIGHT:
312+
raindrop[1] = 0
313+
raindrop[0] = random.randint(0, WIDTH)
314+
315+
pygame.init()
316+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
317+
clock = pygame.time.Clock()
318+
populate()
319+
320+
running = True
321+
while running:
322+
for event in pygame.event.get():
323+
if event.type == pygame.QUIT:
324+
running = False
325+
326+
update()
327+
screen.fill(BG_COLOUR)
328+
draw(screen)
329+
pygame.display.flip()
330+
clock.tick(FRAME_RATE)
331+
pygame.quit()
332+
```
333+
334+
## Other ideas
335+
336+
With this system, you could make the raindrops different sizes. You could add wind factors and other elements.
337+
338+
You can add backgrounds or other embellishments.
339+
340+
## Summary
341+
342+
You've built a simple particle system, raindrops, using Python and PyGame. You've used the random number generator to position them on the screen, and for other aspects of the particles.
343+
344+
You've also seen how particles have a lifecycle.
345+
346+
Over the coming for posts, we can explore what other ways you can use particle systems, some variations on this theme, and some quite different.
347+
348+
I've built this inspired by the Kingston University Coder Dojo where I mentor Python, and will have other particle systems inspired by research I've done for my books.
2.09 KB
Loading
14.7 KB
Loading

0 commit comments

Comments
 (0)