Давайте-ка наваяем PumpKeen Game. Как Commander Keen, только про Pumpkin (тыкву). Хэллоуин же

от автора

Лучший способ отпугнуть монстров на Хэллоуин — это не только свечку в тыкве зажечь, но и страшную игру написать.

Чтобы вы, при желании, смогли это сделать вместе со мной, не заморачиваясь настройками и установкой, выберем следующий учебный стек:

  • Python

  • Модуль p5py (p5.js, но только для Python)

  • Online-IDE в браузере

    (при желании, можно стандартно, скажем, в VS Code)

pumpkeen

 Как в старые добрые времена, графика на заставке будет… эээ… «немного» отличаться от таковой в игре

В чистом поле

Нарисуем красную/оранжевую тыкву, пока просто в стиле Майнкрафт в виде квадрата.

from p5py import * run() size(400, 300) background(0, 0, 100)  # PumpKeen Game (like a Commander Keen) x = width / 2 y = height - 30 size = 20  # Персонаж fill(255, 0, 0) rect((x, y), size, size)

Чтобы открыть в online-IDE, нажмите здесь.

pumpkeen

Сделаем классно

Но давайте, что ль, сразу в ООП писать? Персонаж у нас уже есть. Создадим для него класс, экземпляр и всё такое:

from p5py import * run()  # PumpKeen Game (Helloween + Commander Keen) size(400, 300)  # Персонаж class Player:     def __init__(self):         self.x = width / 2         self.y = height - 30         self.size = 20      def display(self):         fill(255, 0, 0)         rect((self.x, self.y), self.size, self.size)  player = Player()  def draw():     background(0, 0, 100)     player.display()

Вообще, для одного персонажа можно и без классов обойтись. Но так код более структурный получается.

Код

Главное — не стоять на месте

У нас, конечно, не Doom, а PumpKeen, но… Добавим движение. Чтобы стрелочками вправо-влево можно было перемещаться.

from p5py import * run()  # PumpKeen Game (Helloween + Commander Keen) size(400, 300)  # Персонаж class Player:     def __init__(self):         self.x = width / 2         self.y = height - 30         self.speed = 5         self.size = 20      def move(self):         if keyCode == LEFT_ARROW:             self.x -= self.speed         elif keyCode == RIGHT_ARROW:             self.x += self.speed      def display(self):         fill(255, 0, 0)         rect((self.x, self.y), self.size, self.size)  player = Player()  def draw():     background(0, 0, 100)     player.display()  def key_pressed():     player.move()

Код

Откуда взялись константы LEFT_ARROW и RIGHT_ARROW? Константы BACKSPACE, DELETE, ENTER, RETURN, TAB, ESCAPE, SHIFT, CONTROL, OPTION, ALT, UP_ARROW, DOWN_ARROW, LEFT_ARROW и RIGHT_ARROW — это просто удобные сокращения кодов клавиш. Их можно найти на сайте вроде keycode.info.

Ну вот и почти готов платформер. Герой есть, платформа есть.

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

Беги, Пампкин, беги…

Перенесём движение персонажа в главный игровой цикл, чтобы при нажатии кнопки игрок продолжал двигаться:

def draw():     background(0, 0, 100)     player.move()     player.display()p

Теперь если нажать стрелку влево или вправо, он начинает бежать без остановки.

Эти прыжки посложнее, чем я думал

Прыжки!
Какой же платформер без прыжков? Введём переменную для отслеживания его состояния:

self.is_jumping = False self.jump_speed = 10 self.velocity_y = 0

Гравитация!
Добавим гравитацию, чтобы персонаж мог вернуться на землю после прыжка:

self.gravity = 0.5

Добавим метод «прыжок» Пампкину:

Суть простая — если не прыгал, то теперь прыгает:

def jump(self):     if not self.is_jumping:         self.is_jumping = True         self.velocity_y = self.jump_speed

Границы!
Заодно добавим проверку, чтобы персонаж не выходил за пределы экрана:

self.x = constrain(self.x, 0, width - self.size)

constrain() — функция из p5.js, она же и в p5py. Проверка на границы – частая задача в играх и анимации

Соберём всё вместе и расширим метод move():

def move(self):         if keyCode == LEFT_ARROW:             self.x -= self.speed         elif keyCode == RIGHT_ARROW:             self.x += self.speed         if self.is_jumping:             self.y -= self.velocity_y             self.velocity_y -= self.gravity              # Если персонаж снова на земле             if self.y >= height - 30:                 self.y = height - 30                 self.is_jumping = False                 self.velocity_y = 0          # Ограничение по ширине         self.x = constrain(self.x, 0, width - self.size)

