Criando um Space Invaders

Criando o arquivo-fonte do jogo

O primeiro passo no desenvolvimento do seu jogo é a criação do arquivo-fonte. Após escolher a pasta onde irá armazenar os arquivos do jogo, crie um arquivo chamado SpaceInvaders.py. Você pode criar esse arquivo utilizando um editor de texto ou IDE de sua preferência. Então crie seu pacote de imagens e áudios a medida que for desenvolvendo seu jogo de modo que seus arquivos fiquem coerentes com o jogo.

Importações

Agora a programação começa de verdade! Abra o arquivo SpaceInvaders.py para edição. Primeiramente, vamos importar as classes do PPlay que serão necessárias para o nosso projeto:

from pplay.window import *
from pplay.sprite import *
from pplay.gameimage import *
from pplay.sound import *

Para saber mais, visite a documentação das classes WindowSprite, GameImage e Sound.

Além das classes do PPlay, também precisaremos da classe random do Python. Vamos importá-la e gerar uma seed, que é um valor inteiro usado para a geração de números aleatórios:

import random
random.seed()

Variáveis globais

Nesse passo, criaremos duas variáveis globais que auxiliarão no controle do jogo. A primeira é a GAME_SPEED, que é utilizada para controlar a velocidade do jogo e é muito útil para efetuar testes. A segunda é chamada GAME_STATE, que controla o estado atual do jogo:

GAME_SPEED = 1
GAME_STATE = 0

"""
Controla em que estágio o jogo se encontra:
0 - Menu Inicial
1 - Jogo
2 - Finalização do jogo (Pontuação)
"""

Criando a janela do jogo

Agora, criaremos a janela do jogo. O primeiro passo é definir quais serão as dimensões da mesma. Em nosso exemplo, usaremos 400 pixels de largura e 600 pixels de altura:

# Largura da janela
width = 400

# Altura da janela
height = 600

Em seguida, definimos a cor de fundo e o título da janela do jogo:

# Cor de fundo
background_color = [0, 0, 0]

# Título da janela
title = "Space Invaders"

Finalmente, utilizando as definições que criamos anteriormente, criamos efetivamente a janela do jogo:

window = Window(width, height)
window.set_title(title)
window.set_background_color(background_color)

Agora definiremos as imagens de fundo da janela. Usaremos duas GameImage iguais, para gerarmos mais à frente um efeito de scrolling:

# Background Image - 2 para scrolling
background_01 = GameImage("stars_background.png")
background_02 = GameImage("stars_background.png")

Logo após, criamos as definições do efeito de scrolling. O primeiro passo é posicionar as duas imagens de fundo. A primeira (background_01) recebe a valor 0 como posição vertical inicial. A segunda (background_02) recebe como posição vertical o próprio tamanho negativado. Isso acontece para que a mesma fique posicionada logo após a primeira, para que quando termine a rolagem da primeira imagem comece a rolagem da segunda, como visto no esquema ao lado:

# Respectivas posições iniciais
background_01.y = 0
background_02.y = -background_02.height

Para finalizar, definimos a variável background_roll_speed, que é responsável por controlar a velocidade da movimentação do fundo. O efeito de scrolling será
tratado com detalhes mais à frente no tutorial.

# Velocidade de rolagem
background_roll_speed = 50

Teclado e Mouse

Nesse momento criaremos os controladores do jogo: o teclado e o mouse.

keyboard = window.get_keyboard()
mouse = window.get_mouse()

Efeito Sonoro

Agora criaremos o efeito sonoro do disparo utilizando o arquivo bullet_sound.ogg, presente no pacote que foi baixado e descompactado no início do tutorial:

bullet_sound =Sound("bullet_sound.ogg")

Criando a Nave do Jogador

Com a janela do jogo criada e o mouse e teclado definidos, é a hora de criar a nave que o jogador controlará no jogo. Primeiramente, vamos criar o Sprite da nave usando a imagem ship.png, também presente no pacote do princípio do tutorial. Em seguida vamos posicioná-la centralizada, na parte inferior da janela de jogo:

# Sprite da nave do jogador
player = Sprite("ship.png")

