Home¶
In this project we will be making an old school style video game for the Adafruit PyBadge. We will be using CircuitPython and the stage library to create a Asteroids like game. The stage library makes it easy to make classic video games, with helper libraries for sound, sprites and collision detection. The game will also work on other variants of PyBadge hardware, like the PyGamer and the EdgeBadge. The full completed game code with all the assets can be found here.
The guide assumes that you have prior coding experience, hopefully in Python. It is designed to use just introductory concepts. No Object Oriented Programming (OOP) are used so that students in particular that have completed their first course in coding and know just variables, if statements, loops and functions will be able to follow along.
Parts
You will need the following items:
Adafruit PyBadge for MakeCode Arcade, CircuitPython or Arduino
PRODUCT ID: 4200
Pink and Purple Braided USB A to Micro B Cable - 2 meter long
PRODUCT ID: 4148
So you can move your CircuitPython code onto the PyBadge.
You might also want:
Lithium Ion Polymer Battery Ideal For Feathers - 3.7V 400mAh
PRODUCT ID: 3898
So that you can play the game without having it attached to a computer with a USB cable.
Mini Oval Speaker - 8 Ohm 1 Watt
PRODUCT ID: 3923
If you want lots of sound. Be warned, the built in speaker is actually pretty loud.
I did not create this case. I altered Adafruit’s design. One of the screw posts was hitting the built in speaker and the case was not closing properly. I also added a piece of plastic over the display ribbon cable, to keep it better protected. You will need 4 x 3M screws to hold the case together.
Install CircuitPython¶
Before doing anything else, you should delete everything already on your PyBadge and install the latest version of CircuitPython onto it. This ensures you have a clean build with all the latest updates and no leftover files floating around. Adafruit has an excellent quick start guide here to step you through the process of getting the latest build of CircuitPython onto your PyBadge. Adafruit also has a more detailed comprehensive version of all the steps with complete explanations here you can use, if this is your first time loading CircuitPython onto your PyBadge.
Just a reminder, if you are having any problems loading CircuitPython onto your PyBadge, ensure that you are using a USB cable that not only provides power, but also provides a data link. Many USB cables you buy are only for charging, not transfering data as well. Once the CircuitPython is all loaded, come on back to continue the tutorial.
Your IDE¶
One of the great things about CircuitPython hardware is that it just automatically shows up as a USB drive when you attach it to your computer. This means that you can access and save your code using any text editor. This is particularly helpful in schools, where computers are likely to be locked down so students can not load anything. Also students might be using Chromebooks, where only “authorized” Chrome extensions can be loaded.
If you are working on a Chromebook, the easiest way to start coding is to just use the built in Text app. As soon as you open or save a file with a *.py
extension, it will know it is Python code and automatically start syntax highlighting.
If you are using a non-Chromebook computer, your best beat for an IDE is Mu. You can get it for Windows, Mac, Raspberry Pi and Linux. It works seamlessly with CircuitPython and the serial console will give you much needed debugging information. You can download Mu here.
Since with CircuitPython devices you are just writing Python files to a USB drive, you are more than welcome to use any IDE that you are familiar using.
Hello, World!¶
Yes, you know that first program you should always run when starting a new coding adventure, just to ensure everything is running correctly! Once you have access to your IDE and you have CircuitPython loaded, you should make sure everything is working before you move on. To do this we will do the traditional “Hello, World!” program. By default CircuitPython looks for a file called code.py
in the root directory of the PyBadge to start up. You will place the following code in the code.py
file:
1 | print("Hello, World!")
|
As soon as you save the file onto the PyBadge, the screen should flash and you should see something like:
Although this code does work just as is, it is always nice to ensure we are following proper coding conventions, including style and comments. Here is a better version of Hello, World! You will notice that I have a call to a main()
function. This is common in Python code but not normally seen in CircuitPython. I am including it because by breaking the code into different functions to match different scenes, eventually will be really helpful.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #!/usr/bin/env python3
# Created by : Mr. Coxall
# Created on : January 2020
# This program prints out Hello, World! onto the PyBadge
def main():
# this function prints out Hello, World! onto the PyBadge
print("Hello, World!")
if __name__ == "__main__":
main()
|
Congratulations, we are ready to start.
Image Banks¶
Before we can start coding a video game, we need to have the artwork and other assets. The stage library from CircuitPython we will be using is designed to import an “image bank”. These image banks are 16 sprites staked on top of each other, each with a resolution of 16x16 pixels. This means the resulting image bank is 16x256 pixels in size. Also the image bank must be saved as a 16-color BMP file, with a pallet of 16 colors. To get a sprite image to show up on the screen, we will load an image bank into memory, select the image from the bank we want to use and then tell CircuitPython where we would like it placed on the screen.
For sound, the stage library can play back *.wav
files in PCM 16-bit Mono Wave files at 22KHz sample rate. Adafruit has a great learning guide on how to save your sound files to the correct format here.
If you do not want to get into creating your own assets, other people have already made assets available to use. All the assets for this guide can be found in the GitHub repo here:
- ship and lasers image bank
- asteroids and enemies image bank
- background image bank
- coin sound
- pew sound
- boom sound
- crash sound
Please download the assets and place them on the PyBadge, in the root directory. Your previoud “Hello, World!” program should restart and run again each time you load a new file onto the PyBadge, hopefully with no errors once more.
Assets from other people can be found here.
Game¶
The game scene starts out with the player/ship spawning in the middle of the screen. The player can move around using the d-pad and shoot lasers using the a button. Once the game starts, Asteroids will come down from the top of the screen, while two types of enemies come from the left side of the screen moving right. The aim of the game is to kill/avoid all enemies/asteroids before losing your three lives. On the main menu, there is an option to play on easy or hard mode (a for easy, b for hard(code shown in menu part of this documentation)). Destroying asteroids nets you five points on easy(fifteen points for hard), while enemies net you ten points on easy(thirty points for hard). The game gets progressively harder as you kill more enemies and asteroids. Once all three of your lives are gone, GAME OVER!
Here is the code for the main game:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | def game_scene(diff_mul):
# this function is the game scene
# background image bank ready
background_bank = stage.Bank.from_bmp16("background.bmp")
image_bank_0 = stage.Bank.from_bmp16("meteor.bmp")
image_bank_1 = stage.Bank.from_bmp16("ship-and-lasers.bmp")
background = stage.Grid(background_bank, constants.SCREEN_GRID_X, constants.SCREEN_GRID_Y)
for x_location in range(constants.SCREEN_GRID_X):
for y_location in range(constants.SCREEN_GRID_Y):
tile_picked = random.randint(0, 15)
background.tile(x_location, y_location, tile_picked)
a_button = constants.button_state["button_up"]
shoot_sound = open("pew.wav", 'rb')
boom_sound = open("boom.wav", 'rb')
sound = ugame.audio
sound.stop()
sound.mute(False)
# Buttons that you want to keep state information on
a_button = constants.button_state["button_up"]
start_button = constants.button_state["button_up"]
select_button = constants.button_state["button_up"]
sprites = []
ship = stage.Sprite(image_bank_1, 0, 80, 64)
sprites.insert(0, ship) # insert at the top of sprite list
score = 0
scoretext = []
score_text = stage.Text(width=29, height=14, font=None,
palette=constants.SCORE_PALETTE, buffer=None)
score_text.cursor(0, 0)
score_text.move(1, 118)
score_text.text("Points: {0}".format(score))
scoretext.append(score_text)
lives = 3
livestext = []
lives_text = stage.Text(width=29, height=14, font=None,
palette=constants.LIVES_PALETTE, buffer=None)
lives_text.cursor(0, 0)
lives_text.move(1, 1)
lives_text.text("Lives: {0}".format(lives))
livestext.append(lives_text)
asteroids = []
for asteroids_number in range(constants.TOTAL_ASTEROIDS * diff_mul):
single_asteroid = stage.Sprite(image_bank_0, 0, constants.OFF_TOP_SCREEN, constants.OFF_TOP_SCREEN)
asteroids.append(single_asteroid)
enemy_1 = []
for enemy_number_1 in range(constants.TOTAL_ENEMY_1 * diff_mul):
single_1 = stage.Sprite(image_bank_0, 1, constants.OFF_TOP_SCREEN, constants.OFF_TOP_SCREEN)
enemy_1.append(single_1)
enemy_2 = []
for enemy_number_2 in range(constants.TOTAL_ENEMY_2 * diff_mul):
single_2 = stage.Sprite(image_bank_0, 2, constants.OFF_TOP_SCREEN, constants.OFF_TOP_SCREEN)
enemy_2.append(single_2)
lasers = []
for laser_number in range(constants.TOTAL_NUMBER_OF_LASERS):
single_laser = stage.Sprite(image_bank_1, 8, constants.OFF_TOP_SCREEN, constants.OFF_TOP_SCREEN)
lasers.append(single_laser)
enemy_count = 1
show_enemy(asteroids)
show_enemy_2(enemy_1)
show_enemy_3(enemy_2)
death_mul = 1
# set frame rate to 60fps
game = stage.Stage(ugame.display, 60)
# set layers, items show up in order
game.layers = sprites + enemy_1 + enemy_2 + asteroids + lasers + scoretext + livestext + [background]
# render background and sprite list
game.render_block()
# repeat forever, game loop
while True:
# get user input
keys = ugame.buttons.get_pressed()
if keys & ugame.K_X != 0:
if a_button == constants.button_state["button_up"]:
a_button = constants.button_state["button_just_pressed"]
elif a_button == constants.button_state["button_just_pressed"]:
a_button = constants.button_state["button_still_pressed"]
else:
if a_button == constants.button_state["button_still_pressed"]:
a_button = constants.button_state["button_released"]
else:
a_button = constants.button_state["button_up"]
if a_button == constants.button_state["button_just_pressed"]:
for laser_number in range(len(lasers)):
if lasers[laser_number].x < 0:
lasers[laser_number] .move(ship.x, ship.y)
sound.stop()
sound.play(shoot_sound)
break
for laser_number in range(len(lasers)):
if lasers[laser_number].x > 0:
if ship.rotation == 0:
lasers[laser_number].set_frame(rotation=0)
lasers[laser_number].move(lasers[laser_number].x, lasers[laser_number].y - constants.LASER_SPEED)
if lasers[laser_number].y < constants.OFF_SCREEN_Y:
lasers[laser_number].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
elif ship.rotation == 1:
lasers[laser_number].set_frame(rotation=1)
lasers[laser_number].move(lasers[laser_number].x + constants.LASER_SPEED, lasers[laser_number].y)
if lasers[laser_number].x > 160:
lasers[laser_number].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
elif ship.rotation == 2:
lasers[laser_number].set_frame(rotation=0)
lasers[laser_number].move(lasers[laser_number].x, lasers[laser_number].y + constants.LASER_SPEED)
if lasers[laser_number].y > 128:
lasers[laser_number].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
elif ship.rotation == 3:
lasers[laser_number].set_frame(rotation=1)
lasers[laser_number].move(lasers[laser_number].x - constants.LASER_SPEED, lasers[laser_number].y)
if lasers[laser_number].x < 5:
lasers[laser_number].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
# Move ship right
if keys & ugame.K_RIGHT:
state_of_button = 2
if ship.x > constants.SCREEN_X - constants.SPRITE_SIZE:
ship.move(constants.SCREEN_X - constants.SPRITE_SIZE, ship.y)
else:
ship.move(ship.x + constants.SHIP_MOVEMENT_SPEED * death_mul * diff_mul, ship.y)
ship.set_frame(rotation=1)
pass
# Move ship left
if keys & ugame.K_LEFT:
state_of_button = 4
if ship.x < 5:
ship.move(5, ship.y)
else:
ship.move(ship.x - constants.SHIP_MOVEMENT_SPEED * death_mul * diff_mul, ship.y)
ship.set_frame(rotation=3)
pass
# Move ship up
if keys & ugame.K_UP:
state_of_button = 1
if ship.y < 0:
ship.move(ship.x, 0)
else:
ship.move(ship.x, ship.y - constants.SHIP_MOVEMENT_SPEED * death_mul * diff_mul)
ship.set_frame(rotation=0)
pass
# Move ship down
if keys & ugame.K_DOWN:
state_of_button = 3
if ship.y > constants.SCREEN_Y - constants.SPRITE_SIZE:
ship.move(ship.x, constants.SCREEN_Y - constants.SPRITE_SIZE * death_mul * diff_mul)
else:
ship.move(ship.x, ship.y + constants.SHIP_MOVEMENT_SPEED * death_mul * diff_mul)
ship.set_frame(rotation=2)
pass
# update game logic
for asteroid_number in range(len(asteroids)):
if asteroids[asteroid_number].x > 0:
asteroids[asteroid_number].move(asteroids[asteroid_number].x, asteroids[asteroid_number].y + constants.ENEMY_SPEED * diff_mul * death_mul)
if asteroids[asteroid_number].y > constants.SCREEN_Y:
asteroids[asteroid_number].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
show_enemy(asteroids)
for enemy_number_1 in range(len(enemy_1)):
if enemy_1[enemy_number_1].y > 0:
enemy_1[enemy_number_1].move(enemy_1[enemy_number_1].x + constants.ENEMY_SPEED * death_mul * diff_mul, enemy_1[enemy_number_1].y)
if enemy_1[enemy_number_1].x > constants.SCREEN_X:
enemy_1[enemy_number_1].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
show_enemy_2(enemy_1)
for enemy_number_2 in range(len(enemy_2)):
if enemy_2[enemy_number_2].y > 0:
enemy_2[enemy_number_2].move(enemy_2[enemy_number_2].x + constants.ENEMY_SPEED * death_mul * diff_mul, enemy_2[enemy_number_2].y)
if enemy_2[enemy_number_2].x > constants.SCREEN_X:
enemy_2[enemy_number_2].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
show_enemy_3(enemy_2)
for laser_number in range(len(lasers)):
if lasers[laser_number].x > 0:
for enemy_number_1 in range(len(enemy_1)):
if enemy_1[enemy_number_1].x > 0:
if stage.collide(lasers[laser_number].x, lasers[laser_number].y,
lasers[laser_number].x + 16, lasers[laser_number].y + 16,
enemy_1[enemy_number_1].x, enemy_1[enemy_number_1].y,
enemy_1[enemy_number_1].x + 16, enemy_1[enemy_number_1].y + 16):
enemy_1[enemy_number_1].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
lasers[laser_number].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
score += 10*diff_mul
score_text.clear()
score_text.cursor(0,0)
score_text.move(1, 118)
score_text.text("Points: {0}".format(score))
sound.stop()
sound.play(boom_sound)
show_enemy_2(enemy_1)
show_enemy_2(enemy_1)
death_mul += (1/30)
for enemy_number_2 in range(len(enemy_2)):
if enemy_2[enemy_number_2].x > 0:
if stage.collide(lasers[laser_number].x, lasers[laser_number].y,
lasers[laser_number].x + 16, lasers[laser_number].y + 16,
enemy_2[enemy_number_2].x, enemy_2[enemy_number_2].y,
enemy_2[enemy_number_2].x + 16, enemy_2[enemy_number_2].y + 16):
enemy_2[enemy_number_2].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
lasers[laser_number].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
score += 10*diff_mul
score_text.clear()
score_text.cursor(0,0)
score_text.move(1, 118)
score_text.text("Points: {0}".format(score))
sound.stop()
sound.play(boom_sound)
show_enemy_3(enemy_2)
show_enemy_3(enemy_2)
death_mul += (1/30)
for asteroid_number in range(len(asteroids)):
if asteroids[asteroid_number].x > 0:
if stage.collide(lasers[laser_number].x, lasers[laser_number].y,
lasers[laser_number].x + 16, lasers[laser_number].y + 16,
asteroids[asteroid_number].x, asteroids[asteroid_number].y,
asteroids[asteroid_number].x + 16, asteroids[asteroid_number].y + 16):
asteroids[asteroid_number].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
lasers[laser_number].move(constants.OFF_SCREEN_X, constants.OFF_SCREEN_Y)
score += 5*diff_mul
score_text.clear()
score_text.cursor(0,0)
score_text.move(1, 118)
score_text.text("Points: {0}".format(score))
sound.stop()
sound.play(boom_sound)
show_enemy(asteroids)
show_enemy(asteroids)
death_mul += (1/30)
for enemy_number_1 in range(len(enemy_1)):
if enemy_1[enemy_number_1].x > 0:
if stage.collide(enemy_1[enemy_number_1].x, enemy_1[enemy_number_1].y,
enemy_1[enemy_number_1].x + 16, enemy_1[enemy_number_1].y + 16,
ship.x, ship.y,
ship.x + 16, ship.y + 16):
lives -= 1
ship.move(-100, -100)
sound.stop()
sound.play(boom_sound)
time.sleep(1)
if lives == 0:
game_over_scene(score)
else:
lives_text.clear()
lives_text.cursor(0,0)
lives_text.move(1, 1)
lives_text.text("Lives: {0}".format(lives))
ship.move (random.randint(16, 146), random.randint(16, 106))
for enemy_number_2 in range(len(enemy_2)):
if enemy_2[enemy_number_2].x > 0:
if stage.collide(enemy_2[enemy_number_2].x, enemy_2[enemy_number_2].y,
enemy_2[enemy_number_2].x + 16, enemy_2[enemy_number_2].y + 16,
ship.x, ship.y,
ship.x + 16, ship.y + 16):
lives -= 1
ship.move(-100, -100)
sound.stop()
sound.play(boom_sound)
time.sleep(1)
if lives == 0:
game_over_scene(score)
else:
lives_text.clear()
lives_text.cursor(0,0)
lives_text.move(1, 1)
lives_text.text("Lives: {0}".format(lives))
ship.move (random.randint(16, 146), random.randint(16, 106))
for asteroid_number in range(len(asteroids)):
if asteroids[asteroid_number].x > 0:
if stage.collide(asteroids[asteroid_number].x, asteroids[asteroid_number].y,
asteroids[asteroid_number].x + 16, asteroids[asteroid_number].y + 16,
ship.x, ship.y,
ship.x + 16, ship.y + 16):
lives -= 1
ship.move(-100, -100)
sound.stop()
sound.play(boom_sound)
time.sleep(1)
if lives == 0:
game_over_scene(score)
else:
lives_text.clear()
lives_text.cursor(0,0)
lives_text.move(1, 1)
lives_text.text("Lives: {0}".format(lives))
ship.move (random.randint(16, 146), random.randint(16, 106))
# redraw sprite list
game.render_sprites(sprites + asteroids + enemy_1 + enemy_2 + lasers)
game.tick()
|
The full game code is here for full use for anybody wishing to make this game :)
Background¶
For the game, the background is space themed, just like the original arcade game(see home page link for example). To make the background show up for this game, we take the background image bank which is already created and take random images from the bank to paste it across the PyBadge screen. First, we set up our background image bank:
1 2 | # background image bank ready
background_bank = stage.Bank.from_bmp16("background.bmp")
|
Then, we create a loop where we take a single image from the background bank and paste it on the screen. This loop occurs until the background fills up the whole PyBadge screen:
1 2 3 4 5 | background = stage.Grid(background_bank, constants.SCREEN_GRID_X, constants.SCREEN_GRID_Y)
for x_location in range(constants.SCREEN_GRID_X):
for y_location in range(constants.SCREEN_GRID_Y):
tile_picked = random.randint(0, 15)
background.tile(x_location, y_location, tile_picked)
|
The background now shows up on the screen!
Ship/Player¶
As stated in the main game page, the player controls a ship using the d-pad. When a player presses a direction, the ship rotates to the direction in which the player is pressing(ex. player presses left, ship rotates and moves left). The player shoots lasers by pressing a, and the lasers also go in the direction that the ship is facing in. The controls for the game are very simple, thus a simple and fun game! Here is the code to get the ship showing up in the game:
1 2 3 | sprites = []
ship = stage.Sprite(image_bank_1, 0, 80, 64)
sprites.insert(0, ship) # insert at the top of sprite list
|
The code for moving the ship is in the main game page.