Bloom-Effect Defender#

Creating a “glowing” effect can enhance 2D games. This example shows how to do it.
Create Frame Buffer and Post-Processor#
Lines 176-202
Here we create the frame buffer, and add a color attachment to store the pixel data into.
It also creates a post-processor that will do a gaussian blur on what is rendered. There are a lot of parameters to the blur, depending on how you want it to look.
Render To Framebuffer#
Lines 239-252
When we draw, we render the objects we want to be blurred to our frame buffer, then run the post-processor to do the blur.
Note: This buffer is not transparent! Anything behind it will be hidden. So multiple layers of glow are not possible at this time, nor can you put anything ‘behind’ the glow.
Render Framebuffer To Screen#
Lines 264-265
Finally we render that buffer to the screen.
mini_map_defender.py#
1"""
2Defender Clone.
3
4This example shows how to create a 'bloom' or 'glow' effect.
5
6If Python and Arcade are installed, this example can be run from the command line with:
7python -m arcade.examples.bloom_defender
8"""
9
10import arcade
11import random
12
13# --- Bloom related ---
14from arcade.experimental import postprocessing
15
16# Size/title of the window
17SCREEN_WIDTH = 1280
18SCREEN_HEIGHT = 720
19SCREEN_TITLE = "Defender Clone"
20
21# Size of the playing field
22PLAYING_FIELD_WIDTH = 5000
23PLAYING_FIELD_HEIGHT = 1000
24
25# Size of the playing field.
26MAIN_SCREEN_HEIGHT = SCREEN_HEIGHT
27
28# How far away from the edges do we get before scrolling?
29VIEWPORT_MARGIN = SCREEN_WIDTH / 2 - 50
30TOP_VIEWPORT_MARGIN = 30
31DEFAULT_BOTTOM_VIEWPORT = -10
32
33# Control the physics of how the player moves
34MAX_HORIZONTAL_MOVEMENT_SPEED = 10
35MAX_VERTICAL_MOVEMENT_SPEED = 5
36HORIZONTAL_ACCELERATION = 0.5
37VERTICAL_ACCELERATION = 0.2
38MOVEMENT_DRAG = 0.08
39
40# How far the bullet travels before disappearing
41BULLET_MAX_DISTANCE = SCREEN_WIDTH * 0.75
42
43
44class Player(arcade.SpriteSolidColor):
45 """ Player ship """
46 def __init__(self):
47 """ Set up player """
48 super().__init__(40, 10, color=arcade.color.SLATE_GRAY)
49 self.face_right = True
50
51 def accelerate_up(self):
52 """ Accelerate player up """
53 self.change_y += VERTICAL_ACCELERATION
54 if self.change_y > MAX_VERTICAL_MOVEMENT_SPEED:
55 self.change_y = MAX_VERTICAL_MOVEMENT_SPEED
56
57 def accelerate_down(self):
58 """ Accelerate player down """
59 self.change_y -= VERTICAL_ACCELERATION
60 if self.change_y < -MAX_VERTICAL_MOVEMENT_SPEED:
61 self.change_y = -MAX_VERTICAL_MOVEMENT_SPEED
62
63 def accelerate_right(self):
64 """ Accelerate player right """
65 self.face_right = True
66 self.change_x += HORIZONTAL_ACCELERATION
67 if self.change_x > MAX_HORIZONTAL_MOVEMENT_SPEED:
68 self.change_x = MAX_HORIZONTAL_MOVEMENT_SPEED
69
70 def accelerate_left(self):
71 """ Accelerate player left """
72 self.face_right = False
73 self.change_x -= HORIZONTAL_ACCELERATION
74 if self.change_x < -MAX_HORIZONTAL_MOVEMENT_SPEED:
75 self.change_x = -MAX_HORIZONTAL_MOVEMENT_SPEED
76
77 def update(self):
78 """ Move the player """
79 # Move
80 self.center_x += self.change_x
81 self.center_y += self.change_y
82
83 # Drag
84 if self.change_x > 0:
85 self.change_x -= MOVEMENT_DRAG
86 if self.change_x < 0:
87 self.change_x += MOVEMENT_DRAG
88 if abs(self.change_x) < MOVEMENT_DRAG:
89 self.change_x = 0
90
91 if self.change_y > 0:
92 self.change_y -= MOVEMENT_DRAG
93 if self.change_y < 0:
94 self.change_y += MOVEMENT_DRAG
95 if abs(self.change_y) < MOVEMENT_DRAG:
96 self.change_y = 0
97
98 # Check bounds
99 if self.left < 0:
100 self.left = 0
101 elif self.right > PLAYING_FIELD_WIDTH - 1:
102 self.right = PLAYING_FIELD_WIDTH - 1
103
104 if self.bottom < 0:
105 self.bottom = 0
106 elif self.top > SCREEN_HEIGHT - 1:
107 self.top = SCREEN_HEIGHT - 1
108
109
110class Bullet(arcade.SpriteSolidColor):
111 """ Bullet """
112
113 def __init__(self, width, height, color):
114 super().__init__(width, height, color)
115 self.distance = 0
116
117 def update(self):
118 """ Move the particle, and fade out """
119 # Move
120 self.center_x += self.change_x
121 self.center_y += self.change_y
122 self.distance += self.change_x
123 if self.distance > BULLET_MAX_DISTANCE:
124 self.remove_from_sprite_lists()
125
126
127class Particle(arcade.SpriteSolidColor):
128 """ Particle from explosion """
129 def update(self):
130 """ Move the particle, and fade out """
131 # Move
132 self.center_x += self.change_x
133 self.center_y += self.change_y
134 # Fade
135 self.alpha -= 5
136 if self.alpha <= 0:
137 self.remove_from_sprite_lists()
138
139
140class MyGame(arcade.Window):
141 """ Main application class. """
142
143 def __init__(self, width, height, title):
144 """ Initializer """
145
146 # Call the parent class initializer
147 super().__init__(width, height, title)
148
149 # Variables that will hold sprite lists
150 self.player_list = None
151 self.star_sprite_list = None
152 self.enemy_sprite_list = None
153 self.bullet_sprite_list = None
154
155 # Set up the player info
156 self.player_sprite = None
157
158 # Track the current state of what key is pressed
159 self.left_pressed = False
160 self.right_pressed = False
161 self.up_pressed = False
162 self.down_pressed = False
163
164 self.view_bottom = 0
165 self.view_left = 0
166
167 # Set the background color of the window
168 self.background_color = arcade.color.BLACK
169
170 # --- Bloom related ---
171
172 # Frame to receive the glow, and color attachment to store each pixel's
173 # color data
174 self.bloom_color_attachment = self.ctx.texture((SCREEN_WIDTH, SCREEN_HEIGHT))
175 self.bloom_screen = self.ctx.framebuffer(
176 color_attachments=[self.bloom_color_attachment]
177 )
178
179 # Down-sampling helps improve the blur.
180 # Note: Any item with a size less than the down-sampling size may get missed in
181 # the blur process. Down-sampling by 8 and having an item of 4x4 size, the item
182 # will get missed 50% of the time in the x direction, and 50% of the time in the
183 # y direction for a total of being missed 75% of the time.
184 down_sampling = 4
185 # Size of the screen we are glowing onto
186 size = (SCREEN_WIDTH // down_sampling, SCREEN_HEIGHT // down_sampling)
187 # Gaussian blur parameters.
188 # To preview different values, see:
189 # https://observablehq.com/@jobleonard/gaussian-kernel-calculater
190 kernel_size = 21
191 sigma = 4
192 mu = 0
193 step = 1
194 # Control the intensity
195 multiplier = 2
196
197 # Create a post-processor to create a bloom
198 self.bloom_postprocessing = postprocessing.BloomEffect(size,
199 kernel_size,
200 sigma,
201 mu,
202 multiplier,
203 step)
204
205 def setup(self):
206 """ Set up the game and initialize the variables. """
207
208 # Sprite lists
209 self.player_list = arcade.SpriteList()
210 self.star_sprite_list = arcade.SpriteList()
211 self.enemy_sprite_list = arcade.SpriteList()
212 self.bullet_sprite_list = arcade.SpriteList()
213
214 # Set up the player
215 self.player_sprite = Player()
216 self.player_sprite.center_x = 50
217 self.player_sprite.center_y = 50
218 self.player_list.append(self.player_sprite)
219
220 # Add stars
221 for i in range(80):
222 sprite = arcade.SpriteSolidColor(4, 4, color=arcade.color.WHITE)
223 sprite.center_x = random.randrange(PLAYING_FIELD_WIDTH)
224 sprite.center_y = random.randrange(PLAYING_FIELD_HEIGHT)
225 self.star_sprite_list.append(sprite)
226
227 # Add enemies
228 for i in range(20):
229 sprite = arcade.SpriteSolidColor(20, 20, color=arcade.csscolor.LIGHT_SALMON)
230 sprite.center_x = random.randrange(PLAYING_FIELD_WIDTH)
231 sprite.center_y = random.randrange(PLAYING_FIELD_HEIGHT)
232 self.enemy_sprite_list.append(sprite)
233
234 def on_draw(self):
235 """ Render the screen. """
236 # This command has to happen before we start drawing
237 self.clear()
238
239 # --- Bloom related ---
240
241 # Draw to the 'bloom' layer
242 self.bloom_screen.use()
243 self.bloom_screen.clear(arcade.color.TRANSPARENT_BLACK)
244
245 arcade.set_viewport(self.view_left,
246 SCREEN_WIDTH + self.view_left,
247 self.view_bottom,
248 SCREEN_HEIGHT + self.view_bottom)
249
250 # Draw all the sprites on the screen that should have a bloom
251 self.star_sprite_list.draw()
252 self.bullet_sprite_list.draw()
253
254 # Now draw to the actual screen
255 self.use()
256
257 arcade.set_viewport(self.view_left,
258 SCREEN_WIDTH + self.view_left,
259 self.view_bottom,
260 SCREEN_HEIGHT + self.view_bottom)
261
262 # --- Bloom related ---
263
264 # Draw the bloom layers
265 self.bloom_postprocessing.render(self.bloom_color_attachment, self)
266
267 # Draw the sprites / items that have no bloom
268 self.enemy_sprite_list.draw()
269 self.player_list.draw()
270
271 # Draw the ground
272 arcade.draw_line(0, 0, PLAYING_FIELD_WIDTH, 0, arcade.color.WHITE)
273
274 def on_update(self, delta_time):
275 """ Movement and game logic """
276
277 # Calculate speed based on the keys pressed
278 if self.up_pressed and not self.down_pressed:
279 self.player_sprite.accelerate_up()
280 elif self.down_pressed and not self.up_pressed:
281 self.player_sprite.accelerate_down()
282
283 if self.left_pressed and not self.right_pressed:
284 self.player_sprite.accelerate_left()
285 elif self.right_pressed and not self.left_pressed:
286 self.player_sprite.accelerate_right()
287
288 # Call update to move the sprite
289 self.player_list.update()
290 self.bullet_sprite_list.update()
291
292 for bullet in self.bullet_sprite_list:
293 enemy_hit_list = arcade.check_for_collision_with_list(bullet,
294 self.enemy_sprite_list)
295 for enemy in enemy_hit_list:
296 enemy.remove_from_sprite_lists()
297 for i in range(10):
298 particle = Particle(4, 4, arcade.color.RED)
299 while particle.change_y == 0 and particle.change_x == 0:
300 particle.change_y = random.randrange(-2, 3)
301 particle.change_x = random.randrange(-2, 3)
302 particle.center_x = enemy.center_x
303 particle.center_y = enemy.center_y
304 self.bullet_sprite_list.append(particle)
305
306 # Scroll left
307 left_boundary = self.view_left + VIEWPORT_MARGIN
308 if self.player_sprite.left < left_boundary:
309 self.view_left -= left_boundary - self.player_sprite.left
310
311 # Scroll right
312 right_boundary = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN
313 if self.player_sprite.right > right_boundary:
314 self.view_left += self.player_sprite.right - right_boundary
315
316 # Scroll up
317 self.view_bottom = DEFAULT_BOTTOM_VIEWPORT
318 top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
319 if self.player_sprite.top > top_boundary:
320 self.view_bottom += self.player_sprite.top - top_boundary
321
322 self.view_left = int(self.view_left)
323 self.view_bottom = int(self.view_bottom)
324
325 def on_key_press(self, key, modifiers):
326 """Called whenever a key is pressed. """
327
328 if key == arcade.key.UP:
329 self.up_pressed = True
330 elif key == arcade.key.DOWN:
331 self.down_pressed = True
332 elif key == arcade.key.LEFT:
333 self.left_pressed = True
334 elif key == arcade.key.RIGHT:
335 self.right_pressed = True
336 elif key == arcade.key.SPACE:
337 # Shoot out a bullet/laser
338 bullet = arcade.SpriteSolidColor(35, 3, arcade.color.WHITE)
339 bullet.center_x = self.player_sprite.center_x
340 bullet.center_y = self.player_sprite.center_y
341 bullet.change_x = max(12, abs(self.player_sprite.change_x) + 10)
342
343 if not self.player_sprite.face_right:
344 bullet.change_x *= -1
345
346 self.bullet_sprite_list.append(bullet)
347
348 def on_key_release(self, key, modifiers):
349 """Called when the user releases a key. """
350
351 if key == arcade.key.UP:
352 self.up_pressed = False
353 elif key == arcade.key.DOWN:
354 self.down_pressed = False
355 elif key == arcade.key.LEFT:
356 self.left_pressed = False
357 elif key == arcade.key.RIGHT:
358 self.right_pressed = False
359
360
361def main():
362 """ Main function """
363 window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
364 window.setup()
365 arcade.run()
366
367
368if __name__ == "__main__":
369 main()