# Posição inicial
player.set_position((window.width - player.width) / 2,
                    (window.height - player.height))

Agora que já posicionamos a nave do jogador, vamos definir algumas informações sobre o jogador. Em primeiro lugar, definimos a velocidade do jogador. Em seguida, definimos em que direção ele está se movendo (no caso, para cima). Por final, criamos a variável que irá armazenar a pontuação do jogador:

# Velocidade do jogador
player.speed = 200

# Direção do jogador 
player.direction = -1  # [cima]

# Pontuação
player.score = 0

Inimigos

Para começar, definimos o endereço sprite do inimigo, utilizando a imagem red_enemy.png. Em seguida, definimos a velocidade do movimento como 200, o mesmo valor que definimos anteriormente para a nave do jogador, e também definimos a direção do movimento, que é contrária à direção do movimento do jogador.

# Sprite dos inimigos
enemy_image = "red_enemy.png"

# Velocidade dos inimigos
enemy_speed = 200

# Direção dos inimigos
enemy_direction = 1  # [baixo]

No Space Invaders, os inimigos se apresentam na forma de uma matriz. Vamos agora definir as informações dessa matriz de inimigos. A primeira informação que declaramos a variável MAXSIZE, que define o tamanho máximo de ambas as dimensões da matriz.

# Tamanho limite da matriz
MAXSIZE = 10

Em seguida, usando nosso gerador de números aleatórios declarado anteriormente, definimos as dimensões horizontais e verticais da matriz. Para o número de linhas, “sorteamos” um valor entre 3 e 10. Já para o número de colunas, o valor “sorteado” é entre 1 e 10:

# Numero de linhas da matriz
matrix_x = int(random.uniform(3, MAXSIZE))

# Numero de coluna da matriz
matrix_y = int(random.uniform(1, MAXSIZE))

Múltiplos Objetos

Nesse momento, iremos declarar dois vetores muito importantes para o jogo. O primeiro deles é o bullets, que é o conjunto dos tiros disparados pelo jogador. O segundo é a matriz enemies, de tamanho 10×10, que armazenará os nossos inimigos:

bullets = []
enemies = [[0 for x in range(10)] for x in range(10)]

Controladores Temporais

Os controladores temporais são variáveis que controlam o atraso dos disparos dos inimigos e do jogador (enemy_shoot_delay e player.shoot_delay, respectivamente). Elas são definidas em função da variável global GAME_SPEED, declarada anteriormente. Em adição temos a variável auxiliar player.shoot_tickque será usada mais à frente em nosso tutorial:

enemy_shoot_delay = 1 / GAME_SPEED
player.shoot_delay = 1 / GAME_SPEED * 0.5

player.shoot_tick = player.shoot_delay

win()

A função win() é muito importante em nosso jogo. É ela que define quando o jogador efetivamente “ganhou” a partida. Caso o jogador tenha vencido, a função inicia uma nova partida.

def win():
    """
    Função para verificar se o jogador ganhou
    Caso afirmativo, reinicia o jogo
    """
    # Criamos o acesso às variáveis globais
    global matrix_x
    global matrix_y
    global GAME_STATE
    
    # Criamos uma variável de controle, para sabermos se o jogador ganhou o jogo
    won = True
    
    # Verifica em todas as linhas se ainda existe algum inimigo vivo
    for row in range(matrix_x):
        if won:
            for column in range(matrix_y):
                if enemies[row][column] != 0:
                    # Se ele encontrar algum inimigo vivo, seta a variável
                    # won como False e quebra a cadeia de repetições
                    won = False
                    break
    if won:
        # Se o jogo percorrer toda a matriz e não encontrar
        # nenhum inimigo vivo, reinicia o jogo
        GAME_STATE = 2

Como você pode perceber, primeiramente setamos a variável win como True e verificamos toda a matriz em busca de algum inimigo vivo. Caso nenhum seja encontrado, a variável continua com seu valor original. Caso algum inimigo ainda esteja presente, o valor da variável é invertido e a sequência de repetições é quebrada, já que não é necessário saber se existe mais de um inimigo vivo.

restart(player, enemies, bullets)

