Лучший способ отпугнуть монстров на Хэллоуин — это не только свечку в тыкве зажечь, но и страшную игру написать.
Чтобы вы, при желании, смогли это сделать вместе со мной, не заморачиваясь настройками и установкой, выберем следующий учебный стек:
-
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;
— или про книгу, с которой всё и началось.
ссылка на оригинал статьи https://habr.com/ru/articles/855374/
Добавить комментарий