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