Essa função, como o próprio nome sugere, reinicializa o jogo. Ela é responsável por excluir todos os inimigos e disparos efetuados, além de levar o jogador de volta ao seu estado inicial.

def restart(player, enemies, bullets):
    """
    Função para (re)iniciar o jogo.
    :param player: jogador
    :param enemies: matriz de inimigos
    :param bullets: lista de instancias de balas
    """
    # Gera o acesso às variáveis globais
    global matrix_x
    global matrix_y
    
    # Deleta todos os objetos enemies e bullets
    del enemies
    del bullets
    
    # Retorna o jogador à posição e pontuação inicial do jogo
    player.score = 0
    player.set_position((window.width - player.width) / 2,
                        (window.height - player.height))
    
    # Reinicia os contadores de disparos
    player.shoot_tick = player.shoot_delay
    
    # Cria uma nova matriz de inimigos
    matrix_x = int(random.uniform(3, 10))
    matrix_y = int(random.uniform(1, 10))
    spawn_enemy(matrix_x, matrix_y, enemies)

adjust_bullet(actor, bullet)

Definimos agora a função adjust_bullet pois ela será necessária na próxima função que iremos implementar, a shoot(). Essa função receber como parâmetros o “ator” (ator = jogador ou inimigo) que irá efetuar o disparo e o projetil que será disparado e ajusta a posição do projétil para que o mesmo se alinhe com o “ator”.

def adjust_bullet(actor, bullet):
    """
    Recebe o atirador e a bala, e ajusta sua posição
    :param actor: Instancia do jogador ou inimigo
    :param bullet: Instancia do projétil
    """
    # Calcula posição X da bala, utilizando como referência o
    # centro do ator e armazena em x_fire
    x_fire = actor.x + (actor.width / 2) - (bullet.width / 2)
    
    # Calcula posição Y do projétil, utilizando como referência
    # a direção de movimento e o tamanho do jogador, salvando
    # o resultado na variável y_fire
    if actor.direction == -1:
        y_fire = actor.y
    elif actor.direction == 1:
        y_fire = actor.y + actor.height - bullet.height
    
    # Transfere o valor das variáveis auxiliares x_fire e y_fire
    # para o projétil
    bullet.x = x_fire
    bullet.y = y_fire
    
    # Define direção do projétil
    bullet.direction = actor.direction

shoot(shooter)

Essa função cria um novo projétil (bullet) e o associa ao responsável pelo seu disparo. Ela também reproduz o efeito sonoro do disparo e ainda zera a variável shoot_tick do responsável pelo mesmo (falaremos mais à frente sobre a utilização dessa variável).

def shoot(shooter):
    """
    Cria um bullet, associando-o a um ator
    :param shooter: Ator responsável pelo disparo (jogador ou inimigo)
    """
    # Reproduz o som do disparo
    bullet_sound.play()
    
    # Zera o contador de último disparo
    shooter.shoot_tick = 0
    
    # Cria uma nova bullet, dependendo de quem for que atirou
    if shooter.direction == -1:
        b = Sprite("bullet_player.png")
    elif shooter.direction == 1:
        b = Sprite("bullet_enemy.png")
    
    # Ajusta a posição inicial e a direção do projétil
    adjust_bullet(shooter, b)
    
    # Adiciona o novo projétil que criamos para ser desenhado na tela
    bullets.append(b)

spawn_enemy(i, j, enemy_matrix)

Dessa vez criaremos a função responsável por popular a matriz de inimigos. Os dados de entrada são i e j, que representam o número de linhas e colunas da matriz respectivamente, além de enemy_matrix, que referencia a matriz dos inimigos.

def spawn_enemy(i, j, enemy_matrix):
    """
    Gera a matriz de inimigos
    :param i: numero de linhas na matriz
    :param j: numero de colunas na matriz
    :param enemy_matrix: matriz de inimigos
    """
    # for x e for y percorrem cada elemento da matriz
    for x in range(i):
        for y in range(j):
            # Cria o Sprite do inimigo
            enemy = Sprite(enemy_image);
            # Define a posição
            enemy.set_position(x * enemy.width, y * enemy.height)
            # Define a direção do movimento, no caso para baixo
            enemy.direction = 1  # 1 = para baixo
            # Define randomicamente o intervalo entre os disparos
            enemy.shoot_delay = random.uniform(0, 15) / GAME_SPEED
            # Zera a variável de controle de disparos
            enemy.shoot_tick = 0
            # Coloca o inimigo recém criado na matriz
            enemy_matrix[x][y] = enemy

