Mini-Map Defender

Screen shot of a Defender clone with mini-map.

This example shows how to create a ‘mini-map’ using frame buffers. Frame buffers allow us to draw off-screen. We can then take that image and draw it elsewhere.

Section 1 - Define Constants

Lines 26-32

At the start of our code, define some constants that specify how large your mini-map is. In this case, our mini-map is at the top of the screen, and the main playing field is below.

Section 2 - Create Frame Buffer

Lines 172-189

As our window opens up, we need to define a frame buffer that will hold our mini-map.

  • Figure out the size and position.

  • Create an OpenGL shader program. This is a simple pre-defined shader that just passes through vertex and texture data.

  • Create a color attachment, which holds what color each pixel is.

  • Create a frame buffer, and tell it to use the color attachment to hold data.

  • Create a rectangle that defines where we will draw the mini-map.

Section 3 - Draw to Mini-Map

Lines 226-238

Any time we want to draw to the frame buffer instead of the screen, there’s a use method in the frame buffer. So in this case, we can do:

self.mini_map_screen.use()

To switch back to the screen, use the windows use method:

self.use()

Once we select the frame buffer, we set the viewport to encompass our entire playing field. Then we draw the sprites we want in the mini-map. Note that this example doesn’t draw the background stars or bullets to the mini-map. The program can easily select what should appear in the mini-map this way.

Section 4 - Draw Mini-Map to Screen

Lines 263-280

While we’ve drawn the mini-map, it is off-screen and we can’t see it. In this case, we render it to the pre-defined location with:

self.mini_map_color_attachment.use(0)
self.mini_map_rect.render(self.program)

