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

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

  • 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;
— или про книгу, с которой всё и началось.

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