Говорят, что на Python легко и просто создавать платформеры. Правда ли?

Предисловие

Все мы были когда-то детьми. И те, кто вырос в 90-ые, наверняка играли в приставку Денди (в США она называлась NES - Nintendo Entertainment System). Среди всех игр была одна игра, которая мне особенно запомнилась, так как была не похожа на все остальные. Это игра The Addams Family (1992 год, студия Ocean). В игре была какая-то своя атмосфера. И мне захотелось создать что-то похожее.

Придумываем сюжет и геймплей

Сюжет в игре будет очень простым: мы купили поместье викторианской эпохи, и когда туда приехали, нам очень сильно захотелось заснуть. При этом мы обратили внимание, что повсюду разбросаны детали некоторого механизма. Проблема в том, что дверь в поместье закрыта и нам надо найти ключи. Ходим ищем ключи от дверей, собирая детали, а когда открываем двери ищем кровать, чтобы поспать. Видим очень странные сны, в которых также собираем предметы, уклоняясь от врагов и шипов. И в конце находим проблему этого поместья.

Рисуем ассеты

Перед тем как приступить к созданию ассетов (разных картинок, которые потом можно будет анимировать), надо разобраться в каком разрешении будет работать игра. У всех разные мониторы с разным разрешением. И не у всех даже FullHD. Поэтому выбираем 720p - то есть разрешение 1280х720 пикселей. Для такого разрешения органично будут смотреться плитки земли размером 64х64 пикселя. Берем любой векторный графический редактор и начинаем рисовать.

Рисуем ассеты поместья
Рисуем ассеты поместья

Рисовал как мог, вспоминая как у всех ребят в школе по рисованию было «5», и только у меня одного «4». Вы могли заметить, что это все статические объекты, которые не имеют анимации. Ну а как же с анимированными объектами? Все просто, анимированные объекты - это те же отдельные картинки, у которых изменяется положение некоторых пикселей. Вот пример анимации на примере главного героя игры:

Анимация главного героя
Анимация главного героя

Собственно программирование

Инструмент для создания игр мы будем выбирать на основании того языка программирования, который более-менее знаем. В моем случае, это Python. Выбираем какую-нибудь современную версию языка - я выбрал Python 3.11.4. Для создания игр на Python существует библиотека Pygame. Причем это не графический движок типа Unity. Здесь нет графического интерфейса, а есть только набор разных классов, в которых есть свои атрибуты и методы. Эта библиотека служит для осуществления манипуляции графическими объектами на основе событий (events).

Заполняем словарики

Игру начинаем даже не с файла main.py,  а с файла settings.py, куда будут прописаны основные параметры и настройки игры. Помимо названия игры, длины и ширины окна игры, мы должны прописать доступы к ассетам. В связи с тем, что данных много, а в Python есть такой тип данных как словарь (dict), в основе которого лежит хеш-таблица, мы выбираем именно словарь и заполняем. Фрагмент такого словаря:

12: {
        'style': 'enemy',
        'type': 'tile',
        'menu': 'enemy',
        'menu_surf': 'images/menu/bug.png',
        'preview': 'images/preview/bug.png',
        'graphics': 'images/enemies/cementry/bug'
    },

С чего начинается родина - с main.py

Наконец-то поработаем с main.py. Конечно, будем использовать нашу любимую концепцию ООП. Есть такая штука как принципы SOLID, которым следует придерживаться при разработке программы. Некоторые из них я собираюсь нарушить, но вот один я никак нарушить не могу - буковка “S” - Single Resposibility Principle. Суть его в том, что каждый класс должен выполнять строго обозначенную функцию и быть ограниченным своей задачей. Не надо создавать классы, которые делают все сразу. Поэтому main.py будет класс Main, в котором есть главный цикл игры - бесконечный цикл с условием While, но все события будут обрабатываться в других отдельных классах, а в Main мы будем создавать экземпляры этих других классов.

Режим редакторы игры

Как думаете, что объединяет такие игры как Heroes of Might and Magic 3, The Elder Scrolls III: Morrowind и Max Payne. Их объединяет, что на диске с ними шел встроенный редактор уровней. Это мощнейший инструмент в разработке игры. Беда в том, что в Pygame ничего подобного нет, а значит мы его сами создадим. Здесь лучше найти какой-нибудь туториал. Я лично нашел на YouTube видео немецкого программиста Christian Koch “Creating a Mario Maker style game in Python” (канал ClearCode). На основе этого видео и создаем редактор уровней. Но нам надо как-нибудь сохранять те произведения искусства, которые мы создали. И здесь у нас есть выбор: либо сохранять как текстовый формат передачи данных (например, JSON), или же сохранять в бинарном виде, используя библиотеку pickle. Я решил, что для уровней у нас будут бинарные файлы, а для сохранений в процессе игры у нас будет JSON.