Como você pode perceber, o intervalo entre os disparos é definido randomicamente para cada um dos inimigos. Isso é feito para que o jogo seja mais dinâmico, com os disparos sendo menos repetitivos. Com essa definição, o jogador não sabe de onde virão os disparos quando o jogo inicia.

scrolling(bg_bottom, bg_top, roll_speed)

Anteriormente, citamos o efeito de scrolling, do qual essa função é responsável. Esse efeito faz com que o background do nosso jogo seja aparentemente infinito.

O que essa função realmente faz é rolar as duas imagens até o topo da imagem de cima tocar o início da tela. Neste momento, as duas imagens retornam às sua posições iniciais e a rolagem começa novamente. Veja ao lado um representação gráfica deste processo:

def scrolling(bg_bottom, bg_top, roll_speed):
    """
    Recebe dois background e a velocidade que devem rolar infinitamente
    :param bg_bottom: Sprite do fundo 1
    :param bg_top: Sprite do fundo 2
    :param roll_speed: Velocidade de deslocamento dos fundos
    """

    # Movimenta ambos os Sprites verticalmente
    bg_bottom.y += roll_speed * window.delta_time()
    bg_top.y += roll_speed * window.delta_time()

    # Se a imagem do topo já tiver sido completamente exibida,
    # retorna ambas imagens às suas posições iniciais
    if bg_top.y >= 0:
        bg_bottom.y = 0
        bg_top.y = -bg_top.height

    # Renderiza as duas imagens de fundo
    bg_bottom.draw()
    bg_top.draw()

update_counters()

Essa função tem como objetivo atualizar os contadores de disparo, usando como referência o valor de window.delta_time(). Se você quiser saber um pouco mais sobre essa função da classe window, verifique a documentação da classe.

def update_counters():
    """
    Atualiza contadores do jogo
    """

    # Atualiza o contador de controle de tiro do jogador
    player.shoot_tick += window.delta_time()

    # Atualiza o contador de controle de tiro de cada inimigo
    # presente na matriz de inimigos
    for row in range(matrix_x):
        for column in range(matrix_y):
            if enemies[row][column] != 0:
                enemies[row][column].shoot_tick += window.delta_time()

player_shoot()

O papel dessa função é gerir os disparos do jogador. Ela fica verificando se a tecla de disparo, no caso [espaço], foi pressionada. Caso detecte esse evento, verifica se já passou tempo suficiente desde o último disparo, para o jogador não disparar milhares de projéteis seguidos. Caso já seja hora de efetuar um novo disparo, ela chama a função shoot(), definida anteriormente em nosso tutorial.

def player_shoot():
    """
    Ação de atirar
    """

    # Verifica se o jogador apertou o botão de disparar
    if keyboard.key_pressed("space"):
        # Verifica se já pode disparar
        if player.shoot_tick > player.shoot_delay:
            # Chama a função shoot(), para que ela efetue do disparo
            shoot(player)

player_movement()

Essa função é responsável por controlar o movimento do jogador. Como a nave do jogador só pode se mover lateralmente, chamamos apenas a função player.move_key_x(), função da classe Sprite responsável pelo movimento lateral do mesmo. Você pode verificar mais informações sobre essa função e sobre a classe Sprite na documentação da classe.

Além de movimentar a nave, essa função também controla a posição máxima e mínima da mesma para que ela não ultrapasse os limites laterais da janela de jogo.

def player_movement():
    """
    Ação de movimento da nave do jogador
    """

    # Atualiza a posição do jogador
    player.move_key_x(player.speed * window.delta_time() * GAME_SPEED)

    # Não permite que a lateral esquerda da nave ultrapasse a
    # lateral esquerda da tela, onde x = 0
    if player.x <= 0:
        player.x = 0

    # Não permite que a lateral esquerda da nave ultrapasse a
    # lateral direita da tela, onde x = largura da tela.
    if player.x + player.width >= window.width:
        player.x = window.width - player.width

