Лучший способ отпугнуть монстров на Хэллоуин — это не только свечку в тыкве зажечь, но и страшную игру написать.
Чтобы вы, при желании, смогли это сделать вместе со мной, не заморачиваясь настройками и установкой, выберем следующий учебный стек:
Python
Модуль p5py (p5.js, но только для Python)
-
Online-IDE в браузере
(при желании, можно стандартно, скажем, в VS Code)
В чистом поле
Нарисуем красную/оранжевую тыкву, пока просто в стиле Майнкрафт в виде квадрата.
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, нажмите здесь.
Сделаем классно
Но давайте, что ль, сразу в ООП писать? Персонаж у нас уже есть. Создадим для него класс, экземпляр и всё такое:
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() # Нажмите пробел для прыжка
Довольно резво теперь бегает и прыгает.
Проверьте: код
Но, похоже, ему там скучно. Обещали праздник, 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)) # Показать очки на экране
Можно порезвиться
Наш Пампкин теперь бегает и прыгает, собирает монетки и получает за это очки. А ещё он очень пытается запрыгнуть на платформу, но это ему пока не удаётся...
Подправим сеттинг
Заодно подправим цветовую гамму. Пусть будет более хэллоуински.
Заменим квадрат на тыкву:
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))
Финальный штрих. Превратим декоративные платформы в настоящие
Запрыгиваем на платформу. Всё что нужно сделать, это добавить проверку столкновения с платформами:
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 > platform.x
: проверяет, что правый край персонажа находится справа от левого края платформы.
self.x < platform.x + platform.width
: проверяет, что левый край персонажа находится слева от правого края платформы.
self.y + self.size >= platform.y
: проверяет, что нижний край персонажа находится выше верхнего края платформы.
self.y + self.size <= 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 < 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)]
Фикс джамп
Сейчас если при беге Пампкина и нажать пробел, то он останавливается и прыгает вверх. Давайте сделаем так, чтобы он не останавливался в этом случае, а продолжал бежать и одновременно прыгнул — получится прыжок по диагонали.
Для этого нам нужна не одна переменная, а две. Первая будет отвечать за горизонтальное движение, а вторая – за вертикальное. Добавим 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().
Вы ловите багов? Красивое
Что-то всё слишком гладко, не находите? Так не бывает. А на самом деле у нас два бага:
Если прыгать с самой высокой платформы вниз, то Пампкин так сильно разгоняется, что перелетает платформу на этаж ниже и не останавливается на ней.
-
Иногда Пампкин оказывается на земле ниже обычного своего уровня. И из-за этого появляется ещё более скрытый баг: в таком случае при нажатии пробела Пампкин выпрыгивает на свой обычный уровень у земли, вместо того чтобы сделать высокий прыжок.
Попробуйте поймать эти ошибки.
Второй баг исправляется элементарно. Добавим ограничение по вертикали, как раньше делали по горизонтали:
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)
]
ФИНАЛЬНЫЙ РЕЗУЛЬТАТ
Баги закончились?
А вот и нет. Как только мы сделали платформу толще, становится заметно, что в её середине спавнятся бонусы. И как их забрать? Эту доработку оставлю вам :)
Сможете репродуцировать этот баг? А пофиксить?
Разработка игры никогда не заканчивается. Но мы с вами можем продолжить в следующей части. Монстры, враги, бонусы, конец игры, заставка — много ещё что придумаем. Жду вас на продолжении.
Ну вот и всё
Если вам понравилось экспериментировать и вы заинтересовались развитием или использованием p5py
, то подключайтесь к новой группе.
А ещё можно глянуть:
— прошлую статью на Хабре про то, как мы пишем Игру Жизнь на p5py;
— или про книгу, с которой всё и началось.