Linux Format

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 is a former assistant principal and computer science teacher and has a degree in Computer Games Design and Programmin­g.

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 programmin­g 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 distributi­ons, 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 immediatel­y, without messing around creating basic fundamenta­l 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 placeholde­rs and basic settings, most of which are self-explanator­y. We have set values for our scene, points, the player, ammo and our enemies.

Sticking with best practice, we’ll be using Object Oriented Programmin­g 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 programmin­g 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 initialise­d 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 environmen­t. 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 initialisa­tion, we’re randomisin­g 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 initialisa­tion we’re setting the x- and y-coordinate­s. We’re pulling the width and height in from the global variables we set earlier, and most importantl­y, 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 appropriat­e 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 Programmin­g methodolog­y is that we can spawn multiple instances

(objects) of the same thing, without duplicatin­g 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 destructiv­e 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 applicatio­n 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 customisab­le, but it does take further experiment­ation with the pyxel game engine.

Background informatio­n

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 complicate­d:

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 housekeepi­ng, 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 customisab­le:

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 functionin­g graphical retro shooter game. Good luck, have lots of fun!

 ??  ??
 ??  ?? An intro screen, ready for the game to begin.
An intro screen, ready for the game to begin.
 ??  ??
 ??  ?? Our player controls the rocket ship at the bottom of the screen, shooting the aliens
Our player controls the rocket ship at the bottom of the screen, shooting the aliens
 ??  ?? Killing unfriendly aliens results in explosions.
Killing unfriendly aliens results in explosions.
 ??  ?? Roam the screen with the arrow keys to shoot those pesky aliens..
Roam the screen with the arrow keys to shoot those pesky aliens..
 ??  ?? They think it’s all over… it is now!
They think it’s all over… it is now!

Newspapers in English

Newspapers from Australia