Como você pode perceber, o limite direito da tela é definido por largura da tela menos o tamanho do Sprite da nave. Isso acontece pois a posição x de um Sprite é definido pelo canto esquerdo do mesmo. Logo, você precisa compensar a distância entre as laterais para manter o Sprite dentro da tela de jogo. Para um melhor entendimento dessa relação, observe a imagem ao lado:

bullet_movement()

Agora criaremos a função responsável por controlar os disparos efetuados no jogo, tanto os do jogador quanto os dos inimigos. Ela percorre o nosso conjunto de disparos bullets e atualiza o movimento de cada uma das instâncias de projétil, na direção a qual ele está orientado. Além disso, verifica se o projétil já saiu dos limites da tela e o destrói caso isso tenha ocorrido.

def bullet_movement():
    """
    Realiza a movimentação de cada bala em jogo
    """

    # Para cada bala instanciada no jogo
    for b in bullets:
        # Atualiza a sua posição, baseado em sua direção
        b.move_y(200 * b.direction * window.delta_time() * GAME_SPEED)
        
        # Verifica se saiu da tela e, caso tenha saído, destrói o projétil
        if b.y < -b.height or b.y > window.height + b.height:
            bullets.remove(b)

enemy_movement()

A função que controla o movimento dos inimigos é um pouco mais complicada que as anteriores, pois estamos lidando com o movimento de toda a matriz de inimigos. O primeiro passo é calcular a nova posição da matriz como um todo. Após definir essa posição, ela entra em cada um dos inimigos e faz vários subprocessos.

O primeiro processo que é realizado em um inimigo é redefinir a posição atual do mesmo, utilizando a nova posição da matriz como referência. Em seguida, ela verifica se o intervalo de disparo do inimigo já foi cumprido e efetua um disparo se for o caso. Logo após, a função define um novo intervalo de disparo aleatório para o inimigo.

Depois de efetuar essas operações, é hora de verificar se a matriz de inimigos já chegou a algum dos limites laterais da tela. Caso a matriz tenha alcançado alguma das laterais, invertemos o movimento da matriz, indo agora na direção da parede contrária.

def enemy_movement():
    """
    Realiza a movimentação de cada inimigo
    """

    # Acessando variáveis globais
    global enemy_direction
    global enemy_speed

    # Cria variável de controle
    inverted = False

    # Calcula a nova posição da matriz de inimigos
    new_position = enemy_speed * enemy_direction * window.delta_time() * GAME_SPEED

    # Percorre toda a matriz de inimigos
    for row in range(matrix_x):
        for column in range(matrix_y):
            # Caso a posição esteja preenchida, isto é, o inimigo
            # ainda esteja vivo, efetua as ações em seguida
            if enemies[row][column] != 0:
                # Move o inimigo para sua nova posição
                enemies[row][column].move_x(new_position)

                # Caso já tenha alcançado o intervalo de disparo,
                # efetua um novo disparo
                if enemies[row][column].shoot_tick > enemies[row][column].shoot_delay:
                    shoot(enemies[row][column])
                    enemies[row][column].shoot_tick = 0

                    # Gera um novo intervalo de disparo aleatório
                    enemies[row][column].shoot_delay = random.uniform(0, 15) / GAME_SPEED

                # Verifica se nenhuma extremidade da matriz bateu na parede
                if not inverted:
                    # Se bateu na parede, então inverte a direção da matriz
                    # Altera direção para direita
                    if enemies[row][column].x <= 0:
                        enemy_direction = 1
                        inverted = True
                    # Altera direção para esquerda
                    elif enemies[row][column].x + Sprite(enemy_image).width >= window.width:
                        enemy_direction = -1
                        inverted = True

Verifique que, como na função player_movement(), que declaramos anteriormente, é necessário compensar o tamanho lateral da matriz ao verificar se a mesma tocou a lateral direita da janela.

bullet_ship_collision()

