article banner

Гра «Змійка», частина 1: Дай мені вікно у світ

Це фрагмент книги Python з нуля, яка допоможе вам навчитися програмуванню з нуля. Ви можете знайти його на Allegro, Empik та в інтернет-книгарнях.

У цьому розділі ми встановимо пакет PyGame і використаємо його для малювання елементів нашої гри.

Настав час застосувати те, чого ми навчилися, на практиці: ми напишемо гру "Змійка" (англ. Snake). Правила прості: гравець водить "змійку" по екрану. Його завдання — направляти її до кульок "їжі". Коли "змія" з’їдає "їжу", вона росте. Гравець програє, коли "змійка" стикається з власним хвостом.

Так виглядатиме наша гра.

Ти вже маєш більшість навичок, необхідних для написання такої гри. Єдине, чого Тобі не вистачає, це знання того, як відображати елементи перегляду та як прописати реакцію інтерфейсу на натискання кнопок гравцем. Решта — це практичне застосування того, що Тобі вдалося дізнатися з попередніх розділів.

Як працюють ігри?

Якщо спростити, графіка в іграх зводиться до того, що в області, де вона відображається, промальовуються зображення, подібно до фільмів, де сцени — це швидка зміна кадрів, такі собі "рухомі картинки". Щоб створити враження руху, малюнки відображаються послідовно на почергових етапах руху. До цього додається зсув. Наприклад, якщо персонаж рухається праворуч, кожне наступне його зображення має бути намальоване трохи правіше від попереднього. Також потрібно стерти попереднє зображення, щоб після нього не залишилося жодного сліду. Таким чином відповідна кількість зображень, які швидко відтворюються, створює ілюзію плавного руху персонажа.

Персонаж гри кадр за кадром.

У нашому випадку ми не займатимемося анімуванням персонажа, натомість відображатимемо "змійку" та її "їжу".

Встановлення PyGame

Встановлення пакетів ми розглядали у розділі Встановлення пакетів. Зараз нам потрібно встановити pygame. Якщо Ти користуєшся pip, введи в командному рядку:

python -m pip install -U pygame --user

У налаштуваннях PyCharm ми можемо запустити вікно керування пакетами і там за допомогою плюса додати pygame.

Керування встановленими пакетами в PyCharm. Щоб додати новий пакет, натисни плюс, і відкриється вікно пошуку пакетів.

Створімо вікно у світ

Тепер, коли у нас є пакет PyGame, створімо файл, у якому ми будемо ним користуватися. Ми можемо його назвати game.py. Імпорт PyGame виглядає, як більшість пакетів, це просто команда import pygame. Потім ми повинні викликати функцію init(), яка ініціалізує всі модулі пакета. Небагато пакетів мають подібні вимоги, але PyGame має багато функцій, тож деякі з них потрібно ініціалізувати.

# game.py
import pygame

pygame.init()

Наступним кроком є налаштування вікна, в якому буде відображатися наша гра. Для цього ми використовуємо функцію pygame.display.set_mode, якій ми передаємо тапл із розміром вікна. Вікно має висоту і ширину, тому тапл повинен мати два значення типу int. Якого розміру має бути це вікно? Припустімо, наше поле матиме розмір 20 на 20, а кожен фрагмент матиме розмір 25 1. Тому вікно має мати висоту і ширину 20 * 25. Оскільки ми малюємо квадрати, то їхню висоту і ширину будемо визначати однаковими координатами WIDTH.

CUBE_SIZE = 25
CUBES_NUM = 20
WIDTH = CUBE_SIZE * CUBES_NUM
screen = pygame.display.set_mode((WIDTH, WIDTH))

Постійні (тобто незмінні) значення, такі як CUBE_SIZECUBES_NUM або WIDTH, ми зазвичай записуємо великими літерами, згідно з конвенцією SCREAMING_SNAKE_CASE.

Наступний крок — установлення білого кольору вікна. Для цього ми використаємо функцію fill з посиланням на вікно. Як значення ми знову повинні передати тапл, у якому буде збережено колір за допомогою запису RGB.

RGB — це одна із основних систем позначень кольорів. Послідовні числа представляють значення червоного, зеленого та синього кольору (звідси назва: Red, Green, Blue). Базові кольори змішуються між собою, і в результаті утворюються інші барви. Щоб зрозуміти, як з них утворюються кольори, уяви три проєктори, які світять червоним, зеленим та синім. Там, куди не потрапляє жоден промінь, буде абсолютно темно. Там, де всі вони світять із максимальною потужністю, буде біле світло. Там, де перетинатимуться червоний і зелений, ми побачимо жовтий. Регулюючи інтенсивність усіх трьох проєкторів, ми можемо отримати практично будь-який колір.