Сохраняем нарисованный уровень как бинарный файл
Сохраняем нарисованный уровень как бинарный файл

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

Коллизии и анимации спрайтов

Спрайт в Pygame -  это графическое изображение, которое представляет собой одну единицу анимации. Главное здесь - это коллизии и анимации. С коллизиями все просто - у классов Pygame есть встроенные методы для работы с коллизиями, например, collidepoint. Для анимаций в соответствующем классе мы создадим свой метод и используем тернарный оператор:

def animate(self, dt):
        self.frame_index += ANIMATION_SPEED * dt
        self.frame_index = 0 if self.frame_index >= len(
            self.animation_frames) else self.frame_index
        self.image = self.animation_frames[int(self.frame_index)]

Мы просто перебираем картинки в списке, и вызываем их одну за другой с помощью переменной self.frame_index. Тернарый оператор применяется, чтобы из бежать ошибки IndexError: list index out of range.

Обратите внимание на константу ANIMATION_SPEED. Дело в том, что у нас разлочен FPS в игре и скорость анимации никак не зависит от FPS.

Главное меню игры

Не помню какой обзорщик из YouTube 10-ых годов говорил, что главное меню - это как вешалка в театре. Для меня очень удобным было меню из игры The Elder Scrolls V: Skyrim - ничего лишнего. Нечто похожее и мы сделаем с учетом, что я не умею рисовать и игру мы делаем на Pygame. В главном меню мы должны сказать игроку как управлять персонажем в игре и создать возможности поменять язык, перейти в полноэкранный режим, уменьшить или увеличить громкость музыки и звуков, и перейти в режим редактора.

Музыкальная пауза

Если для звуков полно бесплатных ресурсов, где можно скачать free sounds, то музыку я решил сам написать для игры. Все рождается из импровизации. Записываем все на листочек, а потом ноты перебиваем в любой музыкальный midi-редактор, который поддерживает нотный стан.

Вот пример трека Garden из моей игры:

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

Итоги

Самое главное, что я понял, создание игры - это хорошая возможность прокачать ваши hard skills. Процесс создания игры заставляет вас решать множество задач. Библиотека Pygame - это один из самых лучших способов понять концепцию ООП в Python. Я, когда неправильно прописал условия для звуков и музыки, понял, как именно загружаются в память компьютера экземпляры классов, как в них инициализируются атрибуты и как работают методы. Также вы повторите весь базовый синтаксис Python - работу со всеми типами данных, включая файлы, и со стандартными библиотеками типа math, random, configparser, json и pickle.

Полный код игры доступен на моем GitHub

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

И что же получилось на выходе