The rest of the code in this section calculates a rectangle on the mini-map that outlines what the user can see on the mini-map.

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 mini-map
  9
 10If Python and Arcade are installed, this example can be run from the command line with:
 11python -m arcade.examples.minimap_defender
 12"""
 13
 14import arcade
 15import random
 16
 17# --- Minimap Related ---
 18from arcade.gl import geometry
 19
 20# Size/title of the window
 21SCREEN_WIDTH = 1280
 22SCREEN_HEIGHT = 800
 23SCREEN_TITLE = "Defender Clone"
 24
 25# Size of the playing field
 26PLAYING_FIELD_WIDTH = 5000
 27PLAYING_FIELD_HEIGHT = 1000
 28
 29# --- Mini-map related ---
 30# Size of the minimap
 31MINIMAP_HEIGHT = 200
 32
 33# Size of the playing field.
 34# This, plus the mini-map height, should add up to the height of the screen.
 35MAIN_SCREEN_HEIGHT = SCREEN_HEIGHT - MINIMAP_HEIGHT
 36
 37# How far away from the edges do we get before scrolling?
 38VIEWPORT_MARGIN = SCREEN_WIDTH / 2 - 50
 39TOP_VIEWPORT_MARGIN = 30
 40DEFAULT_BOTTOM_VIEWPORT = -10
 41
 42# Control the physics of how the player moves
 43MAX_HORIZONTAL_MOVEMENT_SPEED = 10
 44MAX_VERTICAL_MOVEMENT_SPEED = 5
 45HORIZONTAL_ACCELERATION = 0.5
 46VERTICAL_ACCELERATION = 0.2
 47MOVEMENT_DRAG = 0.08
 48
 49# How far the bullet travels before disappearing
 50BULLET_MAX_DISTANCE = SCREEN_WIDTH * 0.75
 51
 52class Player(arcade.SpriteSolidColor):
 53    """ Player ship """
 54    def __init__(self):
 55        """ Set up player """
 56        super().__init__(40, 10, arcade.color.SLATE_GRAY)
 57        self.face_right = True
 58
 59    def accelerate_up(self):
 60        """ Accelerate player up """
 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_down(self):
 66        """ Accelerate player down """
 67        self.change_y -= VERTICAL_ACCELERATION
 68        if self.change_y < -MAX_VERTICAL_MOVEMENT_SPEED:
 69            self.change_y = -MAX_VERTICAL_MOVEMENT_SPEED
 70
 71    def accelerate_right(self):
 72        """ Accelerate player right """
 73        self.face_right = True
 74        self.change_x += HORIZONTAL_ACCELERATION
 75        if self.change_x > MAX_HORIZONTAL_MOVEMENT_SPEED:
 76            self.change_x = MAX_HORIZONTAL_MOVEMENT_SPEED
 77
 78    def accelerate_left(self):
 79        """ Accelerate player left """
 80        self.face_right = False
 81        self.change_x -= HORIZONTAL_ACCELERATION
 82        if self.change_x < -MAX_HORIZONTAL_MOVEMENT_SPEED:
 83            self.change_x = -MAX_HORIZONTAL_MOVEMENT_SPEED
 84
 85    def update(self):
 86        """ Move the player """
 87        # Move
 88        self.center_x += self.change_x
 89        self.center_y += self.change_y
 90
 91        # Drag
 92        if self.change_x > 0:
 93            self.change_x -= MOVEMENT_DRAG
 94        if self.change_x < 0:
 95            self.change_x += MOVEMENT_DRAG
 96        if abs(self.change_x) < MOVEMENT_DRAG:
 97            self.change_x = 0
 98
 99        if self.change_y > 0:
100            self.change_y -= MOVEMENT_DRAG
101        if self.change_y < 0:
102            self.change_y += MOVEMENT_DRAG
103        if abs(self.change_y) < MOVEMENT_DRAG:
104            self.change_y = 0
105
106        # Check bounds
107        if self.left < 0:
108            self.left = 0
109        elif self.right > PLAYING_FIELD_WIDTH - 1:
110            self.right = PLAYING_FIELD_WIDTH - 1
111
112        if self.bottom < 0:
113            self.bottom = 0
114        elif self.top > PLAYING_FIELD_HEIGHT - 1:
115            self.top = PLAYING_FIELD_HEIGHT - 1
116
117class Bullet(arcade.SpriteSolidColor):
118    """ Bullet """
119
120    def __init__(self, width, height, color):
121        super().__init__(width, height, color)
122        self.distance = 0
123
124    def update(self):
125        """ Move the particle, and fade out """
126        # Move
127        self.center_x += self.change_x
128        self.center_y += self.change_y
129        self.distance += self.change_x
130        if self.distance > BULLET_MAX_DISTANCE:
131            self.remove_from_sprite_lists()
132
133class Particle(arcade.SpriteSolidColor):
134    """ Particle from explosion """
135    def update(self):
136        """ Move the particle, and fade out """
137        # Move
138        self.center_x += self.change_x
139        self.center_y += self.change_y
140        # Fade
141        self.alpha -= 5
142        if self.alpha <= 0:
143            self.remove_from_sprite_lists()
144
145class MyGame(arcade.Window):
146    """ Main application class. """
147
148    def __init__(self, width, height, title):
149        """ Initializer """
150
151        # Call the parent class initializer
152        super().__init__(width, height, title)
153
154        # Variables that will hold sprite lists
155        self.player_list = None
156        self.star_sprite_list = None
157        self.enemy_sprite_list = None
158        self.bullet_sprite_list = None
159
160        # Set up the player info
161        self.player_sprite = None
162
163        # Track the current state of what key is pressed
164        self.left_pressed = False
165        self.right_pressed = False
166        self.up_pressed = False
167        self.down_pressed = False
168
169        self.view_bottom = 0
170        self.view_left = 0
171
172        # Set the background color
173        arcade.set_background_color(arcade.color.BLACK)
174
175        # --- Mini-map related ---
176        # How big is our screen?
177        screen_size = (SCREEN_WIDTH, SCREEN_HEIGHT)
178        # How big is the mini-map?
179        mini_map_size = (SCREEN_WIDTH, MINIMAP_HEIGHT)
180        # Where is the mini-map to be drawn?
181        mini_map_pos = (SCREEN_WIDTH / 2, SCREEN_HEIGHT - MINIMAP_HEIGHT / 2)
182        # Load a vertex and fragment shader
183        self.program = self.ctx.load_program(
184            vertex_shader=arcade.resources.shaders.vertex.default_projection,
185            fragment_shader=arcade.resources.shaders.fragment.texture)
186        # Add a color attachment to store pixel colors
187        self.mini_map_color_attachment = self.ctx.texture(screen_size)
188        # Create a frame buffer with the needed color attachment
189        self.mini_map_screen = self.ctx.framebuffer(color_attachments=[self.mini_map_color_attachment])
190        # Create a rectangle that will hold where the mini-map goes
191        self.mini_map_rect = geometry.screen_rectangle(0, SCREEN_WIDTH, MINIMAP_HEIGHT, SCREEN_HEIGHT)
192
193    def setup(self):
194        """ Set up the game and initialize the variables. """
195
196        # Sprite lists
197        self.player_list = arcade.SpriteList()
198        self.star_sprite_list = arcade.SpriteList()
199        self.enemy_sprite_list = arcade.SpriteList()
200        self.bullet_sprite_list = arcade.SpriteList()
201
202        # Set up the player
203        self.player_sprite = Player()
204        self.player_sprite.center_x = 50
205        self.player_sprite.center_y = 50
206        self.player_list.append(self.player_sprite)
207
208        # Add stars
209        for i in range(100):
210            sprite = arcade.SpriteSolidColor(4, 4, arcade.color.WHITE)
211            sprite.center_x = random.randrange(PLAYING_FIELD_WIDTH)
212            sprite.center_y = random.randrange(PLAYING_FIELD_HEIGHT)
213            self.star_sprite_list.append(sprite)
214
215        # Add enemies
216        for i in range(30):
217            sprite = arcade.SpriteSolidColor(20, 20, arcade.csscolor.LIGHT_SALMON)
218            sprite.center_x = random.randrange(PLAYING_FIELD_WIDTH)
219            sprite.center_y = random.randrange(PLAYING_FIELD_HEIGHT)
220            self.enemy_sprite_list.append(sprite)
221
222    def on_draw(self):
223        """ Render the screen. """
224        # This command has to happen before we start drawing
225        arcade.start_render()
226
227        # --- Mini-map related ---
228
229        # Draw to the frame buffer used in the mini-map
230        self.mini_map_screen.use()
231        self.mini_map_screen.clear()
232
233        arcade.set_viewport(0,
234                            PLAYING_FIELD_WIDTH,
235                            0,
236                            PLAYING_FIELD_HEIGHT)
237
238        self.enemy_sprite_list.draw()
239        self.player_list.draw()
240
241        # Now draw to the actual screen
242        self.use()
243
244        arcade.set_viewport(self.view_left,
245                            SCREEN_WIDTH + self.view_left,
246                            self.view_bottom,
247                            SCREEN_HEIGHT + self.view_bottom)
248
249        self.star_sprite_list.draw()
250        self.enemy_sprite_list.draw()
251        self.bullet_sprite_list.draw()
252        self.player_list.draw()
253
254        # Draw the ground
255        arcade.draw_line(0, 0, PLAYING_FIELD_WIDTH, 0, arcade.color.WHITE)
256
257        # Draw a background for the minimap
258        arcade.draw_rectangle_filled(SCREEN_WIDTH - SCREEN_WIDTH / 2 + self.view_left,
259                                        SCREEN_HEIGHT - MINIMAP_HEIGHT + MINIMAP_HEIGHT / 2 + self.view_bottom,
260                                        SCREEN_WIDTH,
261                                        MINIMAP_HEIGHT,
262                                        arcade.color.DARK_GREEN)
263
264        # --- Mini-map related ---
265
266        # Draw the minimap
267        self.mini_map_color_attachment.use(0)
268        self.mini_map_rect.render(self.program)
269
270        # Draw a rectangle showing where the screen is
271        width_ratio = SCREEN_WIDTH / PLAYING_FIELD_WIDTH
272        height_ratio = MINIMAP_HEIGHT / PLAYING_FIELD_HEIGHT
273        width = width_ratio * SCREEN_WIDTH
274        height = height_ratio * MAIN_SCREEN_HEIGHT
275
276        x = (self.view_left + SCREEN_WIDTH / 2) * width_ratio + self.view_left
277        y = (SCREEN_HEIGHT - MINIMAP_HEIGHT) + self.view_bottom + height / 2 + (MAIN_SCREEN_HEIGHT / PLAYING_FIELD_HEIGHT) * self.view_bottom
278
279        arcade.draw_rectangle_outline(center_x=x, center_y=y,
280                                        width=width, height=height,
281                                        color=arcade.color.WHITE)
282
283    def on_update(self, delta_time):
284        """ Movement and game logic """
285
286        # Calculate speed based on the keys pressed
287        if self.up_pressed and not self.down_pressed:
288            self.player_sprite.accelerate_up()
289        elif self.down_pressed and not self.up_pressed:
290            self.player_sprite.accelerate_down()
291
292        if self.left_pressed and not self.right_pressed:
293            self.player_sprite.accelerate_left()
294        elif self.right_pressed and not self.left_pressed:
295            self.player_sprite.accelerate_right()
296
297        # Call update to move the sprite
298        self.player_list.update()
299        self.bullet_sprite_list.update()
300
301        for bullet in self.bullet_sprite_list:
302            enemy_hit_list = arcade.check_for_collision_with_list(bullet, self.enemy_sprite_list)
303            for enemy in enemy_hit_list:
304                enemy.remove_from_sprite_lists()
305                for i in range(10):
306                    particle = Particle(4, 4, arcade.color.RED)
307                    while particle.change_y == 0 and particle.change_x == 0:
308                        particle.change_y = random.randrange(-2, 3)
309                        particle.change_x = random.randrange(-2, 3)
310                        particle.center_x = enemy.center_x
311                        particle.center_y = enemy.center_y
312                        self.bullet_sprite_list.append(particle)
313
314        # Scroll left
315        left_boundary = self.view_left + VIEWPORT_MARGIN
316        if self.player_sprite.left < left_boundary:
317            self.view_left -= left_boundary - self.player_sprite.left
318
319        # Scroll right
320        right_boundary = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN
321        if self.player_sprite.right > right_boundary:
322            self.view_left += self.player_sprite.right - right_boundary
323
324        # Scroll up
325        self.view_bottom = DEFAULT_BOTTOM_VIEWPORT
326        top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN - MINIMAP_HEIGHT
327        if self.player_sprite.top > top_boundary:
328            self.view_bottom += self.player_sprite.top - top_boundary
329
330        self.view_left = int(self.view_left)
331        self.view_bottom = int(self.view_bottom)
332
333    def on_key_press(self, key, modifiers):
334        """Called whenever a key is pressed. """
335
336        if key == arcade.key.UP:
337            self.up_pressed = True
338        elif key == arcade.key.DOWN:
339            self.down_pressed = True
340        elif key == arcade.key.LEFT:
341            self.left_pressed = True
342        elif key == arcade.key.RIGHT:
343            self.right_pressed = True
344        elif key == arcade.key.SPACE:
345            # Shoot out a bullet/laser
346            bullet = arcade.SpriteSolidColor(35, 3, arcade.color.WHITE)
347            bullet.center_x = self.player_sprite.center_x
348            bullet.center_y = self.player_sprite.center_y
349            bullet.change_x = max(12, abs(self.player_sprite.change_x) + 10)
350
351            if not self.player_sprite.face_right:
352                bullet.change_x *= -1
353
354            self.bullet_sprite_list.append(bullet)
355
356    def on_key_release(self, key, modifiers):
357        """Called when the user releases a key. """
358
359        if key == arcade.key.UP:
360            self.up_pressed = False
361        elif key == arcade.key.DOWN:
362            self.down_pressed = False
363        elif key == arcade.key.LEFT:
364            self.left_pressed = False
365        elif key == arcade.key.RIGHT:
366            self.right_pressed = False
367
368
369def main():
370    """ Main method """
371    window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
372    window.setup()
373    arcade.run()
374
375
376if __name__ == "__main__":
377    main()