Bloom-Effect Defender#

Screen shot of a Defender clone with a bloom/glow effect.

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()