Найти тему
ZDG

Пишем Питона на Питоне #8: Геймплей

Предыдущие части: Есть ли у него душа, Организация ввода, View, Визуализация поля, Загрузка уровня, INI-файл, Пишем Питона на Питоне!

В этой части мы получим работающую игру. Но в голом виде. Но работающую.

Файлы становятся большие, и копировать их сюда теряет смысл. Я сделал репозиторий на github, где вы можете смотреть все файлы онлайн. Вы также можете выгрузить оттуда весь проект к себе.

Сначала доработаем класс Level, чтобы при загрузке он считал количество свободных клеток. Это будет свойство free_cells.

Также в этом классе вместо константы PYTHON (тело питона) сделаем две отдельные константы: HEAD (голова) и TAIL (хвост). Они обе тело, но голова визуально отличается при рендере и требует отдельной константы.

Затем перепишем класс Python, который был просто заглушкой.

Во-первых, мы добавили константы (статические свойства), описывающие состояния питона:

  • INIT – стартовая позиция. В этом режиме питон не движется, пока игрок не задаст направление движения.
  • MOVE – обычное движение, когда движутся и голова, и хвост.
  • GROW – движется только голова, длина питона растёт. Это происходит, когда питон съел яблоко.
  • SHRINK – движется только хвост, длина питона сокращается. Это происходит, когда питон врезался в стенку или в себя. После того, как его длина сократится до нуля, питон перейдёт в финальное состояние DEAD.
  • DEAD – питон больше ничего не может делать.

Управляющим событием для питона является задание направления движения – метод set_dir().

Свойство count – счётчик состояния. Например, питон съел яблоко и должен удлиниться на 5 клеток. Мы устанавливаем mode = GROW и count = 5. На каждом шаге движения счётчик уменьшается на 1, если он не нулевой. Значит, в состоянии GROW голова питона отрастёт 5 раз. После этого счётчик обнулится, и из состояния GROW мы вернёмся в MOVE, т.е. обычное движение. Проверкой счётчика и переключением состояний занимается метод update().

Состояние INIT всегда переходит в GROW со счётчиком 1. Это сделано для того, чтобы в стартовой позиции не думать о том, как расположить хвост питона относительно головы. Питон на старте будет состоять из одной головы, а при начале движения вырастет на 1 сегмент автоматически.

Свойство-список body в комплекте со свойствами head (голова) и tail (хвост) описывают кольцевой буфер, в котором хранится тело питона. Буфер создаётся такой длины, какая передана в параметре конструктора buffer_size (для этого в классе Level и нужно вычислять количество свободных клеток).

Методы movе_head() и move_tail() предназначены, соответственно, для движения головы и хвоста. В задачи методов входит проверка состояний и корректная работа с указателями кольцевого буфера.

Наконец, метод set_mode() устанавливает состояние питона и счётчик состояния. Задача метода в том, чтобы предыдущий счётчик не обнулялся, если, допустим, питон съел 2 яблока подряд. То есть если он находится в состоянии GROW и его счётчик равен 2, а мы переводим его в состояние GROW со счётчиком 5, то, так как состояния одинаковые, счётчики надо сложить и получится 7.

Контроллер

В задачи контроллера GameController теперь входит проверка, куда направляется голова питона, и принятие решений по результату.

  1. Сдвинуть хвост, вызвав метод move_tail(). Это всегда безопасная операция. Её надо делать первой, чтобы при движении голова не наехала на кончик хвоста, который не успел сдвинуться.
  2. Получить от питона адрес его головы и направление движения, из них вычислить новый адрес головы.
  3. Если по новому адресу расположено яблоко, вызвать у питона метод set_mode(Python.GROW, 5)
  4. Если по новому адресу стена или тело питона, то откатить адрес назад на старый, и вызвать у питона метод set_mode(Python.SHRINK, python.length), то есть счетчик состояния будет равен длине питона, чтобы он укоротился полностью.
  5. Вызвать метод питона move_head() с новым адресом головы, чтобы сдвинуть голову на новый адрес.
  6. Если пользователь нажимал управляющие клавиши, вычислить нужное направление движения и вызвать у питона метод set_dir()

Все эти проверки имеют смысл только когда голова питона движется, т.е. это состояния MOVE или GROW.

Но в любом случае контроллер должен получить адрес головы и хвоста, чтобы обновлять содержимое модели уровня. Движется голова или хвост, значения не имеет. Содержимое по адресу головы заменяется на константу Level.TAIL, содержимое по адресу хвоста заменяется на Level.FIELD, после чего мы двигаем питона и получаем новые адреса головы и хвоста. Теперь по новому адресу головы мы записываем Level.HEAD, а по новому адресу хвоста – Level.TAIL. Если голова или хвост не двигались, то они просто перезапишутся на старые места и содержимое клеток уровня останется тем же, что и было.

Контроллер также отслеживает состояние DEAD у питона, и как только заметит его, сбрасывает игру в начальное состояние (метод reset_python()).

Метод create_apple() создаёт на карте яблоко по случайному адресу. Работает он примитивно: выбирает случайный адрес и проверяет, свободен ли он. Если несвободен, то подбор случайного адреса повторяется. Это может принести проблем в будущем, но оптимизировать будем позже.

Наконец, изменена обработка событий в main.py. Теперь игра не ждёт, когда поступит событие, а работает с частотой 30 кадров в секунду, позволяя контроллеру перерисовывать змею в движении. Эти изменения я также не могу пока обсуждать, так как объём материала становится очень большим, а писать можно вечно. Поэтому я выкладываю код конкретно этой части на Яндекс-диск, а если вам что-то интересно или непонятно, давайте обсуждать в комментах.

Мы получили прототип работающей игры. Не забывайте, что вы можете отредактировать файл level.txt и создать свой собственный уровень. В следующих частях сделаем новое представление – экран с заставкой. Также нас ждут: подсчёт и вывод на экран очков и жизней; улучшения дизайна; таблица рекордов; настройки. И главное – пририсуем голове питона глаза, чтобы они всегда смотрели на яблоко.

Следующая часть: Главное меню

Читайте также:

  • ООП в Python: особенности реализации
  • Коллекции: Массивы