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