Agora que já temos vários projéteis voando na direção do jogador e dos inimigos, precisamos verificar se algum deles efetivamente acertou o alvo. Essa função percorre nosso conjunto de projéteis bullets e verifica:

  • caso o disparo seja do jogador, se atingiu algum inimigo
  • caso o disparo seja de um inimigo, se atingiu a nave do jogador

Se um disparo atingiu um inimigo, o mesmo será destruído dentro da função check_enemy_collision(). Já se um disparo acertar a nave do jogador, a variável global GAME_STATE recebe o valor 2, que representa o fim de jogo.

def bullet_ship_collision():
    """
    Verifica se os disparos colidiram com alguma nave
    """

    # Acessando variável global
    global GAME_STATE

    # Para cada instância dos disparos
    for b in bullets:
        # Se for disparo do jogador
        if b.direction == -1:
            # Verifica se bateu em algum inimigo
            check_enemy_collision(b)
        
        # Se for disparo do inimigo
        elif b.direction == 1:
            # Verifica se bateu no jogador
            if b.collided(player):
                # Se bateu no jogador, define o fim de jogo
                GAME_STATE = 2

check_enemy_collision(b)

Chegamos agora na função que “chamamos” anteriormente. Essa função recebe uma instância de disparo e verifica em toda a lista de inimigos se houve uma colisão. Caso tenha havido colisão, ela destrói a bala e o inimigo.

def check_enemy_collision(b):
    """
    Verifica se o projétil colidiu com o inimigo
    :param b: Instância de projétil
    """

    # Percorre toda a matriz de inimigos
    for row in range(matrix_x):
        for column in range(matrix_y):
            if enemies[row][column] != 0:
                # Se o inimigo ainda estiver vivo (enemies[row][column] != 0),
                # verifica se o disparo b colidiu com o mesmo
                if b.collided(enemies[row][column]):
                    # Caso tenha havido colisão, remove a bala e o
                    # inimigo do jogo
                    bullets.remove(b)
                    enemies[row][column] = 0
                    # Atualiza a pontuação do jogador
                    player.score += 50
                    # Interrompe a função, pois o projétil foi destruído
                    # e não poderá colidir com mais nenhum inimigo
                    return

bullet_bullet_collision()

Uma das características do Space Invaders é que você pode se defender dos disparos inimigos utilizando seus próprios disparos. Essa função é responsável por verificar se quaiser dois disparos colidiram no jogo e, caso isso aconteça, destuir ambos.

def bullet_bullet_collision():
    """
    Verifica se o projétil colidiu com alguma outro projétil
    """

    # Para cada instância de projétil
    for b1 in bullets:
        # Se for projétil do jogador
        if b1.direction == -1:
            # Verifica em todas as instâncias se ele colidiu com outro
            for b2 in bullets:
                # Verifica se o projétil atual é inimigo
                if b2.direction == 1:
                    # Se for inimigo, verifica se existiu colisão
                    if b1.collided(b2):
                        # Se houver colisão, remove os dois projéteis
                        bullets.remove(b1)
                        bullets.remove(b2)
                        break

Repare que após destruir os disparos quando ocorre uma colisão, existe uma linha de break. A função desse break é parar de comparar o primeiro projétil (b1) com outros projéteis. Como ele foi destruído, ele não poderá mais colidir com nenhum outro projétil da lista. Logo, “saltamos” para o próximo projétil (b1) e seguimos comparando esse novo projétil com os outros da lista.

draw()

Essa função é uma das mais importantes do jogo. Ela é responsável por desenhar, ou melhor dizendo, renderizar os companentes que formam o seu jogo.

def draw():
    """
    Desenha todos os elementos na tela
    """

    # Desenha todas as instâncias de projétil
    for b in bullets:
        b.draw()

    # Percorre todo a matriz de inimigos
    for row in range(matrix_x):
        for column in range(matrix_y):
            # Se o inimigo estiver vivo (!=0), desenha o inimigo
            if enemies[row][column] != 0:
                enemies[row][column].draw()

    # Desenha a nave do jogador
    player.draw()

start_window()

Essa função, como o nome sugere, é responsável por criar a janela inicial do jogo. Ela utiliza todas as variáveis que definimos no início do nosso tutorial para criar a primeira partida.