Комментарии (19)


  1. ignorabimus
    09.10.2023 08:44
    +3

    Это ж Dangerous Dave в гриме! )))


    1. vesper-bot
      09.10.2023 08:44

      А шотган где?


  1. Zara6502
    09.10.2023 08:44

    у мня вопрос, я когда пишу на C# то для меня самое тяжелое это разбор ошибок и их исправление, учитывая что в питоне с этим всё еще хуже - как вы занимаетесь отладкой таких проектов? я даже скрипт на 30 строк порой пишу 3 часа как раз занимаясь отладкой.


    1. IvanZaycev0717 Автор
      09.10.2023 08:44

      я бы не сказал, что отладка в Python уж слишком сложна. Для интерпритируемых языков легче делать отладку, чем для компилируемых. Когда запускаешь проект с ошибкой у тебя сразу в консоль выводится, в какой строке ошибка и с чем она связано (например, AttrubuteError, когда в класс что-то не передал). Плюс у Python есть прекрасная IDE Pycharm и можно дебаггером отследить в какой именно момент произошла ошибка. Поэтому проблем особых с этим не было


      1. Zara6502
        09.10.2023 08:44

        в консоль выводится, в какой строке ошибка и с чем она связано

        у меня в основном с этим и проблема, строку пишет, а что конкретно не понравилось - не пишет.


        1. Andrey_Solomatin
          09.10.2023 08:44

          В Питоне добавляются ошибки типов (хотя как там с null-safty в С#?), а так тоже самое.

          Используйте собственные исключения с контекстом, вместо встроенных в С#.
          Делайте в них информативыне сообщения об ошибках.

          Если в мапе нет ключа, толко вы как автор приложения знаете, что это значит и что нужно делать.



          1. Zara6502
            09.10.2023 08:44
            -1

            Используйте собственные исключения с контекстом

            это вы про этап выполнения? с этим проблем нет, проблема на этапе компиляции или оформления, то пробел забыл, то запятую, в C# это проще.


            1. Andrey_Solomatin
              09.10.2023 08:44
              +1

              Мой выбор для Питона это IDE, аннотации типов и куча линтеров в прекоммите. Хорошо помогают ловить опечатки и прочие синтаксические ошибки.

              Такие вещи как незакрытые скобки имеют тенденцию показывать ошибку где-то после того как скобка пропущенна, такие проблемы минимизируются хорошей декомпозицией или вынесением шагов в переменную.

              Чем более плоский код вы пишите, тем сложнее с ним ошибиться.

              Если у вас большие структуры, возможно это вообще не код, а данные.
              Если взять пример автора, вот эту часть можно в отдельный тектовый файл убрать. Взять какой-нибудь популярный фомат, с возможностью провреки на базовый синтаксис и схему (yaml, json, toml?).

              https://github.com/IvanZaycev0717/the_mystery_of_the_mansion/blob/main/stages/main.py#L357


              1. Zara6502
                09.10.2023 08:44

                спасибо за пояснения, я слишком редко общаюсь с питоном и думаю просто застрял в 2005 году.


  1. MajorMotokoKusanagi
    09.10.2023 08:44
    +1

    Раскрыть бы чуть подробнее сюжет и геймплей, описать механики уровней/этапов, вписав в историю аномалии/демона/полтергейста/проклятия поместья. В общей части.
    Раскрыть движок в плане функционала, анимация в котором реализуется через или массив спрайтов (удобство чего и послужило выбором типа движка), или ещё как. Обработка столкновений (коллизий) — туда же. Звуковое сопровождение игры — в функционале движка или самому довешивать коллбэки на события. То же с меню. С алгоритмически/общим описанием и внятными кусками кода. И прочие детали реализации.
    А публикация в текущем варианте — это набросок черновика, который как-то вышел из песочницы. Автору прочитать Марио Цехнера, например.


    1. IvanZaycev0717 Автор
      09.10.2023 08:44
      +1

      тогда получилась бы "Война и Мир". Здесь просто описание того, что можно сделать, речь о самой возможности реализации на Python с минимальным описанием как делать. Статья создавалась с тем фокусом, чтобы не забивать людям голову и не грузить излишними техническими подробностями


  1. AcckiyGerman
    09.10.2023 08:44
    +1

    Мне понравился дизайн главного героя! Костюмчик с галстуком смотрится свежо)


    1. vassabi
      09.10.2023 08:44

      все ожидал что будет элегантный удар\бросок тростью, но как видно - у автора не было на это времени :)


  1. Andrey_Solomatin
    09.10.2023 08:44

    Мы просто перебираем картинки в списке, и вызываем их одну за другой с
    помощью переменной self.frame_index. Тернарый оператор применяется,
    чтобы из бежать ошибки IndexError: list index out of range.


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

    self.frame_index = self.frame_index % len(self.animation_frames)

    Разница в том, что этот код может проскочить первую анимацию.



  1. Andrey_Solomatin
    09.10.2023 08:44
    +1

    но вот один я никак нарушить не могу - буковка “S” - Single
    Resposibility Principle. Суть его в том, что каждый класс должен
    выполнять строго обозначенную функцию и быть ограниченным своей задачей.


    А как же https://github.com/IvanZaycev0717/the_mystery_of_the_mansion/blob/main/stages/level.py ?


  1. Andrey_Solomatin
    09.10.2023 08:44
    +1

    Инструмент для переводов.

    Размечаете то, что должно быть переведено в коде
    Запускаете тулзу которая парсит все включения и создаёт/обновляет текстовый файл
    Делаете переводы
    Компилируете его в бинарный
    Используете в приложении этот файл.

    https://phrase.com/blog/posts/translate-python-gnu-gettext/


    1. IvanZaycev0717 Автор
      09.10.2023 08:44

      Спасибо, Андрей за ценные комментарии, почти код-ревью)))


  1. Delnor
    09.10.2023 08:44

    Хах, главный герой просто шикарен)


  1. temabed
    09.10.2023 08:44
    +1

    Прекрасная работа. Брависсимо!