Поправим key_pressed(), теперь будем в нём отслеживать только прыжок:

def key_pressed():     if key == ' ':         player.jump()  # Нажмите пробел для прыжка

Довольно резво теперь бегает и прыгает. 

pumpkeen

Проверьте: код

Но, похоже, ему там скучно. Обещали праздник, Halloween, а посадили в пустую коробку. Сейчас поправим…

Декоративные платформы

Добавим платформы, по которым Пампкин сможет прыгать. Когда-нибудь. Но это не точно.

class Platform:     def __init__(self, x, y, width):         self.x = x         self.y = y         self.width = width      def display(self):         fill(0, 255, 0)         rect((self.x, self.y), self.width, 10)  platforms = [Platform(0, height - 50, 100), Platform(200, height - 100, 100), Platform(400, height - 150, 100)]  def draw():     background(0, 0, 100)     player.move()     player.display()     for platform in platforms:         platform.display()

Пока платформы только отображаются, а запрыгнуть на них никак нельзя. Декорация — она и в Хэллоуин декорация.

Сбор всякой нечисти

Можно добавить возможность сбора предметов (например, монеток) для набора очков.

class Collectible:     def __init__(self, x, y):         self.x = x         self.y = y         self.size = 15      def display(self):         fill(255, 165, 0)         ellipse((self.x, self.y), self.size, self.size)  collectibles = [Collectible(rand(0, width), rand(0, height - 100)) for _ in range(5)]  def draw():     background(0, 0, 100)     player.move()     player.display()     for platform in platforms:         platform.display()     for collectible in collectibles:         collectible.display()

Монетки, как вы заметили, у нас пока просто кружочки. «Концептъ», так сказать.

Фармим

Можно добавить систему очков. Они будут увеличиваться при сборе предметов.

score = 0  def check_collectibles():     global score     for collectible in collectibles:         if (player.x < collectible.x + collectible.size and             player.x + player.size > collectible.x and             player.y < collectible.y + collectible.size and             player.y + player.size > collectible.y):             collectibles.remove(collectible)             score += 1

Не забудем вызывать check_collectibles() в функции draw.

def draw():     background(0, 0, 100)     player.move()     player.display()     for platform in platforms:         platform.display()     for collectible in collectibles:         collectible.display()     check_collectibles()     fill(255)     text(f"Очки: {score}", (10, 20))  # Показать очки на экране

Можно порезвиться

pumpkeen

Код

Наш Пампкин теперь бегает и прыгает, собирает монетки и получает за это очки. А ещё он очень пытается запрыгнуть на платформу, но это ему пока не удаётся…

Подправим сеттинг

Заодно подправим цветовую гамму. Пусть будет более хэллоуински.

Код

Заменим квадрат на тыкву:

class Player:     def display(self):         # fill(255, 140, 0)  # Оранжевый цвет для тыквы         # rect((self.x, self.y), self.size, self.size)         text_size(self.size)         text_align(LEFT, TOP)         text("🎃", (self.x, self.y))

А кружочки на алмазы:

class Collectible:     def display(self):         # fill(255, 165, 0)         # ellipse((self.x, self.y), self.size, self.size)         text_size(self.size)         text_align(CENTER, CENTER)         text("💎", (self.x, self.y))
pumpkeen

Код

Финальный штрих. Превратим декоративные платформы в настоящие

Запрыгиваем на платформу. Всё что нужно сделать, это добавить проверку столкновения с платформами:

on_ground = False for platform in platforms:     if (self.x + self.size > platform.x and         self.x < platform.x + platform.width and         self.y + self.size >= platform.y and         self.y + self.size <= platform.y + 10):  # Учитываем высоту платформы         self.y = platform.y - self.size  # Поставить персонажа на платформу         self.velocity_y = 0         on_ground = True         self.is_jumping = False if not on_ground and self.y < height - 30 and not self.is_jumping:  # Если не на платформе и не на земле и не в прыжке         self.y -= self.velocity_y         self.velocity_y -= self.gravity

Код

Разберём их чуть подробней

on_ground = False: эта переменная используется для отслеживания того, находится ли персонаж на платформе (то есть на земле). Изначально предполагается, что персонаж не находится на земле.

Цикл for platform in platforms: проходит по всем платформам, которые были созданы в игре.

Условия столкновения, проверяют, пересекается ли область персонажа с платформой::

if (self.x + self.size > platform.x and     self.x < platform.x + platform.width and     self.y + self.size >= platform.y and     self.y + self.size <= platform.y + 10):

self.x + self.size &gt; platform.x: проверяет, что правый край персонажа находится справа от левого края платформы.