Оскільки максимальне значення становить 255, білий колір створюється через (255, 255, 255).

WHITE = (255, 255, 255)
screen.fill(WHITE)

Додаймо до нашої гри кілька корисних кольорів:

BLUE = (0, 0, 255)
GREEN = (0, 255, 0)
WHITE = (255, 255, 255)

Після кожної зміни виду ми повинні викликати pygame.display.update(), щоб пакет знав, що потрібно відобразити зміну.

pygame.display.update()

Якби ми запустили гру зараз, вікно б з’явилося і негайно закрилося. Тобто наша програма закінчилася б і припинила роботу. Так працюють програми: коли ми підходимо до кінця сценарію, їхнє виконання припиняється. Як цьому протидіяти? Найпростіший спосіб — за допомогою циклу while. Для початку ми можемо використати нескінченний цикл:

while True:
    pass

Коли ми запустимо цю програму, то нарешті побачимо вікно з білим фоном. Тепер програма працюватиме, доки триває гра. На жаль, виникає протилежна проблема: коли хтось закриє вікно, програма далі працюватиме. Цю проблему можна вирішити різними способами. Часто цикли while ставлять у залежність від змінної, яка сигналізує, чи програма досі повинна працювати. Однак більш надійний метод — це виклик функції quit(), яка негайно завершує програму. Ми повинні викликати її, коли користувач просить закрити гру, а отже, у відповідь на певну подію. Ми ще поговоримо про події пізніше, а поки що достатньо зазначити, що в тіло нашого нескінченного циклу ми повинні додати такий код:

for event in pygame.event.get():
    if event.type == pygame.QUIT:
        quit()

Код на цьому етапі має виглядати так:

# game.py
import pygame

pygame.init()

CUBE_SIZE = 25
CUBES_NUM = 20
WIDTH = CUBE_SIZE * CUBES_NUM
screen = pygame.display.set_mode((WIDTH, WIDTH))
WHITE = (255, 255, 255)
screen.fill(WHITE)
pygame.display.update()

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            quit()

Коли ми його запустимо, то побачимо біле вікно. В ньому буде наша гра. Коли ми закриємо це вікно, наша програма також завершиться.

Це дуже гарний початок нашої пригоди. Тепер поговорімо про малювання "змійки" та її "їжі".

Квадрат

Щоб намалювати квадрат у PyGame, ми використовуємо pygame.draw.rect з аргументами:

  • посилання на вікно, тобто screen;
  • колір (тапл RGB);
  • тапли з координатами лівого верхнього кута та розміром квадрата.

Визначення координат точки.

Квадрати розміром 25 на 25 із координатами (25, 25) і (175, 75).

У нашому випадку квадрати будуть використані для малювання "змійки". Можемо використати зелений колір (0, 255, 0). Ми визначили його висоту і ширину як CUBE_SIZE. Що стосується розміщення, то фрагменти повинні перебувати на сітці розміром CUBES_NUM. Тому ми б віддали перевагу визначенню розташування фрагмента за координатами на цій сітці. Таке розташування можна визначати за двома параметрами x та y.

def draw_snake_part(x, y):
    position = (x * CUBE_SIZE,
                y * CUBE_SIZE,
                CUBE_SIZE,
                CUBE_SIZE)
    pygame.draw.rect(screen, GREEN, position)
    pygame.display.update()

Використаймо цю функцію, щоб намалювати кілька частин "змійки" в різних місцях:

draw_snake_part(0, 0)
draw_snake_part(13, 16)
draw_snake_part(10, 10)
draw_snake_part(12, 8)
draw_snake_part(19, 19)

Малювання кола

Малювання кола дуже схоже на малювання квадрата. Для цього ми використовуємо функцію pygame.draw.circle, до якої додаємо по черзі:

  • посилання на вікно, тобто у нас screen;
  • колір (тапл RGB);
  • положення центру кола (тапл з координатами);
  • розмір кола (радіус).

Оскільки ми хочемо, щоб наше коло ідеально вписувалося в квадрати, його радіус повинен дорівнювати рівно половині CUBE_SIZE. Оскільки ми визначаємо положення центру кола, його також слід змістити на половину CUBE_SIZE відносно положення, де починався би квадрат.

Cітка координат на полотні.

def draw_food(x, y):
    radius = float(CUBE_SIZE) / 2
    position = (x * CUBE_SIZE + radius,
                y * CUBE_SIZE + radius)
    pygame.draw.circle(screen, BLUE, position, radius)
    pygame.display.update()

