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