self.x &lt; platform.x + platform.width: проверяет, что левый край персонажа находится слева от правого края платформы.

self.y + self.size &gt;= platform.y: проверяет, что нижний край персонажа находится выше верхнего края платформы.

self.y + self.size &lt;= platform.y + 10: проверяет, что нижний край персонажа не проходит ниже низа платформы (учитывается высота платформы, которая равна 10).

self.y = platform.y - self.size: если персонаж сталкивается с платформой, его положение (по вертикали) устанавливается так, чтобы он «стоял» на платформе.

self.velocity_y = 0: сбрасывает вертикальную скорость персонажа, чтобы он не продолжал «падение» после столкновения с платформой.

on_ground = True: теперь мы знаем, что персонаж на земле, поэтому устанавливаем эту переменную в True.

if not on_ground and self.y &lt; height - 30 and not self.is_jumping:: условный оператор проверяет, не находится ли персонаж на платформе (или на земле). Если персонаж не на земле и не прыгает, то его положение будет обновляться для симуляции гравитации.

self.y -= self.velocity_y и self.velocity_y -= self.gravity: обновляет положение персонажа с учётом гравитации. Если персонаж «в воздухе», то его вертикальная скорость уменьшается, что создаёт эффект падения…

Больше платформ, доступнее бонусы

Добавим новые платформы:

platforms = [     Platform(0, height - 50, 100),     Platform(200, height - 100, 100),     Platform(400, height - 150, 100),     Platform(100, height - 200, 150),     Platform(500, height - 250, 150),     Platform(300, height - 250, 120) ]

И чуть ниже сдвинем бонусы:

collectibles = [Collectible(rand(0, width), rand(30, height - 50)) for _ in range(5)]
pumpkeen

Код

Фикс джамп

Сейчас если при беге Пампкина и нажать пробел, то он останавливается и прыгает вверх. Давайте сделаем так, чтобы он не останавливался в этом случае, а продолжал бежать и одновременно прыгнул — получится прыжок по диагонали.

Для этого нам нужна не одна переменная, а две. Первая будет отвечать за горизонтальное движение, а вторая – за вертикальное. Добавим self.velocity_x = 0

Стандартная для физики проекция вектора скорости на ось X и Y.

А заодно причешем код в соответствии с принципами ООП. Инкапсулируем поведение в класс Пампкина. Спрячем внутрь класса обращение с его переменными: player.is_moving = False. Теперь у нас красивые функции, обрабатывающие клавиатуру. Почему красивые? Они ничего не знают о внутреннем устройстве класса, а просто дёргают его методы:

def key_pressed():     if keyCode == SPACE:         player.start_jump()     if keyCode == LEFT_ARROW:         player.start_move_left()     if keyCode == RIGHT_ARROW:         player.start_move_right()  def key_released():     if keyCode == LEFT_ARROW or keyCode == RIGHT_ARROW:         player.stop_horizontal_movement()

Вот так изменится метод move():

def move(self):     self.x += self.velocity_x     if self.is_jumping:         self.y -= self.velocity_y         self.velocity_y -= self.gravity         # Если персонаж снова на земле         if self.y >= height - 30:             self.y = height - 30             self.is_jumping = False             self.velocity_y = 0             self.is_moving = False      # Проверка столкновения с платформами     on_ground = False     for platform in platforms:         if (self.x + self.size > platform.x and             self.x < platform.x + platform.width and             self.y + self.size >= platform.y and             self.y + self.size <= platform.y + 10):  # Учитываем высоту платформы             self.y = platform.y - self.size  # Поставить персонажа на платформу             self.velocity_y = 0             on_ground = True             self.is_jumping = False     if not on_ground and self.y < height - 30 and not self.is_jumping:  # Если не на платформе и не на земле             self.y -= self.velocity_y             self.velocity_y -= self.gravity      # Ограничение по ширине     self.x = constrain(self.x, 0, width - self.size)

В нём мы заменили всё вот это:

    if self.is_moving or self.is_jumping :         if keyCode == SPACE:             self.start_jump()  # Нажмите пробел для прыжка         if keyCode == LEFT_ARROW:             self.x -= self.speed         if keyCode == RIGHT_ARROW:             self.x += self.speed

На одну строчку:

self.x += self.velocity_x

Так как обработку нажатий перенесли в key_pressed() и key_released()

Код

Вы ловите багов? Красивое

Что-то всё слишком гладко, не находите? Так не бывает. А на самом деле у нас два бага:

  1. Если прыгать с самой высокой платформы вниз, то Пампкин так сильно разгоняется, что перелетает платформу на этаж ниже и не останавливается на ней.

  2. Иногда Пампкин оказывается на земле ниже обычного своего уровня. И из-за этого появляется ещё более скрытый баг: в таком случае при нажатии пробела Пампкин выпрыгивает на свой обычный уровень у земли, вместо того чтобы сделать высокий прыжок. 

    Попробуйте поймать эти ошибки.

