Pac-Man — классический платформер, который, наверное, сегодня известен каждому. Название «Pac-Man» происходит от японского слова «paku», что означает открывание и закрывание рта. Создателя Тору Иватани вдохновила японская история о существе, которое защищает детей от монстров, поедая их. При создании игры он опирался на ключевые слова из легенды, а глагол «съесть» стал основой всего.
(Данное изображение и все последующие взяты отсюда.)
Монстры представлены в виде четырех призраков, которые атакуют игрока последовательными «волнами», подобно космическим захватчикам. Каждый призрак также имеет уникальную личность. В истории есть еще один важный элемент, концепция жизненной силы «кокоро», которая позволяла существам поедать монстров. В игре эта энергия представлена в виде печенья с усилением, которое дает Pac-Man кратковременную способность поедать монстров.
В этом уроке я сначала проведу вас через базовую настройку, затем мы создадим игровые объекты для стены лабиринта, Pac-Man и призраков, обеспечим поиск пути в лабиринте, зададим призракам случайное движение, реализуем элементы управления стрелками для игрока и наконец, разместим еду в виде печенья по всему лабиринту.
>> basic_settings
Получившаяся игра содержит примерно 300 строк кода, поэтому я перечисляю здесь только самые важные части. Полный код доступен в моем репозитории GitHub. Первым делом установим необходимые пакеты. Нам понадобятся pygame, numpy и tcod. Установите их все с помощью инструмента pip. Если вы используете IDE, такую как PyCharm (я рекомендую ее), установка произойдет после нажатия на сообщение об ошибке отсутствующего пакета.
Во-первых, мы создадим игровое окно, аналогично предыдущему руководству по игре Space Invaders (в котором было всего 100 строк). Здесь я подготовлю параметры для указания размера окна, названия игры, частоты обновления и несколько полей данных, которые будут содержать ссылки на игровые объекты и игрока. Функция tick
итеративно проходит по всем игровым объектам и вызывает их внутреннюю логику и рендеринг. Затем остается только перерисовать всю игровую область и обработать события ввода, такие как щелчки мышью и ввод с клавиатуры. Для этой цели будет служить функция _handle_events
.
>> parent_game_object
Затем я создаю родительский игровой объект с именем GameObject
, от которого другие классы будут наследовать функциональность. В игре у нас будут объекты для стены (Wall), Pac-Man (Hero), привидения (Ghost) и печенья (Cookie). Для упомянутых выше подвижных игровых объектов позже я создам класс MovableObject
, который будет расширением класса GameObject
с функциями перемещения.
Во время инициализации объекта я задаю его цвет, форму и положение. Каждый объект также имеет ссылку на поверхность рендеринга _surface
, так что он может сам заботиться о своем рендеринге на основной поверхности. Для этой цели у нас есть функция draw
, которая вызывается ранее созданным GameRenderer
для каждого игрового объекта. В зависимости от параметра is_circle
объект отображается либо в виде круга, либо в виде прямоугольника (в нашем случае я использую квадрат со слегка закругленными углами для стен и круг для Pac-Man и печенья).
Создать класс стены будет просто. Для стен выбираю синий цвет согласно оригинальному Pac-Man (параметр цвета — Blue 255, остальное 0).
Подготовлен код для рендеринга и объект для стен. При написании следите за тем, чтобы классы Wall
и GameObject
были выше класса GameRenderer
, чтобы класс их «видел». Следующим шагом является визуализация лабиринта на экране. Но перед этим мы должны создать один вспомогательный класс.
>> the_game_controller_class
Я сохраню лабиринт в символах ASCII в переменной в новом классе PacmanGameController
. Я буду использовать исходный размер лабиринта — 28x31 тайл. Позже мне нужно будет убедиться, что призраки смогут правильно пройти через лабиринт и, возможно, найти игрока. Сначала я прочитаю лабиринт как символы и преобразую его в матрицу единиц и нулей, где стена равна нулю, а проходимое пространство равно единице. Эти значения служат алгоритму поиска пути в качестве так называемой функции стоимости. Ноль означает бесконечную стоимость прохождения, поэтому элементы в массиве, отмеченные таким образом, не будут считаться проходимыми. Обратите внимание на массив reachable_spaces
, который содержит проходимые части лабиринта. Но об этом позже, сначала я должен подготовить структуры классов. Вы можете скопировать лабиринт в формате ASCII с моего GitHub. В обозначении персонажа я использовал X
для стены, P
для Pac-Man и G
для призрака.
>> rendering_the_maze
Все необходимое для рендеринга лабиринта подготовлено, поэтому осталось только создать экземпляры наших классов PacmanGameController
, пройтись по 2D-массиву с позициями стен и создать в этих местах объект Wall
(я использую функцию add_wall
, которая не показана здесь, еще раз взгляните на полный код на моем GitHub). Я установил частоту обновления 120 кадров в секунду.
>> let’s_add_ghosts!
В оригинальном Pac-Man было четыре призрака по имени Блинки, Пинки, Инки и Клайд, каждый со своим характером и способностями. Концепция игры основана на японской сказке (подробнее здесь и здесь), а оригинальные названия на японском языке также предполагают их способности (например, у Пинки японское имя Вор, у Блинки — Тень). Однако для нашей игры мы не будем вдаваться в такие подробности, и каждый призрак будет использовать только базовый поведенческий цикл, как и в оригинале, то есть режимы «преследование», «рассеивание» и «испуг». Мы опишем и обработаем эти режимы ИИ во второй части.
Класс-призрак будет простым, унаследовав большую часть своего поведения от родительского класса MovableObject
(посмотрите мой GitHub, этот класс немного сложнее и включает логику для движения в четырех направлениях, следования по маршруту и проверки на столкновения со стенами).
Я добавлю значения RGB для цветов каждого призрака в класс PacmanGameController
и сгенерирую четыре цветных призрака в основной функции. Я также подготовлю статическую функцию для преобразования координат, которая просто преобразует координаты лабиринта (например, x=16 y=16 приблизительно соответствует центру лабиринта, и умножение на размер ячейки или тайла дает мне координату на поверхность игры в пикселях).
На этом этапе четыре призрака будут отображаться в лабиринте при запуске игры. Теперь мы хотим заставить их двигаться.
>> maze_pathfinding
Теперь наступает, пожалуй, самая сложная часть. Поиск пути в двумерном пространстве или графе — сложная задача. Реализация алгоритма решения такой задачи заняла бы отдельную статью, поэтому воспользуемся готовым решением. Наиболее эффективным алгоритмом поиска пути является алгоритм A*. Это обеспечивается пакетом tcod
, который мы установили вначале.
Чтобы перемещать призраков, я создам класс с именем Pathfinder
. В конструкторе я инициализирую массив numpy
со стоимостью прохождения (массив единиц и нулей, описанный ранее) и создам переменную класса pf
, которая будет содержать экземпляр навигатора A*. Затем функция get_path
рассчитает и вернет путь в виде серии шагов в массиве при вызове с координатами в лабиринте (откуда, куда).
Теперь я добавлю раздел к основной функции, чтобы продемонстрировать поиск пути. Я выбираю начальные координаты [1,1]
и пункт назначения маршрута [24,24]
. Это необязательный код.
В игре рендеринг кратчайшего маршрута выглядит так:
>> randomized_ghost_movement
Теперь мы собираемся создать новую функцию в классе PacmanGameController
для выбора случайной точки из массива reachable_spaces
. Каждый призрак будет использовать эту функцию после того, как достигнет места назначения. Таким образом, призраки будут просто выбирать путь от своего текущего положения в лабиринте до случайного места назначения на неопределенный срок. В следующей части мы реализуем более сложное поведение, такое как погоня и бегство от игрока.
В классе Ghost
мы добавляем новую логику для следования по пути. Функция reached_target
вызывает каждый кадр и проверяет, достиг ли уже призрак своей цели. Если да, то он определяет, в каком направлении находится следующий шаг пути через лабиринт, и начинает менять свое положение вверх, вниз, влево или вправо (логика движения вызывается в родительском классе MovableObject
).
Призраки теперь создаются в позициях, обозначенных буквой G
в исходном лабиринте ASCII, и начинают искать случайный путь. Я запер в клетке трех призраков — как и в оригинальном Pacman, они будут выпускаться по одному — и один бродит по лабиринту:
>> player_controls
Чтобы добавить функциональность плеера, я создам класс под названием Hero
. Большая часть логики управления как игроком, так и призраками обрабатывается в классе MovableObject
, поэтому нам нужно всего несколько функций для определения поведения игрока. В оригинальном Pacman игрок может двигаться в четырех направлениях, управляемых клавишами со стрелками. Если ни одна клавиша со стрелкой не нажата, игрок продолжит движение в последнем допустимом направлении. Если клавиша нажата в направлении, в котором игрок не может двигаться, это направление сохраняется и используется в следующем доступном ходе. Я воспроизведу это поведение в нашей игре, а также добавлю способность Пакмана телепортироваться из одного конца лабиринта в другой — я просто проверю, находится ли игрок вне игровой зоны с левой или правой стороны, и установлю его положение на противоположную сторону лабиринта соответственно. В Pacman также есть модифицированная функция рендеринга, нам нужно отображать его вдвое меньше, чем он обычно занимает (используя pygame.rect
).
Я создаю экземпляр класса Hero
в конце основной функции. Задаю позицию с координатами [1,1]
— unified_size
— размер одного тайла. Также нам нужно добавить обработку входных событий в класс GameRenderer
, чтобы мы могли управлять игровым персонажем.
После запуска игры теперь мы можем управлять игроком — Pacman!
>> adding_cookies
Это был бы не Пакман без печенья в лабиринте. С точки зрения игрового процесса они определяют степень исследования мира, а некоторые файлы cookie даже меняют способности призраков и Пакмана. Таким образом, они являются высшей наградой для игроков и основным показателем их прогресса в уровнях. В современных играх поведение, которое геймдизайнер хочет поощрять в игроке, обычно вознаграждается. Прекрасным примером является Elden Ring, где каждый, кто исследует каждый уголок мира, получает вознаграждение. Чем опаснее и отдаленнее, тем больше награда. С другой стороны, такие игры, как Assassin's Creed, поддерживают выполнение задач, поэтому во время игры у вас возникает ощущение, что вы работаете, а не играете.
Добавление файлов cookie будет самым простым во всем уроке, поэтому я оставил его на конец, как вишенку на торте. Я создам класс под названием Cookie
. Его экземпляр всегда будет иметь размер четыре пикселя, желтый цвет и круглую форму. В основной функции я создам куки на всех тайлах, которые мы вначале сохранили в массиве cookie_spaces
(тот же, что и reachable_spaces
). Я добавлю в плеер функцию с именем handle_cookie_pickup
, в которой я постоянно проверяю, не сталкивается ли плеер с каким-либо cookie. Если это так, я удалю файл cookie из массива, и он больше не будет отображаться.
А теперь о результате наших стараний:
Небольшой интересный факт напоследок — в оригинальной игре Пакман останавливается на один кадр после того, как съест каждое печенье, поэтому призракам легче его поймать в начале игры, когда поле еще заполнено. В следующей части мы реализуем аналогичную игровую механику, и вы также можете рассчитывать на искусственный интеллект для призраков, отслеживание очков, звуки, анимацию, текстуры, бонусы, эффекты дрожания экрана, жизни и конечные состояния игры.
Почему вы решили разрабатывать игры на Python и какой ваш самый любимый игровой движок?