Скористаймося цією функцією, щоб намалювати кружечки "їжі" в різних місцях нашого поля:

draw_food(0, 0)
draw_food(13, 16)
draw_food(10, 10)
draw_food(12, 8)
draw_food(19, 19)

Малювання "змійки"

Ми можемо подати "змійку" просто як список її елементів. Елементи повністю представлені їхнім розташуванням. Ми могли б створити список таплів, але нам зручніше визначити клас для представлення координат точки. Тут ми можемо згадати клас Position із розділу Оператори. У нашому прикладі ми будемо використовувати варіант, де він зберігає координати точки, а також дозволяє порівнювати значення і заміняти об’єкт на рядок. Порівняння точок знадобиться для перевірки, чи не наступили ми на хвіст чи "їжу". Їх виписування стане нам в нагоді під час роботи над грою. Додай наступний клас до свого проєкту в окремому файлі під назвою position.py.

# position.py
class Position:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return isinstance(other, Position) and \
               self.x == other.x and \
               self.y == other.y

    def __str__(self):
        return f"({self.x}, {self.y})"

    __repr__ = __str__

Не забудь імпортувати цей клас у файл гри:

from position import Position

Як координати ми будемо вводити номер квадрата по горизонтальній або вертикальній осі. Якщо розмір нашого поля 20, то позиція описується числами від 0 до 19. "Змійка" може виглядати, наприклад, так:

snake = [
    Position(2, 2),
    Position(3, 2),
    Position(4, 2),
    Position(5, 2),
    Position(5, 1),
]

Тепер відображення "змійки" — це просто відображення її послідовних частин. Для цього ми можемо використати цикл for:

def draw_snake(snake):
    for part in snake:
        draw_snake_part(part.x, part.y)

Місце, де міститься "їжа", також буде зручно представляти за допомогою класу Position:

food = Position(11, 14)

Тож я перероблю функції для малювання клітинок "змійки" та "їжі" так, щоб вони приймали Position як аргументи:

def draw_snake_part(pos):
    position = (pos.x * CUBE_SIZE,
                pos.y * CUBE_SIZE,
                CUBE_SIZE,
                CUBE_SIZE)
    pygame.draw.rect(screen, GREEN, position)
    pygame.display.update()
def draw_food(pos):
    radius = float(CUBE_SIZE) / 2
    position = (pos.x * CUBE_SIZE + radius,
                pos.y * CUBE_SIZE + radius)
    pygame.draw.circle(screen, BLUE, position, radius)
    pygame.display.update()


def draw_snake(snake):
    for part in snake:
        draw_snake_part(part)

Очищення поля

Щоб малювати на полі, нам потрібна ще тільки одна функція. Перш ніж малювати елементи на новому місці, потрібно стерти елементи, намальовані раніше. Без цього наше поле швидко заповниться застарілими зображеннями. Для очищення поля ми будемо використовувати вже відому нам функцію fill, яка заповнює все поле певним кольором.

screen.fill(WHITE)

Оптимізація

Наразі кожна функція, яка вносить зміни до вигляду екрана, викликає функцію pygame.display.update(), щоб показати зміну. Однак, коли стан нашої гри почне змінюватися, це може призвести до того, що деякі елементи будуть блимати 2. Це також не найефективніше рішення, оскільки ми перемальовуємо елементи набагато частіше, ніж потрібно. Було би набагато краще спочатку визначити всі зміни, а потім відобразити їх одночасно. Щоб зробити це, ми позбудемося update з окремих функцій, натомість введемо функцію draw, яка намалює всі цікаві для нас елементи.

def draw_snake_part(pos):
    position = (pos.x * CUBE_SIZE,
                pos.y * CUBE_SIZE,
                CUBE_SIZE,
                CUBE_SIZE)
    pygame.draw.rect(screen, GREEN, position)


def draw_snake(snake):
    for part in snake:
        draw_snake_part(part)


def draw_food(pos):
    radius = float(CUBE_SIZE) / 2
    position = (pos.x * CUBE_SIZE + radius,
                pos.y * CUBE_SIZE + radius)
    pygame.draw.circle(screen, BLUE, position, radius)


def fill_bg():
    screen.fill(WHITE)


def draw(snake, food):
    fill_bg()
    draw_snake(snake)
    draw_food(food)
    pygame.display.update()
1:

Одиницею вимірювання висоти та ширини в PyGame є пікселі. Звичайно, це небагато говорить про те, що таке 25. Найпростіше підбирати потрібні значення "на око".

2:

Швидко з’являтися та зникати.