Второй баг исправляется элементарно. Добавим ограничение по вертикали, как раньше делали по горизонтали:

self.x = constrain(self.x, 0, width - self.size) self.y = constrain(self.y, 0, height - 30)

А вот первый баг завязан на условие:

if (self.x + self.size > platform.x and     self.x < platform.x + platform.width and     self.y + self.size >= platform.y and     self.y + self.size <= platform.y + 10):

Один из вариантов решения — это смотреть на один ход вперёд. То есть если текущее положение по y выше платформы, а следующее ниже, это значит, что мы летим очень быстро и нужно притормозить.

Сначала просто добавим ещё один if, а потом применим DRY, чтобы не повторяться:

for platform in platforms:     if (self.x + self.size > platform.x and         self.x < platform.x + platform.width and         self.y + self.size >= platform.y and         self.y + self.size <= platform.y + 10):  # Учитываем высоту платформы         self.y = platform.y - self.size  # Поставить персонажа на платформу         self.velocity_y = 0         on_ground = True         self.is_jumping = False     if (self.x + self.size > platform.x and         self.x < platform.x + platform.width and         self.y + self.size < platform.y and         self.y + self.size - self.velocity_y > platform.y + 10):  # Учитываем высоту платформы         self.y = platform.y - self.size  # Поставить персонажа на платформу         self.velocity_y = 0         on_ground = True         self.is_jumping = False

Код

И сразу улучшим:

if (self.x + self.size > platform.x and     self.x < platform.x + platform.width and     (self.y + self.size >= platform.y and     self.y + self.size <= platform.y + 10 or     self.y + self.size < platform.y and     self.y + self.size - self.velocity_y > platform.y + 10)):  # Учитываем высоту платформы     self.y = platform.y - self.size  # Поставить персонажа на платформу     self.velocity_y = 0     on_ground = True     self.is_jumping = False

Код

Читабельность — вырви глаз.

PEP и 8 спешат на помощь

У нас конвенция по оформлению есть, вот пора к ней и обратиться: PEP 8 indentation. Там три рекомендуемых стиля, я выберу последний, получится так:

if (self.x + self.size > platform.x         and self.x < platform.x + platform.width         and (self.y + self.size >= platform.y             and self.y + self.size <= platform.y + 10             or self.y + self.size < platform.y             and self.y + self.size - self.velocity_y > platform.y + 10)):     self.y = platform.y - self.size     self.velocity_y = 0     on_ground = True     self.is_jumping = False

Нууу, такое… Хотя блок кода лучше видно.

На прикиде

И заодно, раз уж заговорили о стиле, пора избавиться от магического числа 10 (высота платформы). Вынесем его куда-нибудь.Где лучше всего его хранить?
В отдельной константе PLATFORM_HEIGHT = 10? Брр, нет, конечно. Это же характеристика платформы, значит, ей место в классе Platform. Как это правильно сделать? Например, так:

class Platform:     def __init__(self, x, y, width, height=10):  # Добавляем параметр height со значением по умолчанию         self.x = x         self.y = y         self.width = width         self.height = height  # Сохраняем высоту как атрибут

И заменим везде десяточки на self.height или platform.height. Заодно получили возможность задать каждой платформе свою высоту:

platforms = [     Platform(0, height - 50, 100, 10),     Platform(200, height - 100, 100),     Platform(400, height - 150, 100, 20),     Platform(100, height - 200, 150),     Platform(500, height - 250, 150, 30),     Platform(300, height - 250, 120) ]

ФИНАЛЬНЫЙ РЕЗУЛЬТАТ

final

Код

Баги закончились?

А вот и нет. Как только мы сделали платформу толще, становится заметно, что в её середине спавнятся бонусы. И как их забрать? Эту доработку оставлю вам 🙂

Сможете репродуцировать этот баг? А пофиксить?

Разработка игры никогда не заканчивается. Но мы с вами можем продолжить в следующей части. Монстры, враги, бонусы, конец игры, заставка — много ещё что придумаем. Жду вас на продолжении.

Ну вот и всё

Если вам понравилось экспериментировать и вы заинтересовались развитием или использованием p5py, то подключайтесь к новой группе

А ещё можно глянуть:

— прошлую статью на Хабре про то, как мы пишем Игру Жизнь на p5py;
— или про книгу, с которой всё и началось.


ссылка на оригинал статьи https://habr.com/ru/articles/855374/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *