В данной статье мы расскажем, как при помощи Python и PyGame Zero можно быстро и легко написать клон игры Арканоид (Breakout).
Содержание статьи
- Установка Pygame Zero
- Функции update() и draw() в Pgzero
- Создание двигающейся панели и мячика
- Создание стенки с кирпичиками для игры Арканоид
- Добавление физики для двигающейся панели
- Добавление физики для движения мячика в игре
- Система координат в Pygame Zero
- Коллизия (обнаружения столкновений) объектов в Pygame Zero
- Отскакивание мяча от двигающей панели в игре на Pygame Zero
Что ж, приступим!
Весь код, который будет использоваться в данном руководстве, доступен по ссылке.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Изображения были взяты с Kenney. На данном сайте есть множество клевых бесплатных ассетов для создания игры. Обязательно зацените его!
Установка Pygame Zero
1 |
pip install pgzero |
Первым делом нужно создать пустое окно:
1 2 3 4 5 6 7 |
import pgzrun TITLE = "Arkanoid clone" WIDTH = 800 HEIGHT = 500 pgzrun.go() |
Здесь мы импортируем Pygame Zero. Переменная TITLE
— это заголовок отображаемого окна, а переменные WIDTH
и HEIGHT
определяют ширину и высоту данного окна соответственно.
Метод pgzrun.go()
запускает программу.
Вы должны увидеть пустое окно:
Давайте попробуем отобразить несколько игровых блоков.
Добавим следующий код под высотой и шириной:
1 2 3 4 5 6 7 8 9 10 |
WIDTH = 800 HEIGHT = 500 paddle = Actor("paddleblue.png") paddle.x = 120 paddle.y = 420 ball = Actor("ballblue.png") ball.x = 30 ball.y = 300 |
Actor
отвечает за то, как Pygame Zero будет показывать картинки. Изображения всегда хранятся в папке images
, которая находится в том же месте, где расположен скрипт игры.
1 2 3 4 5 6 7 8 |
➜ tree . ├── game.py └── images ├── ballblue.png └── paddleblue.png 1 директория, 3 файла |
Мы также указываем начальные позиции x
и y
для тех изображений, которые будут загружены:
1 2 |
paddle.x = 120 paddle.y = 420 |
Эти значения могут быть любыми, я получил вышеуказанные числа методом проб и ошибок.
Итак, у нас есть изображения, загруженные в память, однако они пока не отображаются. Давайте это исправим. В Pgzero есть встроенная функция draw()
, которая вызывается автоматически при запуске игры. Есть также функция update
, которая вызывается 60 раз в секунду и обновляет экран по мере вашего движения.
По сути, у нашей игры будет 60 FPS.
Функции update() и draw() в Pgzero
Функции update
и draw
похожи — разница лишь в том, что update
вызывается 60 раз в секунду, в то время как draw
вызывается только в тех случаях, когда что-то нужно изменить, нарисовать…
Здесь нет никаких строгих правил, но я бы использовал функцию update
для вещей, которые часто меняются. Например, для стенки с кирпичиками и мячика. В то время как для фоновых изображений можно использовать draw
.
Создание двигающейся панели и мячика
Для начала воспользуемся функцией draw()
, а update()
пока оставим пустой:
1 2 3 4 5 6 |
def draw(): paddle.draw() ball.draw() def update(): pass |
Давайте сделаем еще кое-что. Вам не кажется, что сейчас фон очень скучный? Давайте сделаем его немного повеселее.
Обновим функцию draw()
:
1 2 3 4 |
def draw(): screen.blit("background.png", (0,0)) paddle.draw() ball.draw() |
В качестве фона мы используем файл background.png
, который находится в папке images
. Функция blit()
рисует наше изображение на экране. Кортеж (0,0)
является стартовой позицией, где x=0
и y=0
. Систему координат в Pygame Zero мы обсудим немного позже.
Намного лучше!
Создание стенки с кирпичиками для игры Арканоид
Сейчас мы займемся созданием стенки с кирпичиками, которых будет сбивать мяч.
У нас в папке есть несколько подходящих изображений, именно их мы используем. Для начала попробуем вставить только один кирпичик:
1 2 3 4 5 6 |
bar = Actor("element_blue_rectangle_glossy.png") bar.x=120 bar.y=100 def draw(): bar.draw() |
Очевидно, что строить стенку по одному кирпичику за раз будет очень долго и муторно. Давайте используем цикл for, чтобы упростить задачу.
1 2 3 4 5 6 7 8 9 10 |
def draw(): bar_x = 120 bar_y = 100 for i in range(8): bar = Actor("element_blue_rectangle_glossy.png") bar.x = bar_x bar.y = bar_y bar.draw() bar_x += 70 |
Здесь мы создаем начальные переменные для координат x
и y
—bar_x
инициализируется на 120
, а bar_y
— на 100
.
Мы выполняем цикл 8 раз. Почему 8? Потому что именно столько кирпичиков мы можем удобно разместить на экране.
Для каждого цикла мы создаем объект Actor
, инициализируем его координаты x
и y
и рисуем все на экране. Затем делаем следующее:
1 |
bar_x += 70 |
Таким образом, каждый следующий кирпичик будет передвигаться на 70 пикселей влево. Опять же, число 70 было выбрано методом проб и ошибок. Можете попробовать изменить это значение и посмотреть, если кирпичики будут «налезать» друг на друга или когда они будут слишком далеко друг от друга.
Запустим код:
Неплохо. Теперь нужно разложить все остальные кирпичи. Я планирую построить 3 ряда, причем цвет каждого будет отличаться.
Первым делом я хочу превратить приведенный выше код в удобную функцию:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def draw(): screen.blit("background.png", (0,0)) paddle.draw() ball.draw() place_blue_bars() def place_blue_bars(): bar_x = 120 bar_y = 100 for i in range(8): bar = Actor("element_blue_rectangle_glossy.png") bar.x = bar_x bar.y = bar_y bar.draw() bar_x += 70 |
Здесь я поместил использованный раннее код в функцию place_blue_bars()
.
Я мог бы создать больше функций наподобие create_red_bars()
и так далее, но, думаю, будет лучше использовать более умный подход.
Итак, у нас будет общая функция place_bars()
:
1 |
def place_bars(x, y, image): |
В верхней части мы также создадим еще одну глобальную переменную bars_list = []
. Мы будем использовать ее для проверки того, какие кирпичики отображать, а какие удалять после попадания в них мячика.
Передадим начальные координаты x
и y
первого кирпичика, а также изображение, которое мы будем использовать. Конечная функция выглядит следующим образом:
1 2 3 4 5 6 7 8 9 |
def place_bars(x,y,image): bar_x = x bar_y = y for i in range(8): bar = Actor(image) bar.x = bar_x bar.y = bar_y bar_x += 70 bars_list.append(bar) |
Единственное изменение заключается в том, что мы инициализируем x
, y
и изображение из полученных входных данных.
Мы вызовем эту функцию до запуска основного кода игры, то есть перед pgzero.run()
:
1 2 3 |
coloured_box_list = ["element_blue_rectangle_glossy.png", "element_green_rectangle_glossy.png","element_red_rectangle_glossy.png"] x = 120 y = 100 |
У нас есть список с 3 изображениями. Мы инициализируем значения x
и y
. Затем делаем цикл for
по списку:
1 2 3 |
for coloured_box in coloured_box_list: place_bars(x, y, coloured_box) y += 50 |
Нам нужно сделать y += 50
в каждом цикле, иначе кирпичики будут располагаться друг над другом.
Окончательный код:
1 2 3 4 5 6 7 8 9 |
coloured_box_list = ["element_blue_rectangle_glossy.png", "element_green_rectangle_glossy.png", "element_red_rectangle_glossy.png"] x = 120 y = 100 for coloured_box in coloured_box_list: place_bars(x, y, coloured_box) y += 50 |
Нам нужно сделать еще кое-что. Создадим кирпичики, но отображать их не будем. Обновим функцию draw()
:
1 2 3 |
def draw() for bar in bars_list: bar.draw() |
Теперь у нас есть красивый макет. Наконец-то можно заняться логикой!
Добавление физики для двигающейся панели
Давайте начнем с добавления физики для нижней панели. Нам нужно, чтобы игрок мог двигать данный элемент туда-сюда, стараясь отбить мячик. В Pygame Zero это сделать очень просто — вы можете просто проверить события клавиатуры напрямую. Давайте обновим функцию update()
:
1 2 3 4 5 |
def update(): if keyboard.left: paddle.x = paddle.x - 5 if keyboard.right: paddle.x = paddle.x + 5 |
if keyboard.left
проверяет, нажата ли левая стрелка на клавиатуре, и если да, то изменяет x-позицию двигающейся панели на -5
(то есть перемещает ее влево на 5 пикселей). И то же самое происходит для правой стрелки на клавиатуре.
Почему я выбрал 5 пикселей? Чтобы найти баланс между слишком быстрым/медленным перемещением. Попробуйте изменить значения на 1 и 10 и посмотрите, что получится.
Нажмите на левую и правую стрелки — теперь вы можете перемещать нижнюю панель.
Добавление физики для движения мячика в игре
Помните, что встроенный метод update()
вызывается 60 раз в секунду. Поэтому любая игровая логика, например, перемещение мяча, проверка столкновений (коллизии) и тому подобное, будет происходить здесь.
Мы создадим новую функцию update_ball()
, которую будем вызывать из функции update()
.
1 2 3 4 5 6 |
def update(): update_ball() def update_ball(): ball.x -= 1 ball.y -= 1 |
Таким образом мы меняем позиции x
и y
для мяча.
Система координат в Pygame Zero
Координаты верхней левой части экрана — 0, 0
то есть x=0, y=0
.
Если вы двигаетесь вправо, то значение x
возрастает.
Если вниз, то возрастает значение y
.
При движении влево, уменьшается x
. При движении вправо, увеличивает x
.
Для передвижения вниз, увеличиваем y
. Вверх — уменьшаем y
.
Учитывая все это:
ball.x -= 1
двигает мяч влево (так как -1
= влево, +1
= вправо)
ball.y -= 1
двигает мяч вверх (так как -1
= вверх, +1
= вниз)
Итак, в начале игры мяч будет двигаться вверх и влево. Это просто случайное решение — вы можете с таким же успехом выбрать движение вниз и вправо. Но пока что я буду придерживаться этого решения, чтобы объяснить вам возникшую проблему:
Мяч за пределами экрана! Вот незадача!
Нам нужно добавить проверки, чтобы при ударе о стены мяч отскакивал назад. Это физическая часть.
Давайте добавим проверку.
Сначала добавим глобальную переменную для скорости x
и y
. Добавьте эти глобальные переменные в верхнюю часть файла:
1 2 |
ball_x_speed = 1 ball_y_speed = 1 |
Скорость равна 1 пикселю для движения влево/вправо и вверх/вниз. Вы можете попробовать увеличить это число, чтобы заставить мяч двигаться быстрее (и, следовательно, увеличить сложность игры), но мы будем придерживаться значения 1, так как это упрощает тестирование.
Давайте используем эту переменную в нашей функции:
1 2 3 4 |
def update_ball(): global ball_x_speed, ball_y_speed ball.x -= ball_x_speed ball.y -= ball_y_speed |
Код такой же, как и раньше, просто мы заменяем ‘1’ на переменную. Теперь добавим проверки.
1 2 |
if (ball.x >= WIDTH) or (ball.x <=0): ball_x_speed *= -1 |
Если x
превышает максимальное значение WIDTH
, которое мы определили для игры (то есть мяч выходит за правую часть экрана), или меньше 0 (то есть выходит за левую часть экрана), тогда делаем следующее:
1 |
ball_x_speed *= -1 |
Что это значит? Помните, что при каждом обновлении мы двигались со скоростью мяча. Сначала движения были вверх и влево.
Здесь мы умножаем скорость на -1
. Таким образом, если мяч двигался влево, он начнет двигаться вправо, и наоборот.
В результате, как только мяч достигнет границы, он изменит направление.
Мы можем сделать то же самое для оси y
:
1 2 |
if (ball.y >= HEIGHT) or (ball.y <=0): ball_y_speed *= -1 |
Снова проверяем, проходит ли мяч выше или ниже экрана. Конечная функция будет выглядеть следующим образом:
1 2 3 4 5 6 7 8 |
def update_ball(): global ball_x_speed, ball_y_speed ball.x -= ball_x_speed ball.y -= ball_y_speed if (ball.x >= WIDTH) or (ball.x <=0): ball_x_speed *= -1 if (ball.y >= HEIGHT) or (ball.y <=0): ball_y_speed *= -1 |
Протестируем код:
Клево, теперь мячик отскакивает от стенок. Однако он по-прежнему проходит сквозь стенку с кирпичиками. Давайте исправим это.
Коллизия (обнаружения столкновений) объектов в Pygame Zero
Для обнаружения столкновения добавим следующий код в функцию update()
:
1 2 3 4 5 |
def update(): update_ball() for bar in bars_list: if ball.colliderect(bar): bars_list.remove(bar) |
Давайте разберем каждую строчку кода:
Используем цикл для кирпичиков:
1 |
for bar in bars_list: |
И для каждого кирпича мы проверяем, столкнулся ли с ним мяч:
1 |
if ball.colliderect(bar): |
colliderect()
— это встроенная функция, которая проверяет, столкнулись ли два объекта. В данном случае — мяч и кирпич.
Если они столкнулись, мы удаляем кирпич из списка:
1 2 |
if ball.colliderect(bar): bars_list.remove(bar) |
Вы помните, что кирпичики создаются с помощью функции draw()
?
1 2 |
for bar in bars_list: bar.draw() |
Если мы удалим кирпич из списка, он больше не будет показываться и, следовательно, исчезнет с экрана.
Запустите код, чтобы проверить его работоспособность:
Окей, уже лучше. Вот только наш мяч проходит сквозь кирпичи, будто нож сквозь масло. Нам такого не нужно. Игра Арканоид устроена таким образом, что мячик должен отскакивать от кирпичей, когда касается их.
К счастью, нашу проблему можно решить очень легко:
1 2 3 4 |
for bar in bars_list: if ball.colliderect(bar): bars_list.remove(bar) ball_y_speed *= -1 # ==> новая строчка кода |
Последняя строка — это новый код. Мы изменяем направление мячика, так что если он поднимался вверх, то теперь начнет опускаться вниз.
Есть еще одна вещь, которую я бы хотел сделать. В оригинальной игре Арканоид, когда мячик ударялся о кирпичики или двигающуюся нижнюю панель, он мог лететь как влево, так и вправо, имитируя реальную «физику», похожую на физику пинбола. Да, это было не идеально, но это добавляло веселья в игру, так как вы не знали, куда полетит мяч.
Для этого я добавлю следующий код:
1 2 3 4 |
# случайное перемещение мяча влево или вправо после удара rand = random.randint(0,1) if rand: ball_move_x *= -1 |
Когда мяч попадает в какой-то блок, мы случайным образом, примерно в 50% случаев, меняем направление. Так, если мяч двигался вправо, он может начать двигаться влево.
Осталось сделать последний штрих.
Отскакивание мяча от двигающей панели в игре на Pygame Zero
Я просто поделюсь с вами кодом — думаю, вы поймете все сами:
1 2 3 4 5 6 |
if paddle.colliderect(ball): ball_y_speed *= -1 # randomly move ball left or right on hit rand = random.randint(0,1) if rand: ball_x_speed *= -1 |
Мы снова проверяем, столкнулся ли мяч с нижней панелью, и если да, меняем направление по оси y
. Мы также случайным образом меняем направление x
.
Теперь в вашем распоряжении есть простенькая, но рабочая игрушка:
Следующие шаги
Помните, что вы всегда можете скачать код с сайта https://github.com/shantnu/arkanoid-clone.
Чтобы проверить, насколько вы поняли код, попробуйте внести следующие изменения:
- В коде есть серьезный баг — если мячик падает ниже нижней панели, игра продолжается. По сути, вы никогда не можете проиграть!Вам нужно изменить логику таким образом, чтобы если мячик опускался ниже нижней двигающей панели, игра заканчивалась. Мы рассмотрим, как создать экран завершения игры в следующем примере, а пока просто переключайте игрока на консоль.
- Попробуйте добавить очки — каждый раз, когда игрок попадает в кирпич, он получает 1 очко. Опять же, просто выводите счет на консоль.Чтобы получить бонусные очки, для разноцветных кирпичей можно использовать разные баллы. Подсказка: вам нужно будет хранить кирпичи в другом списке, чтобы вы могли проверять количество очков в зависимости от того, какой цвет вы сбили.