def start_window():
“””
Janela inicial do jogo
“””

# Acessando a variável global
global GAME_STATE

# Escreve o texto que informa as opções do jogador na tela
window.draw_text("ENTER para jogar | ESC para sair", 0, 0, 28, (255,255,255), "Calibri")

# Se jogador pressionou 'enter', inicia o jogo
if keyboard.key_pressed("enter"):
    # Define a variável como 1, que significa que a partida está ativa
    GAME_STATE = 1
    # Reinicia o jogador, os inimigos e os disparos
    restart(player, enemies, bullets)

# Se jogador pressionou 'esc', sai do jogo
elif keyboard.key_pressed("escape"):
    window.close()

restart_window()

Essa função é chamada toda vez que uma partida chega ao final. Ela é responsável por exibir a pontuação final do jogador e iniciar uma nova partida quando o jogador pressionar a tecla [ENTER].

def restart_window():
    """
    Reinicia o jogo
    """

    # Acessando variável global
    global GAME_STATE

    # Escreve na tela a pontuação do jogador
    window.draw_text("Sua pontuação foi:" + str(player.score), 5, 5, 16, (255,255,255), "Calibri", True)

    # Quando o jogador pressionar 'enter', reinicia o jogo
    if keyboard.key_pressed("enter"):
        # Modifica o estado do jogo para 1, que significa partida ativa
        GAME_STATE = 1
        # Reinicia o jogador, os inimigos e os disparos
        restart(player, enemies, bullets)

Game Loop

Esse é o coração do nosso jogo. O game loop é uma função que roda infinitamente (ao menos até ser interrompida), chamando as funções do jogo que definimos durante o tutorial e coordenando toda a partida.

while True:
    # Apaga a tela completamente
    window.set_background_color(background_color)
    
    # Chama nossa função de scrolling, que faz o fundo 
    # rolar infinitamente
    scrolling(background_01, background_02, background_roll_speed)
    
    # Se o estado de jogo for = 0, quer dizer que é a primeira
    # vez que o game loop é acionado. Logo, ele cria a janela do
    # jogo.
    if GAME_STATE == 0:
        start_window()
    
    # Se não for a primeira vez, quer dizer que a partida ainda
    # está acontecendo.
    elif GAME_STATE == 1:
        # Verifica se o jogador venceu a partida
        win()
        
        # Atualiza os contadores
        update_counters()
        
        # Atualiza a movimentação do jogador
        player_movement()
        
        # Controla os tiros a cada intervalo
        player_shoot()
        
        # Atualiza o movimento dos disparos
        bullet_movement()
        
        # Atualiza o movimento dos inimigos
        enemy_movement()
        
        # Verifica a colisão de projéteis contra naves
        bullet_ship_collision()
        
        # Verifica colisões entre projéteis
        bullet_bullet_collision()
        
        ## Renderiza todos os dados na tela ##
        draw()
    
    # Caso o jogo tenha terminado (GAME_STATE = 2), reinicia
    # a partida do jogo.
    elif GAME_STATE == 2:
        restart_window()
    
    # Atualiza a janela de jogo cada vez que o game loop roda
    window.update()

Pronto! Seu Space Invaders está completo!

Se você chegou até esta parte do tutorial e o seu jogo funcionou corretamente, parabéns! Você acabou de finalizar o seu primeiro Space Invaders!

O que fazer agora?

Agora que seu jogo está pronto, está na hora de melhorá-lo! Faça uma lista com os melhoramentos que você deseja fazer em seu jogo e mãos à obra. Caso você não tenha muitas ideias, eis algumas sugestões de adições que você pode fazer ao seu Space Invaders:

  • adicionar mais efeitos sonoros (explosão quando um inimigo é destuído, música de fundo, som do motor da nave do jogador etc);
  • adicionar tipos diferentes de inimigos, utilizando imagens diferentes;
  • criar um ranking de pontuações, para você poder competir com seus amigos;

As possibilidades são infinitas! Utiliza a sua criatividade e teste os seus limites melhorando esse jogo. Agora só depende de você!


Créditos do Tutorial: Adônis Gasiglia