Space invaders in Pyxel and Python
Calvin Robinson will always extend the hand of friendship – unless menacing aliens are descending from the skies, in which case… blast ‘em!
Calvin Robinson takes us through creating a retro space shooter video game in Python and the Pyxel advanced toolset.
Space Invaders is probably the most famous space shooter of all time, released way back in 1978 for arcades. Originally called Uchu¯ Shinryaku-sha in Japan, Space Invaders was the first fixed shooter, and paved the way for shooting games as a new genre. The idea being that the player controlled a space ship at the bottom of the screen, and it was their job to move left and right to avoid enemies, while shooting rockets up the screen to destroy them. Nice and simple fun, so let’s have a go at creating our own take on this retro classic.
We’ll be using Python to code our game, since it’s one of the most versatile, accessible high-level programming languages on the market. Python is completely free and should be installed on most distros by default. However, there are multiple versions of Python, so let’s make sure we’re running the correct version by launching a terminal and typing sudo apt-get install python3 if you’re on a Debian based distro. For other distributions, check your software download tool.
Once installed, we can either use a text editor or the built-in Python IDLE to code. If using Python IDLE, remember to press File>new File to start an editable script document because by default Python IDLE opens up a shell window, which won’t save any changes. If you’d prefer to use your favourite text editor, just be sure to save the file with the .py extension, and it can be run from terminal with the command python3 filename.py .
We often use Pygame in these tutorials because it offers a toolset that enables us to begin coding our game pretty much immediately, without messing around creating basic fundamental shapes or physics. This time, we’re using a retro game engine called Pyxel. Pyxel is an advanced toolset, complete with image banks, tilesets, an image and sound editor. It’s fantastic for creating retro games. We’re not going too deep this time around, but it’s definitely a module worth exploring further. To install Pyxel, open a terminal and run pip3 install pyxel . Let’s begin by importing the random module built into Python, along with our newly installed pyxel toolset, and then declare and initialise all of our global variables: from random import random import pyxel
SCENE_TITLE = 0
SCENE_PLAY = 1
SCENE_GAMEOVER = 2
STAR_COUNT = 100
STAR_COLOR_HIGH = 12
STAR_COLOR_LOW = 5
PLAYER_WIDTH = 8
PLAYER_HEIGHT = 8
PLAYER_SPEED = 2
BULLET_WIDTH = 2
BULLET_HEIGHT = 8
BULLET_COLOR = 11
BULLET_SPEED = 4
ENEMY_WIDTH = 8
ENEMY_HEIGHT = 8
ENEMY_SPEED = 1.5
BLAST_START_RADIUS = 1
BLAST_END_RADIUS = 8
BLAST_COLOR_IN = 7
BLAST_COLOR_OUT = 10
enemy_list = [] bullet_list = []
blast_list = []
We’ve set up some placeholders and basic settings, most of which are self-explanatory. We have set values for our scene, points, the player, ammo and our enemies.
Sticking with best practice, we’ll be using Object Oriented Programming throughout this tutorial. Not only will we be using classes from pyxel, but we’ll be creating our own functions so that we don’t need to repeat code. OOP is considered the most efficient way of programming video games.
Let’s get our code sorted for drawing, updating and erasing lists, since everything in the game will be run through arrays we declared and initialised earlier:
def update_list(list): for elem in list: elem.update()
def draw_list(list): for elem in list: elem.draw()
def cleanup_list(list): i = 0 while i < len(list): elem = list[i] if not elem.alive: list.pop(i) else: i += 1
Now we can begin setting up the game environment. Let’s add a new class for the background: class Background: def __init__(self): self.star_list = [] for i in RANGE(STAR_COUNT): self.star_list.append(
(random() * pyxel.width, random() * pyxel. height, random() * 1.5 + 1) )
def update(self): for i, (x, y, speed) in enumerate(self.star_list): y += speed if y >= pyxel.height: y -= pyxel.height self.star_list[i] = (x, y, speed)
def draw(self): for (x, y, speed) in self.star_list: pyxel.pset(x, y, STAR_COLOR_HIGH if speed > 1.8 else STAR_COLOR_LOW)
Upon initialisation, we’re randomising the appearance of the stars with a for loop and the random module, appending them into our star list. We’ve also added functions and drawing and updating the stars in our background.
Player one has entered the game
Next, we need to introduce our player character: class Player: def __init__(self, x, y): self.x = x self.y = y self.w = PLAYER_WIDTH self.h = PLAYER_HEIGHT self.alive = True
def update(self): if pyxel.btn(pyxel.key_left): self.x -= PLAYER_SPEED
if pyxel.btn(pyxel.key_right): self.x += PLAYER_SPEED
if pyxel.btn(pyxel.key_up): self.y -= PLAYER_SPEED
if pyxel.btn(pyxel.key_down):
self.y += PLAYER_SPEED self.x = max(self.x, 0) self.x = min(self.x, pyxel.width - self.w) self.y = max(self.y, 0) self.y = min(self.y, pyxel.height - self.h)
if pyxel.btnp(pyxel.key_space):
Bullet( self.x + (PLAYER_WIDTH - BULLET_WIDTH) / 2, self.y - BULLET_HEIGHT / 2 )
pyxel.play(0, 0)
def draw(self):
pyxel.blt(self.x, self.y, 0, 0, 0, self.w, self.h, 0)
Upon initialisation we’re setting the x- and y-coordinates. We’re pulling the width and height in from the global variables we set earlier, and most importantly, we’re declaring our player character to be alive. Like in the other class, we’re setting a function to draw and then update our player character. The update function will move our player character when the appropriate key is pressed, at a speed based on the speed variable we declared at the beginning.
That’s our player sorted, let’s get our bullet ammo set up, much in the same way as our player class: class Bullet: def __init__(self, x, y): self.x = x self.y = y self.w = BULLET_WIDTH self.h = BULLET_HEIGHT self.alive = True
bullet_list.append(self)
def update(self): self.y -= BULLET_SPEED
if self.y + self.h - 1 < 0: self.alive = False
def draw(self): pyxel.rect(self.x, self.y, self.w, self.h, BULLET_
COLOR)
We’ll need a class for our enemies, too. The beauty of using classes, in an Object Oriented Programming methodology is that we can spawn multiple instances
(objects) of the same thing, without duplicating any code. class Enemy: def __init__(self, x, y): self.x = x self.y = y self.w = ENEMY_WIDTH self.h = ENEMY_HEIGHT self.dir = 1 self.alive = True self.offset = int(random() * 60)
enemy_list.append(self)
def update(self):
if (pyxel.frame_count + self.offset) % 60 < 30: self.x += ENEMY_SPEED self.dir = 1 else: self.x -= ENEMY_SPEED self.dir = -1
self.y += ENEMY_SPEED
if self.y > pyxel.height - 1: self.alive = False
def draw(self): pyxel.blt(self.x, self.y, 0, 8, 0, self.w * self.dir, self.h, 0) The last of these series of classes is the destructive blast itself: class Blast: def __init__(self, x, y): self.x = x self.y = y self.radius = BLAST_START_RADIUS self.alive = True
blast_list.append(self)
def update(self): self.radius += 1
if self.radius > BLAST_END_RADIUS: self.alive = False
def draw(self): pyxel.circ(self.x, self.y, self.radius, BLAST_COLOR_
IN)
pyxel.circb(self.x, self.y, self.radius, BLAST_COLOR_
OUT)
That’s all of our game elements set up, we can now begin coding the application window itself, by tapping into the pyxel module. The rest of the code features in this tutorial belongs in the App class, so let’s create it:
class App: def __init__(self): pyxel.init(120, 160, caption="retro Shooter Game")
Of course, we could change the caption to anything we like, and that would appear in the titlebar of our game window.
We’re now going to use the pyxel image sets to create our sprites:
pyxel.image(0).set(
0,
0,
[
“00c00c00”, “0c7007c0”, “0c7007c0”, “c703b07c”, “77033077”, “785cc587”, “85c77c58”, “0c0880c0”, ], )
pyxel.image(0).set( 8,
0, [
“00088000”, “00ee1200”, “08e2b180”, “02882820”, “00222200”, “00012280”, “08208008”, “80008000”, ],
)
Likewise for the game sounds: pyxel.sound(0).set("a3a2c1a1”, “p”, “7”, “s”, 5) pyxel.sound(1).set("a3a2c2c2”, “n”, “7742”, “s”, 10)
The image sets and sound effects are completely customisable, but it does take further experimentation with the pyxel game engine.
Background information
Set the scene and draw a background and our player: self.scene = SCENE_TITLE self.score = 0 self.background = Background() self.player = Player(pyxel.width / 2, pyxel.height 20)
pyxel.run(self.update, self.draw)
This will need updating – as with everything we draw on to the screen – in order to ensure the player always sees the latest version when something changes:
def update(self): if pyxel.btnp(pyxel.key_q):
pyxel.quit() self.background.update() if self.scene == SCENE_TITLE:
self.update_title_scene() elif self.scene == SCENE_PLAY:
self.update_play_scene() elif self.scene == SCENE_GAMEOVER:
self.update_gameover_scene()
Updating our title scene is simple:
def update_title_scene(self): if pyxel.btnp(pyxel.key_enter):
self.scene = SCENE_PLAY
Updating our main game content, on the other hand, is a little more complicated:
def update_play_scene(self): if pyxel.frame_count % 6 == 0:
Enemy(random() * (pyxel.width - PLAYER_
WIDTH), 0) for a in enemy_list: for b in bullet_list: if (a.x + a.w > b.x and b.x + b.w > a.x and a.y + a.h > b.y and b.y + b.h > a.y): a.alive = False b.alive = False blast_list.append(blast(a.x + ENEMY_WIDTH / 2, a.y + ENEMY_HEIGHT / 2)) pyxel.play(1, 1) self.score += 10 for enemy in enemy_list: if (self.player.x + self.player.w > enemy.x and enemy.x + enemy.w > self.player.x
and self.player.y + self.player.h > enemy.y and enemy.y + enemy.h > self.player.y): enemy.alive = False blast_list.append(
Blast( self.player.x + PLAYER_WIDTH / 2, self.player.y + PLAYER_HEIGHT / 2,)) pyxel.play(1, 1) self.scene = SCENE_GAMEOVER self.player.update() update_list(bullet_list) update_list(enemy_list) update_list(blast_list) cleanup_list(enemy_list) cleanup_list(bullet_list) cleanup_list(blast_list)
A lot is going on here. We’re spawning enemies at random locations, creating blasts when they’re destroyed and cleaning up the sprites (remove the resources) when they’re no longer visible.
We’ll need to update our game over scene, too: def update_gameover_scene(self): update_list(bullet_list) update_list(enemy_list) update_list(blast_list) cleanup_list(enemy_list) cleanup_list(bullet_list) cleanup_list(blast_list) if pyxel.btnp(pyxel.key_enter): self.scene = SCENE_PLAY self.player.x = pyxel.width / 2 self.player.y = pyxel.height - 20 self.score = 0 enemy_list.clear() bullet_list.clear() blast_list.clear()
Here we’re just doing some housekeeping, restoring the lists, deleting unused sprites, and waiting for the enter key to be pressed so we can restart the game. Finally, we need to code the draw functions. It’s usually easier to draw something than to keep it updated, so these are the program’s shortest functions: def draw(self): pyxel.cls(0)
self.background.draw()
if self.scene == SCENE_TITLE:
self.draw_title_scene() elif self.scene == SCENE_PLAY:
self.draw_play_scene() elif self.scene == SCENE_GAMEOVER:
self.draw_gameover_scene()
pyxel.text(39, 4, “SCORE {:5}”.format(self.score), 7) We’ll also need to draw the title scene, play scene and ‘game over’ scene. def draw_title_scene(self): pyxel.text(35, 66, “Start Shooter”, pyxel.frame_ count % 16)
pyxel.text(31, 126, “- PRESS ENTER -”, 13)
That’s the text to display on the start screen, but we can change the text to something more suitable: “Shooter Game – Press Enter to begin”, for example. The final two functions will prepare our bullets, enemies and blast effects for the game and game over scenes:
def draw_play_scene(self): self.player.draw() draw_list(bullet_list) draw_list(enemy_list) draw_list(blast_list) def draw_gameover_scene(self): draw_list(bullet_list) draw_list(enemy_list) draw_list(blast_list)
And finally, the text for our game over screen, again, entirely customisable:
pyxel.text(43, 66, “GAME OVER”, 8) pyxel.text(31, 126, “- PRESS ENTER -”, 13)
That’s it. One more line left, make sure this one isn’t indented. App()
will call the App class we’ve just finished, and kick everything off. Now press F5 to save and run our program, and if all went to plan we should have a functioning graphical retro shooter game. Good luck, have lots of fun!