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
4.. note:: This uses features from the upcoming version 2.4. The API for these
5 functions may still change. To use, you will need to install one of the
6 pre-release packages, or install via GitHub.
7
8This example shows how to create a 'bloom' or 'glow' effect.
9
10If Python and Arcade are installed, this example can be run from the command line with:
11python -m arcade.experimental.bloom_defender
12"""
13
14import arcade
15import os
16import random
17import pyglet.gl as gl
18
19# --- Bloom related ---
20from arcade.experimental import postprocessing
21
22# Size/title of the window
23SCREEN_WIDTH = 1280
24SCREEN_HEIGHT = 720
25SCREEN_TITLE = "Defender Clone"
26
27# Size of the playing field
28PLAYING_FIELD_WIDTH = 5000
29PLAYING_FIELD_HEIGHT = 1000
30
31# Size of the playing field.
32MAIN_SCREEN_HEIGHT = SCREEN_HEIGHT
33
34# How far away from the edges do we get before scrolling?
35VIEWPORT_MARGIN = SCREEN_WIDTH / 2 - 50
36TOP_VIEWPORT_MARGIN = 30
37DEFAULT_BOTTOM_VIEWPORT = -10
38
39# Control the physics of how the player moves
40MAX_HORIZONTAL_MOVEMENT_SPEED = 10
41MAX_VERTICAL_MOVEMENT_SPEED = 5
42HORIZONTAL_ACCELERATION = 0.5
43VERTICAL_ACCELERATION = 0.2
44MOVEMENT_DRAG = 0.08
45
46# How far the bullet travels before disappearing
47BULLET_MAX_DISTANCE = SCREEN_WIDTH * 0.75
48
49
50class Player(arcade.SpriteSolidColor):
51 """ Player ship """
52 def __init__(self):
53 """ Set up player """
54 super().__init__(40, 10, arcade.color.SLATE_GRAY)
55 self.face_right = True
56
57 def accelerate_up(self):
58 """ Accelerate player up """
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_down(self):
64 """ Accelerate player down """
65 self.change_y -= VERTICAL_ACCELERATION
66 if self.change_y < -MAX_VERTICAL_MOVEMENT_SPEED:
67 self.change_y = -MAX_VERTICAL_MOVEMENT_SPEED
68
69 def accelerate_right(self):
70 """ Accelerate player right """
71 self.face_right = True
72 self.change_x += HORIZONTAL_ACCELERATION
73 if self.change_x > MAX_HORIZONTAL_MOVEMENT_SPEED:
74 self.change_x = MAX_HORIZONTAL_MOVEMENT_SPEED
75
76 def accelerate_left(self):
77 """ Accelerate player left """
78 self.face_right = False
79 self.change_x -= HORIZONTAL_ACCELERATION
80 if self.change_x < -MAX_HORIZONTAL_MOVEMENT_SPEED:
81 self.change_x = -MAX_HORIZONTAL_MOVEMENT_SPEED
82
83 def update(self):
84 """ Move the player """
85 # Move
86 self.center_x += self.change_x
87 self.center_y += self.change_y
88
89 # Drag
90 if self.change_x > 0:
91 self.change_x -= MOVEMENT_DRAG
92 if self.change_x < 0:
93 self.change_x += MOVEMENT_DRAG
94 if abs(self.change_x) < MOVEMENT_DRAG:
95 self.change_x = 0
96
97 if self.change_y > 0:
98 self.change_y -= MOVEMENT_DRAG
99 if self.change_y < 0:
100 self.change_y += MOVEMENT_DRAG
101 if abs(self.change_y) < MOVEMENT_DRAG:
102 self.change_y = 0
103
104 # Check bounds
105 if self.left < 0:
106 self.left = 0
107 elif self.right > PLAYING_FIELD_WIDTH - 1:
108 self.right = PLAYING_FIELD_WIDTH - 1
109
110 if self.bottom < 0:
111 self.bottom = 0
112 elif self.top > SCREEN_HEIGHT - 1:
113 self.top = SCREEN_HEIGHT - 1
114
115class Bullet(arcade.SpriteSolidColor):
116 """ Bullet """
117
118 def __init__(self, width, height, color):
119 super().__init__(width, height, color)
120 self.distance = 0
121
122 def update(self):
123 """ Move the particle, and fade out """
124 # Move
125 self.center_x += self.change_x
126 self.center_y += self.change_y
127 self.distance += self.change_x
128 if self.distance > BULLET_MAX_DISTANCE:
129 self.remove_from_sprite_lists()
130
131class Particle(arcade.SpriteSolidColor):
132 """ Particle from explosion """
133 def update(self):
134 """ Move the particle, and fade out """
135 # Move
136 self.center_x += self.change_x
137 self.center_y += self.change_y
138 # Fade
139 self.alpha -= 5
140 if self.alpha <= 0:
141 self.remove_from_sprite_lists()
142
143class MyGame(arcade.Window):
144 """ Main application class. """
145
146 def __init__(self, width, height, title):
147 """ Initializer """
148
149 # Call the parent class initializer
150 super().__init__(width, height, title)
151
152 # Set the working directory (where we expect to find files) to the same
153 # directory this .py file is in. You can leave this out of your own
154 # code, but it is needed to easily run the examples using "python -m"
155 # as mentioned at the top of this program.
156 file_path = os.path.dirname(os.path.abspath(__file__))
157 os.chdir(file_path)
158
159 # Variables that will hold sprite lists
160 self.player_list = None
161 self.star_sprite_list = None
162 self.enemy_sprite_list = None
163 self.bullet_sprite_list = None
164
165 # Set up the player info
166 self.player_sprite = None
167
168 # Track the current state of what key is pressed
169 self.left_pressed = False
170 self.right_pressed = False
171 self.up_pressed = False
172 self.down_pressed = False
173
174 self.view_bottom = 0
175 self.view_left = 0
176
177 # Set the background color
178 arcade.set_background_color(arcade.color.BLACK)
179
180 # --- Bloom related ---
181
182 # Frame to receive the glow, and color attachment to store each pixel's
183 # color data
184 self.bloom_color_attachment = self.ctx.texture((SCREEN_WIDTH, SCREEN_HEIGHT))
185 self.bloom_screen = self.ctx.framebuffer(color_attachments=[self.bloom_color_attachment])
186
187 # Down-sampling helps improve the blur.
188 # Note: Any item with a size less than the down-sampling size may get missed in
189 # the blur process. Down-sampling by 8 and having an item of 4x4 size, the item
190 # will get missed 50% of the time in the x direction, and 50% of the time in the
191 # y direction for a total of being missed 75% of the time.
192 down_sampling = 4
193 # Size of the screen we are glowing onto
194 size = (SCREEN_WIDTH // down_sampling, SCREEN_HEIGHT // down_sampling)
195 # Gaussian blur parameters.
196 # To preview different values, see:
197 # https://observablehq.com/@jobleonard/gaussian-kernel-calculater
198 kernel_size = 21
199 sigma = 4
200 mu = 0
201 step = 1
202 # Control the intensity
203 multiplier = 2
204
205 # Create a post-processor to create a bloom
206 self.bloom_postprocessing = postprocessing.BloomEffect(size, kernel_size, sigma, mu, multiplier, step)
207
208 def setup(self):
209 """ Set up the game and initialize the variables. """
210
211 # Sprite lists
212 self.player_list = arcade.SpriteList()
213 self.star_sprite_list = arcade.SpriteList()
214 self.enemy_sprite_list = arcade.SpriteList()
215 self.bullet_sprite_list = arcade.SpriteList()
216
217 # Set up the player
218 self.player_sprite = Player()
219 self.player_sprite.center_x = 50
220 self.player_sprite.center_y = 50
221 self.player_list.append(self.player_sprite)
222
223 # Add stars
224 for i in range(80):
225 sprite = arcade.SpriteSolidColor(4, 4, arcade.color.WHITE)
226 sprite.center_x = random.randrange(PLAYING_FIELD_WIDTH)
227 sprite.center_y = random.randrange(PLAYING_FIELD_HEIGHT)
228 self.star_sprite_list.append(sprite)
229
230 # Add enemies
231 for i in range(20):
232 sprite = arcade.SpriteSolidColor(20, 20, arcade.csscolor.LIGHT_SALMON)
233 sprite.center_x = random.randrange(PLAYING_FIELD_WIDTH)
234 sprite.center_y = random.randrange(PLAYING_FIELD_HEIGHT)
235 self.enemy_sprite_list.append(sprite)
236
237 def on_draw(self):
238 """ Render the screen. """
239 # This command has to happen before we start drawing
240 arcade.start_render()
241
242 # --- Bloom related ---
243
244 # Draw to the 'bloom' layer
245 self.bloom_screen.use()
246 self.bloom_screen.clear((0, 0, 0, 0))
247
248 arcade.set_viewport(self.view_left,
249 SCREEN_WIDTH + self.view_left,
250 self.view_bottom,
251 SCREEN_HEIGHT + self.view_bottom)
252
253 # Draw all the sprites on the screen that should have a bloom
254 self.star_sprite_list.draw()
255 self.bullet_sprite_list.draw()
256
257 # Now draw to the actual screen
258 self.use()
259
260 arcade.set_viewport(self.view_left,
261 SCREEN_WIDTH + self.view_left,
262 self.view_bottom,
263 SCREEN_HEIGHT + self.view_bottom)
264
265 # --- Bloom related ---
266
267 # Draw the bloom layers
268 self.bloom_postprocessing.render(self.bloom_color_attachment, self)
269
270 # Draw the sprites / items that have no bloom
271 self.enemy_sprite_list.draw()
272 self.player_list.draw()
273
274 # Draw the ground
275 arcade.draw_line(0, 0, PLAYING_FIELD_WIDTH, 0, arcade.color.WHITE)
276
277 def on_update(self, delta_time):
278 """ Movement and game logic """
279
280 # Calculate speed based on the keys pressed
281 if self.up_pressed and not self.down_pressed:
282 self.player_sprite.accelerate_up()
283 elif self.down_pressed and not self.up_pressed:
284 self.player_sprite.accelerate_down()
285
286 if self.left_pressed and not self.right_pressed:
287 self.player_sprite.accelerate_left()
288 elif self.right_pressed and not self.left_pressed:
289 self.player_sprite.accelerate_right()
290
291 # Call update to move the sprite
292 self.player_list.update()
293 self.bullet_sprite_list.update()
294
295 for bullet in self.bullet_sprite_list:
296 enemy_hit_list = arcade.check_for_collision_with_list(bullet, 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 method """
365 window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
366 window.setup()
367 arcade.run()
368
369
370if __name__ == "__main__":
371 main()