Welcome! Meet our Python Code Assistant, your new coding buddy. Why wait? Start exploring now!
Space Invaders is a classic arcade game that has entertained gamers for decades, and now you can build your version of this iconic title.
In this tutorial, we will cover the essentials of game development with Pygame, from setting up your development environment to creating the game's core mechanics. By the end of this tutorial, you'll have a fully functional Space Invaders game that you can customize and expand upon to make it your own. Let's dive into the first step: setting up your development environment.
Let's start by making sure Pygame is installed on your computer. Head to your terminal and install pygame
module using pip
:
$ pip install pygame
After that, create a directory for the game and create the following .py
file inside it; settings.py
, main.py
, world.py
, display.py
, alien.py
, ship.py
, and bullet.py
. Create a folder inside our game directory and call it assets
, where we will store our game assets, such as sprites. Here is the file structure of our code:
Now, we can start coding. Let's define our game variables in settings.py
:
# /* settings.py
WIDTH, HEIGHT = 720, 450
SPACE = 30
FONT_SIZE = 20
EVENT_FONT_SIZE = 60
NAV_THICKNESS = 50
CHARACTER_SIZE = 30
PLAYER_SPEED = 10
ENEMY_SPEED = 1
BULLET_SPEED = 15 # for both sides
BULLET_SIZE = 10
You can always tweak these parameters based on your needs. Next, let's create the main class of our game. This class will be responsible for calling and running the game loop:
# /* main.py
import pygame, sys
from settings import WIDTH, HEIGHT, NAV_THICKNESS
from world import World
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT + NAV_THICKNESS))
pygame.display.set_caption("Space Invader")
class Main:
def __init__(self, screen):
self.screen = screen
self.FPS = pygame.time.Clock()
def main(self):
world = World(self.screen)
while True:
self.screen.fill("black")
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
world.player_move(attack = True)
world.player_move()
world.update()
pygame.display.update()
self.FPS.tick(30)
if __name__ == "__main__":
play = Main(screen)
play.main()
From the name itself, the Main
class will be the main class of our game. It takes an argument of screen
which will serve as the game window for animating the game. The main()
function will run and update our game. It will initialize the World
first (our game world). To keep the game running without intentionally exiting, we put a while
loop inside it. Inside our loop, we place another loop (the for
loop), which will catch all the events going on inside our game window, especially when the player hits the exit button.
Let's create a class for game characters, the Ship
class for the player's character, and the Alien
class for the opponents of the player's character. Let's create the Ship
class first:
# /* ship.py
import pygame
from settings import PLAYER_SPEED, BULLET_SIZE
from bullet import Bullet
class Ship(pygame.sprite.Sprite):
def __init__(self, pos, size):
super().__init__()
self.x = pos[0]
self.y = pos[1]
# ship info
img_path = 'assets/ship/ship.png'
self.image = pygame.image.load(img_path)
self.image = pygame.transform.scale(self.image, (size, size))
self.rect = self.image.get_rect(topleft = pos)
self.mask = pygame.mask.from_surface(self.image)
self.ship_speed = PLAYER_SPEED
# ship status
self.life = 3
self.player_bullets = pygame.sprite.Group()
def move_left(self):
self.rect.x -= self.ship_speed
def move_up(self):
self.rect.y -= self.ship_speed
def move_right(self):
self.rect.x += self.ship_speed
def move_bottom(self):
self.rect.y += self.ship_speed
def _shoot(self):
specific_pos = (self.rect.centerx - (BULLET_SIZE // 2), self.rect.y)
self.player_bullets.add(Bullet(specific_pos, BULLET_SIZE, "player"))
def update(self):
self.rect = self.image.get_rect(topleft=(self.rect.x, self.rect.y))
We start by importing the pygame
module and variables PLAYER_SPEED
, and BULLET_SIZE
in settings.py
. We also import the Bullet
class, which we'll create later.
The Ship
class represents the player's spaceship in the game. It inherits from pygame.sprite.Sprite
, indicating that it's a game object that can be easily managed within Pygame. The constructor __init__()
sets up the ship's initial properties, such as its position, image, mask, speed, and other attributes.
The ship's position and collision mask are also initialized. The speed of the ship is set to the value defined in PLAYER_SPEED
. The ship's life is initialized to 3, and a group for player bullets is created. The functions move_left()
, move_up()
, move_right()
, and move_bottom()
are responsible for the ship's movement and the _shoot()
, from the name itself, is responsible for shooting. To apply all the changes in movement, we have the update()
function to do so.
Now let's move on to creating the class for our opponents, the Alien
class:
# /* alien.py
import pygame
from settings import BULLET_SIZE
from bullet import Bullet
class Alien(pygame.sprite.Sprite):
def __init__(self, pos, size, row_num):
super().__init__()
self.x = pos[0]
self.y = pos[1]
# alien info
img_path = f'assets/aliens/{row_num}.png'
self.image = pygame.image.load(img_path)
self.image = pygame.transform.scale(self.image, (size, size))
self.rect = self.image.get_rect(topleft = pos)
self.mask = pygame.mask.from_surface(self.image)
self.move_speed = 5
self.to_direction = "right"
# alien status
self.bullets = pygame.sprite.GroupSingle()
def move_left(self):
self.rect.x -= self.move_speed
def move_right(self):
self.rect.x += self.move_speed
def move_bottom(self):
self.rect.y += self.move_speed
def _shoot(self):
specific_pos = (self.rect.centerx - (BULLET_SIZE // 2), self.rect.centery)
self.bullets.add(Bullet(specific_pos, BULLET_SIZE, "enemy"))
def update(self):
self.rect = self.image.get_rect(topleft=(self.rect.x, self.rect.y))
The Alien
class works the same as the Ship
class. The only difference between them is they have different movement speeds, no ability to move backward, and can only shoot one after the other explodes or out of the frame.
Now, let's move on to making the Bullet
class:
# /* bullet.py
import pygame
from settings import BULLET_SPEED, HEIGHT
class Bullet(pygame.sprite.Sprite):
def __init__(self, pos, size, side):
super().__init__()
self.x = pos[0]
self.y = pos[1]
# bullet info
img_path = f'assets/bullet/{side}-bullet.png'
self.image = pygame.image.load(img_path)
self.image = pygame.transform.scale(self.image, (size, size))
self.rect = self.image.get_rect(topleft = pos)
self.mask = pygame.mask.from_surface(self.image)
# different bullet movement direction for both player and enemy (alien)
if side == "enemy":
self.move_speed = BULLET_SPEED
elif side == "player":
self.move_speed = (- BULLET_SPEED)
def _move_bullet(self):
self.rect.y += self.move_speed
def update(self):
self._move_bullet()
self.rect = self.image.get_rect(topleft=(self.rect.x, self.rect.y))
# delete the bullet if it get through out of the screen
if self.rect.bottom <= 0 or self.rect.top >= HEIGHT:
self.kill()
The Bullet
class is responsible for managing the bullets fired by the player and enemy ships.
Inside the Bullet
class constructor (__init__()
), several properties are set up. These properties include the bullet's position, image, collision mask, and movement speed.
The image of the bullet is loaded from a file path based on the "side" parameter. For example, if it's a player bullet, it loads 'assets/bullet/player-bullet.png'
. The image is then scaled to the specified size, and the initial position and collision mask are configured. The direction of the bullet's movement is determined based on its "side"
.
The _move_bullet()
method is responsible for updating the bullet's position during the game. It adjusts the bullet's vertical (y) coordinate based on its movement speed, which controls its direction. If it's a player bullet, it moves upward; if it's an enemy bullet, it moves downward. The update()
method is called regularly in the game loop to update the bullet's position and check if it has moved out of the visible screen area. If the bullet goes beyond the screen boundaries (either above or below), it is removed from the game using the kill()
method. This ensures that bullets that have fulfilled their purpose or missed their target are not visible or active in the game, maintaining a clean and efficient game environment.
Let's move on to making the World
class. The World
class coordinates various game elements, controls gameplay mechanics, and ensures that the game responds to player input, handles collisions, and progresses through levels:
# /* world.py
import pygame
from ship import Ship
from alien import Alien
from settings import HEIGHT, WIDTH, ENEMY_SPEED, CHARACTER_SIZE, BULLET_SIZE, NAV_THICKNESS
from bullet import Bullet
from display import Display
class World:
def __init__(self, screen):
self.screen = screen
self.player = pygame.sprite.GroupSingle()
self.aliens = pygame.sprite.Group()
self.display = Display(self.screen)
self.game_over = False
self.player_score = 0
self.game_level = 1
self._generate_world()
In the class constructor, several attributes and groups are initialized. These include the game screen (self.screen
), the player's ship (self.player
), the aliens (self.aliens
), and a display object (self.display
) used to show information like the player's score and game level. The constructor also initializes game over status, player score, and game-level variables. It also calls a method named _generate_world()
, to set up the initial game state:
# /* world.py
def _generate_aliens(self):
# generate opponents
alien_cols = (WIDTH // CHARACTER_SIZE) // 2
alien_rows = 3
for y in range(alien_rows):
for x in range(alien_cols):
my_x = CHARACTER_SIZE * x
my_y = CHARACTER_SIZE * y
specific_pos = (my_x, my_y)
self.aliens.add(Alien(specific_pos, CHARACTER_SIZE, y))
# create and add player to the screen
def _generate_world(self):
# create the player's ship
player_x, player_y = WIDTH // 2, HEIGHT - CHARACTER_SIZE
center_size = CHARACTER_SIZE // 2
player_pos = (player_x - center_size, player_y)
self.player.add(Ship(player_pos, CHARACTER_SIZE))
self._generate_aliens()
The _generate_aliens()
method is responsible for creating and positioning the enemy aliens in the game world. It calculates the number of alien columns and rows based on the game's width and character size and iterates through the grid to create aliens at specific positions.
The _generate_world()
method is used to set up the initial game world. It creates the player's ship at the center of the screen and generates the aliens using the _generate_aliens()
method.
# /* world.py
def add_additionals(self):
# add nav bar
nav = pygame.Rect(0, HEIGHT, WIDTH, NAV_THICKNESS)
pygame.draw.rect(self.screen, pygame.Color("gray"), nav)
# render player's life, score and game level
self.display.show_life(self.player.sprite.life)
self.display.show_score(self.player_score)
self.display.show_level(self.game_level)
The add_additionals()
method adds additional elements to the game, including a navigation bar at the bottom of the screen, and renders information about the player's life, score, and game level using the display
object:
# /* world.py
def player_move(self, attack = False):
keys = pygame.key.get_pressed()
if keys[pygame.K_a] and not self.game_over or keys[pygame.K_LEFT] and not self.game_over:
if self.player.sprite.rect.left > 0:
self.player.sprite.move_left()
if keys[pygame.K_d] and not self.game_over or keys[pygame.K_RIGHT] and not self.game_over:
if self.player.sprite.rect.right < WIDTH:
self.player.sprite.move_right()
if keys[pygame.K_w] and not self.game_over or keys[pygame.K_UP] and not self.game_over:
if self.player.sprite.rect.top > 0:
self.player.sprite.move_up()
if keys[pygame.K_s] and not self.game_over or keys[pygame.K_DOWN] and not self.game_over:
if self.player.sprite.rect.bottom < HEIGHT:
self.player.sprite.move_bottom()
# game restart button
if keys[pygame.K_r]:
self.game_over = False
self.player_score = 0
self.game_level = 1
for alien in self.aliens.sprites():
alien.kill()
self._generate_world()
if attack and not self.game_over:
self.player.sprite._shoot()
The player_move()
method handles player ship movement and shooting. It checks for key presses to move the player's ship in different directions (left, right, up, down) and also allows the player to shoot bullets when an attack flag is passed. The player can move by pressing "W"
, "A"
, "S"
, "D"
or the arrow keys. To shoot, the player can press the SPACE
bar. Additionally, it checks for a game restart command (if the player presses the "R"
key):
# /* world.py
def _detect_collisions(self):
# checks if player bullet hits the enemies (aliens)
player_attack_collision = pygame.sprite.groupcollide(self.aliens, self.player.sprite.player_bullets, True, True)
if player_attack_collision:
self.player_score += 10
# checks if the aliens' bullet hit the player
for alien in self.aliens.sprites():
alien_attack_collision = pygame.sprite.groupcollide(alien.bullets, self.player, True, False)
if alien_attack_collision:
self.player.sprite.life -= 1
break
# checks if the aliens hit the player
alien_to_player_collision = pygame.sprite.groupcollide(self.aliens, self.player, True, False)
if alien_to_player_collision:
self.player.sprite.life -= 1
The _detect_collisions()
method checks for collisions between player bullets and enemy aliens and between alien bullets and the player's ship. It updates the player's score and decreases the player's life accordingly:
# /* world.py
def _alien_movement(self):
move_sideward = False
move_forward = False
for alien in self.aliens.sprites():
if alien.to_direction == "right" and alien.rect.right < WIDTH or alien.to_direction == "left" and alien.rect.left > 0:
move_sideward = True
move_forward = False
else:
move_sideward = False
move_forward = True
alien.to_direction = "left" if alien.to_direction == "right" else "right"
break
for alien in self.aliens.sprites():
if move_sideward and not move_forward:
if alien.to_direction == "right":
alien.move_right()
if alien.to_direction == "left":
alien.move_left()
if not move_sideward and move_forward:
alien.move_bottom()
def _alien_shoot(self):
for alien in self.aliens.sprites():
if (WIDTH - alien.rect.x) // CHARACTER_SIZE == (WIDTH - self.player.sprite.rect.x) // CHARACTER_SIZE:
alien._shoot()
break
The _alien_movement()
method controls the movement of enemy aliens. It determines whether the aliens should move sideward or forward based on whether they've reached the screen's edges. This method also manages the change in direction for the aliens.
The _alien_shoot()
method allows enemy aliens to shoot bullets at the player. It checks the alignment of the player and aliens to determine when the aliens should fire:
# /* world.py
def _check_game_state(self):
# check if game over
if self.player.sprite.life <= 0:
self.game_over = True
self.display.game_over_message()
for alien in self.aliens.sprites():
if alien.rect.top >= HEIGHT:
self.game_over = True
self.display.game_over_message()
break
# check if next level
if len(self.aliens) == 0 and self.player.sprite.life > 0:
self.game_level += 1
self._generate_aliens()
for alien in self.aliens.sprites():
alien.move_speed += self.game_level - 1
The _check_game_state()
method monitors the game's state. It checks if the game should end (game over) when the player's life reaches zero or if any alien reaches the bottom of the screen. If all aliens are defeated, the game progresses to the next level:
# /* world.py
def update(self):
# detecting if bullet, alien, and player group is colliding
self._detect_collisions()
# allows the aliens to move
self._alien_movement()
# allows alien to shoot the player
self._alien_shoot()
# bullets rendering
self.player.sprite.player_bullets.update()
self.player.sprite.player_bullets.draw(self.screen)
[alien.bullets.update() for alien in self.aliens.sprites()]
[alien.bullets.draw(self.screen) for alien in self.aliens.sprites()]
# player ship rendering
self.player.update()
self.player.draw(self.screen)
# alien rendering
self.aliens.draw(self.screen)
# add nav
self.add_additionals()
# checks game state
self._check_game_state()
The update()
method is the core game loop. It updates various game elements, such as bullet positions, alien movements, player ship rendering, alien rendering, and additional elements like the navigation bar. It also checks the game state to determine if the game should continue, end, or progress to the next level.
Lastly, create another class in display.py
and call it the Display
class. The Display
class manages the rendering of in-game elements, including player lives, scores, game levels, and event messages like "GAME OVER"
. These elements are essential for conveying important information to the player, and the class provides a structured way to handle their presentation on the game screen:
# /* display.py
import pygame
from settings import WIDTH, HEIGHT, SPACE, FONT_SIZE, EVENT_FONT_SIZE
pygame.font.init()
class Display:
def __init__(self, screen):
self.screen = screen
self.score_font = pygame.font.SysFont("monospace", FONT_SIZE)
self.level_font = pygame.font.SysFont("impact", FONT_SIZE)
self.event_font = pygame.font.SysFont("impact", EVENT_FONT_SIZE)
self.text_color = pygame.Color("blue")
self.event_color = pygame.Color("red")
def show_life(self, life):
life_size = 30
img_path = "assets/life/life.png"
life_image = pygame.image.load(img_path)
life_image = pygame.transform.scale(life_image, (life_size, life_size))
life_x = SPACE // 2
if life != 0:
for life in range(life):
self.screen.blit(life_image, (life_x, HEIGHT + (SPACE // 2)))
life_x += life_size
def show_score(self, score):
score_x = WIDTH // 3
score = self.score_font.render(f'score: {score}', True, self.text_color)
self.screen.blit(score, (score_x, (HEIGHT + (SPACE // 2))))
def show_level(self, level):
level_x = WIDTH // 3
level = self.level_font.render(f'Level {level}', True, self.text_color)
self.screen.blit(level, (level_x * 2, (HEIGHT + (SPACE // 2))))
def game_over_message(self):
message = self.event_font.render('GAME OVER!!', True, self.event_color)
self.screen.blit(message, ((WIDTH // 3) - (EVENT_FONT_SIZE // 2), (HEIGHT // 2) - (EVENT_FONT_SIZE // 2)))
We start by importing essential modules and constants, such as pygame
for game development and various settings like WIDTH
, HEIGHT
, SPACE
, FONT_SIZE
, and EVENT_FONT_SIZE
. The pygame.font.init()
function is called to initialize the pygame font module, enabling the code to work with fonts for rendering text.
The Display
class is introduced to manage the rendering of text and visual elements on the game screen. It takes the game screen (screen
) as an argument, allowing it to handle display-related tasks. In the constructor, several attributes are initialized. These include self.screen
, which stores the game screen, and fonts (self.score_font
, self.level_font
, and self.event_font
) for displaying text. The font styles and sizes are specified, and colors for regular text (self.text_color
) and event text (self.event_color
) are defined.
The show_life()
method is responsible for presenting the player's remaining lives visually. It loads a life image from a file, scales it to a specific size, and then arranges multiple life images in a row at the bottom of the screen, corresponding to the player's remaining lives.
The show_score()
method is designed to render and display the player's score on the screen. It creates a text surface using the player's score, applying the score_font
and positioning it at a specific location on the screen.
The show_level()
method is used to visually represent the current game level. It renders the game level as text using level_font
and places it in a designated position on the screen.
The game_over_message()
method is responsible for displaying a "GAME OVER"
message on the screen when the game ends. It creates a text surface for the message using event_font
and positions it at the center of the screen.
And we are now done coding! To try our game, open your terminal, head to the game directory, then run python main.py
to call the game.
Here are some game snapshots:
Running the Game
Game while Playing
Different Game Level
Game over
Here's a video of me playing the game:
In conclusion, this tutorial has provided a detailed examination of the Pygame implementation of the classic Space Invaders game. We've delved into the code, dissecting its key components, from player mechanics to enemy behavior and game presentation. With this knowledge, you are well-prepared to modify, expand, and create your versions of Space Invaders or other game projects using Pygame.
You can get the complete code here.
Here are some other game development tutorials:
Happy coding ♥
Ready for more? Dive deeper into coding with our AI-powered Code Explainer. Don't miss it!
View Full Code Assist My Coding
Got a coding query or need some guidance before you comment? Check out this Python Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!