Part 14

Игровой проект

В этой части мы используем pygame, чтобы создать немного более крупную игру. Это вариант классической игры Sokoban, где игрок перемещает робота по сетке и толкает коробки в правильные места, стараясь сделать как можно меньше ходов.

Итоговый результат будет выглядеть так:

game

Карта игры

Начнем с рисования карты, используемой в игре. Игра реализована в классе Sokoban, который будет содержать всю функциональность, необходимую для игры. На первом этапе содержимое класса выглядит так:

import pygame

class Sokoban:
    def __init__(self):
        pygame.init()
        
        self.load_images()
        self.new_game()
        
        self.height = len(self.map)
        self.width = len(self.map[0])
        self.scale = self.images[0].get_width()

        window_height = self.scale * self.height
        window_width = self.scale * self.width
        self.window = pygame.display.set_mode((window_width, window_height))

        pygame.display.set_caption("Sokoban")

        self.main_loop()

    def load_images(self):
        self.images = []
        for name in ["floor", "wall", "target", "box", "robot", "done", "target_robot"]:
            self.images.append(pygame.image.load(name + ".png"))

    def new_game(self):
        self.map = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                    [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1],
                    [1, 2, 3, 0, 0, 0, 1, 0, 0, 1, 2, 3, 0, 0, 0, 0, 1],
                    [1, 0, 0, 1, 2, 3, 0, 2, 3, 0, 0, 0, 1, 0, 0, 0, 1],
                    [1, 0, 4, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1],
                    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]

    def main_loop(self):
        while True:
            self.check_events()
            self.draw_window()

    def check_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                exit()

    def draw_window(self):
        self.window.fill((0, 0, 0))

        for y in range(self.height):
            for x in range(self.width):
                square = self.map[y][x]
                self.window.blit(self.images[square], (x * self.scale, y * self.scale))

        pygame.display.flip()

if __name__ == "__main__":
    Sokoban()

Запуск программы должен показать окно с начальным состоянием игры. Посмотрим подробнее на код, который это делает.

Конструктор

Конструктор класса инициализирует модули pygame, а также основные переменные и структуры данных, участвующие в игре. Он также вызывает метод главного цикла игры.

    def __init__(self):
        pygame.init()
        
        self.load_images()
        self.new_game()
        
        self.height = len(self.map)
        self.width = len(self.map[0])
        self.scale = self.images[0].get_width()

        window_height = self.scale * self.height
        window_width = self.scale * self.width
        self.window = pygame.display.set_mode((window_width, window_height))

        pygame.display.set_caption("Sokoban")

        self.main_loop()

Метод load_images загружает изображения, используемые в игре, в список с именем images. Метод new_game создает двумерный список с именем map, который содержит состояние игровой сетки в начале игры.

Переменные height и width инициализируются на основе размеров игровой сетки. Переменная scale содержит длину стороны одной клетки сетки. Поскольку каждое изображение является квадратом точно такого же размера, размер всех клеток покрывается этой одной переменной, и ширина первого изображения вполне подходит в качестве значения. Это же значение можно использовать для вычисления ширины и высоты всей сетки, что позволяет создать окно подходящего размера для отображения игровой сетки.

Загрузка изображений

Метод load_images загружает все изображения, используемые в игре:

    def load_images(self):
        self.images = []
        for name in ["floor", "wall", "target", "box", "robot", "done", "target_robot"]:
            self.images.append(pygame.image.load(name + ".png"))

В игре используются следующие изображения:

Клетка пола

floor
  • Имя файла: floor.png
  • Позиция в списке: 0

Клетка стены

wall
  • Имя файла: wall.png
  • Позиция в списке: 1

Целевая клетка

target
  • Имя файла: target.png
  • Позиция в списке: 2
  • Робот должен передвинуть какую-нибудь коробку на эту клетку

Коробка

box
  • Имя файла: box.png
  • Позиция в списке: 3

Робот

robot
  • Имя файла: robot.png
  • Позиция в списке: 4

Коробка на целевой клетке

done
  • Имя файла: done.png
  • Позиция в списке: 5
  • Коробка была перемещена на целевую клетку

Робот на целевой клетке

target robot
  • Имя файла: target_robot.png
  • Позиция в списке: 6
  • Робот также может находиться на пустой целевой клетке

Создание сетки

Метод new_game создает начальное состояние игровой сетки:

    def new_game(self):
        self.map = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
                    [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1],
                    [1, 2, 3, 0, 0, 0, 1, 0, 0, 1, 2, 3, 0, 0, 0, 0, 1],
                    [1, 0, 0, 1, 2, 3, 0, 2, 3, 0, 0, 0, 1, 0, 0, 0, 1],
                    [1, 0, 4, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1],
                    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]

Метод создает двумерный список с именем map, который использует пронумерованные позиции изображений в их списке, чтобы отметить, какое изображение куда попадает. Так игра всегда содержит запись состояния игровой сетки.

Обратите внимание: в начале все клетки сетки содержат число от 0 до 4. Числа 5 и 6 не включены, поскольку в начале ни коробка, ни робот не находятся на целевой клетке.

Главный цикл

Метод main_loop довольно короткий. На каждой итерации он вызывает два метода: check_events проходит по событиям, собранным с предыдущей итерации, а метод draw_window обновляет содержимое окна.

    def main_loop(self):
        while True:
            self.check_events()
            self.draw_window()

    def check_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                exit()

    def draw_window(self):
        self.window.fill((0, 0, 0))

        for y in range(self.height):
            for x in range(self.width):
                square = self.map[y][x]
                self.window.blit(self.images[square], (x * self.scale, y * self.scale))

        pygame.display.flip()

На этом этапе единственное событие, которое действительно обрабатывает игра, — закрытие игрового окна, например кнопкой выхода. Тогда игра завершается вызовом функции Python exit.

Каждый раз, когда вызывается метод draw_window, проходится вся игровая сетка, и изображение, соответствующее каждой клетке, рисуется в правильном месте.

Обратите внимание: координаты x и y используются в игре двумя разными способами. При работе с индексами двумерного списка логично сначала указывать координату y, поскольку y относится к номеру строки, а x — к номеру столбца. С другой стороны, при использовании методов pygame обычно сначала передается x, как это часто бывает при работе с графикой, а также в математических контекстах.

Вы дошли до конца этого раздела! Перейти к следующему разделу:

Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.

В этой части:
  1. 1. Игровой проект

  2. 2. Робот и коробки

  3. 3. Завершение игры

  4. 4. Ваша собственная игра