Создание 2D игры на Python

от автора

*P.S.

Я всё ещё школьник, поэтому ждать красивого кода не стоит, но если тебе хочется погрузиться в мир видеоигр с обратной стороны, эта статья должна показаться тебе хоть немного интересной.

Этот подробный гайд по созданию простецкой игры на Python. Всё это делать мы будем конечно в пучарме «PyCharm». Его настройки в этой статье не будет, всё это уже давно есть ру(ю)тубе.

Подготовка к разработке: Установка Pygame и организация проекта

Прежде чем мы начнем, необходимо настроить окружение для разработки. Мы будем делать игру с помощью pygame.

  • Установка Pygame.

    • Проверьте установлен ли у вас Python.

    • Открываем терминал и пишем: pip install pygame

  • Структура проекта. Для удобства организации кода, создайте структуру папок и файлов:

    проект/     ├── img/           # Папка для изображений (спрайтов)     ├── sound/         # Папка для звуков     ├── config.py      # Файл с настройками игры     ├── objects.py     # Файл с классами игровых объектов     └── main.py        # Основной файл игры
  • Файл config.py – Сердце настроек. Создадим файл config.py и поместим туда основные настройки игры. Это упростит изменение параметров в будущем.

    # config.py from os import path  WIDTH = 480  # Ширина экрана HEIGHT = 600  # Высота экрана FPS = 60  # Частота кадров в секунду (FPS) # --- Цвета --- WHITE = (255, 255, 255) BLACK = (0, 0, 0) RED = (255, 0, 0) GREEN = (0, 255, 0) BLUE = (0, 0, 255) YELLOW = (255, 255, 0) # --- Пути к ресурсам --- img_dir = path.join(path.dirname(__file__), 'img')  # Путь к папке img sound_dir = path.join(path.dirname(__file__), 'sound')  # Путь к папке sound # --- Настройки Босса --- BOSS_LASER_SPEED = 5 BOSS_LASER_COLOR = RED
    • Перевожу со змеиного на человеческий

      • WIDTHHEIGHT: Определяют размеры окна игры.

      • from os import path: Импортируем модуль path. Мы будем иметь доступ к переменным из этого файла во всём проекте.

      • FPS: Определяет скорость обновления экрана. Более высокое значение – более плавная анимация, но требует больше ресурсов.

      • Цвета: Задаем цвета в формате RGB (Красный, Зеленый, Синий).

      • img_dirsound_dir: Эти переменные определяют пути к папкам с изображениями и звуками, что упрощает доступ к ресурсам в коде. path.join используется для корректного определения пути в зависимости от операционной системы.

Основные элементы игры: Спрайты и управление

Теперь перейдем к созданию игровых объектов и управлению ими. В Pygame все, что мы видим на экране (игрок, враги, пули), будет представлено спрайтами.

  • Класс Entity. Общие свойства задаются в классах Player и Mob. Иными словами, это общий “скелет” для всего.

    • Что нужно знать:

      • Спрайт – это изображение, которое можно перемещать по экрану.

      • pygame.sprite.Sprite – базовый класс для спрайтов в Pygame.

      • image: Атрибут, в котором хранится изображение спрайта.

      • rect: Атрибут, который представляет прямоугольник, окружающий изображение. Используется для определения положения спрайта и обнаружения коллизий.

      • xy: Координаты верхнего левого угла прямоугольника rect.

      • speed: Скорость перемещения объекта.

  • Класс Player (определение игрока). Класс Player (в objects.py) наследует (или содержит) общие свойства и добавляет специфичную для игрока логику:

    # objects.py import pygame from config import *  # Импортируем настройки из config.py  class Player(pygame.sprite.Sprite):  # Наследуемся от pygame.sprite.Sprite     def __init__(self, all_sprites, bullets):         pygame.sprite.Sprite.__init__(self)         player_img = pygame.image.load(path.join(img_dir, "player.png")).convert_alpha()         self.image = pygame.transform.scale(player_img, (50, 50))  # Масштабируем изображение         self.image.set_colorkey(BLACK)  # Убираем фон (делаем прозрачным)         self.rect = self.image.get_rect()  # Получаем прямоугольник спрайта         self.rect.centerx = WIDTH / 2  # Начальная позиция по x (по центру)         self.rect.bottom = HEIGHT - 10  # Начальная позиция по y (внизу)         self.speedx = 0  # Горизонтальная скорость (изначально 0)         self.all_sprites = all_sprites         self.bullets = bullets      def update(self):  # Метод, вызываемый каждый кадр         self.speedx = 0  # Сбрасываем скорость         keystate = pygame.key.get_pressed()  # Получаем состояние всех клавиш         if keystate[pygame.K_LEFT]:             self.speedx = -8  # Двигаемся влево, если нажата клавиша влево         if keystate[pygame.K_RIGHT]:             self.speedx = 8  # Двигаемся вправо         self.rect.x += self.speedx  # Обновляем позицию         if self.rect.right > WIDTH:  # Не даем вылезти за правую границу             self.rect.right = WIDTH         if self.rect.left < 0:  # Не даем вылезти за левую границу             self.rect.left = 0      def shoot(self):  # Метод для выстрела         bullet = Bullet(self.rect.centerx, self.rect.top)  # Создаем пулю         self.all_sprites.add(bullet)  # Добавляем пулю в общую группу         self.bullets.add(bullet)  # Добавляем пулю в группу пуль         laser_sound.play()  # Проигрываем звук выстрела
    • Что делает код?

      • init(): Конструктор класса. Загружает изображение игрока, масштабирует его, убирает фон (делает прозрачным), устанавливает начальную позицию и скорость.

      • update(): Метод, вызываемый каждый кадр для обновления состояния игрока:

        • Сбрасывает горизонтальную скорость (чтобы игрок останавливался, когда клавиши не нажаты).

        • Получает состояние клавиш.

        • Устанавливает скорость в зависимости от нажатых клавиш (влево/вправо).

        • Обновляет позицию игрока.

        • Ограничивает движение игрока по краям экрана.

      • shoot(): Создает пулю и добавляет ее в группы спрайтов.

  • Обработка событий Pygame (игровой цикл). Теперь, в main.py, необходимо создать основной игровой цикл и обрабатывать события.

    # main.py import pygame from config import * from objects import *  # --- Инициализация --- pygame.init() pygame.mixer.init()  # Инициализация звука screen = pygame.display.set_mode((WIDTH, HEIGHT))  # Создаем окно игры pygame.display.set_caption("Space Shooter")  # Устанавливаем заголовок окна clock = pygame.time.Clock()  # Создаем объект для управления FPS  # --- Загрузка изображений --- background = pygame.image.load(path.join(img_dir, "starfield.png")).convert_alpha() background_rect = background.get_rect()  # Получаем прямоугольник фона  # --- Создание групп спрайтов --- all_sprites = pygame.sprite.Group()  # Группа для всех спрайтов mobs = pygame.sprite.Group()  # Группа для врагов bullets = pygame.sprite.Group()  # Группа для пуль boss_lasers = pygame.sprite.Group()  # Группа лазеров босса  # --- Создание объектов --- player = Player(all_sprites, bullets)  # Создаем игрока all_sprites.add(player)  # Добавляем игрока в общую группу  # --- Загрузка музыки и звуков --- pygame.mixer.music.load(path.join(sound_dir, 'Star_Wars_-_Rogue_One_-_OST_Izgojj-Odin_Zvjozdnye_Vojjny_65069199.mp3'))  # Загружаем музыку pygame.mixer.music.play(-1)  # Проигрываем музыку бесконечно laser_sound = pygame.mixer.Sound(path.join(sound_dir, 'blaster.mp3'))  # Звук выстрела  # --- Состояние игры --- game_over = True  # Изначально игра не запущена  # --- Стартовый экран (пока не используем) --- # start_screen() # Перенесем эту функцию позже  # --- Основной игровой цикл --- def start_screen():     # ... (реализация стартового экрана, как в коде) ...     pass  # заглушка для начала, чтобы игра запустилась  def reset_game(self):     # ... (сброс состояния игры, создание новых объектов) ...     pass  # заглушка для начала  def run():  # Функция запуска игрового цикла     nonlocal game_over     while True:         if game_over:             if not start_screen():  # Отображаем стартовый экран, возвращаем False при выходе                 return              continue          # --- Ограничение FPS ---         clock.tick(FPS)          # --- Обработка событий (ввод, выход) ---         for event in pygame.event.get():             if event.type == pygame.QUIT:  # Если нажали на крестик                 pygame.quit()  # Выход из Pygame                 return  # Завершаем функцию run()             elif event.type == pygame.KEYDOWN:  # Нажата клавиша                 if event.key == pygame.K_SPACE:  # Если пробел                     player.shoot()                 elif event.key == pygame.K_ESCAPE:                     pygame.quit()                     return  # Завершаем игру          # --- Обновление спрайтов ---         all_sprites.update()  # Вызываем update() для всех спрайтов          # --- Отрисовка ---         screen.fill(BLACK)  # Заполняем экран черным цветом         screen.blit(background, background_rect)  # Рисуем фон         all_sprites.draw(screen)  # Рисуем все спрайты          # --- Обновление экрана ---         pygame.display.flip()  # Переворачиваем буфер экрана, чтобы отобразить изменения  def main():     run()  if __name__ == "__main__":     main()
    • Что делает код?

      • Инициализирует Pygame.

      • Создает окно игры и устанавливает заголовок.

      • Создает объект для управления FPS (clock).

      • Загружает фоновое изображение.

      • Создает группы спрайтов:

        • all_sprites: Для всех спрайтов (игрок, враги, пули).

        • mobs: Для врагов.

        • bullets: Для пуль.

        • boss_lasers: Для лазеров босса.

      • Создает объект игрока.

      • Загружает музыку и звук выстрела.

      • Входит в основной игровой цикл run().

      • В игровом цикле:

        • clock.tick(FPS): Ограничивает частоту кадров.

        • pygame.event.get(): Получает все события (действия пользователя, такие как нажатия клавиш, закрытие окна).

        • for event in ...: Обрабатывает события.

          • pygame.QUIT: Если пользователь закрывает окно, игра завершается.

          • pygame.KEYDOWN: Если нажата клавиша:

            • K_SPACE: Игрок стреляет.

            • K_ESCAPE: Игрок выходит из игры.

        • all_sprites.update(): Вызывает метод update() для каждого спрайта в группе. Это обновляет положение и состояние спрайтов (например, движение игрока, движение пуль).

        • Отрисовка:

          • screen.fill(BLACK): Заливает экран черным цветом.

          • screen.blit(background, background_rect): Отображает фон.

          • all_sprites.draw(screen): Отрисовывает все спрайты в группе all_sprites. Pygame автоматически использует атрибут image спрайтов и их rect для отрисовки.

          • pygame.display.flip(): Обновляет (переворачивает) экран, чтобы отобразить все изменения.

Враги и коллизии

Теперь добавим врагов, чтобы сделать игру более интересной, и реализуем систему коллизий, чтобы определить, когда пули попадают во врагов, и когда игрок сталкивается с врагами.

  • Класс Mob (создание врагов). Создадим класс Mob (в objects.py) для представления врагов. Враги будут появляться сверху и двигаться вниз.

    # objects.py import pygame import random from config import *  class Mob(pygame.sprite.Sprite):     def __init__(self):         pygame.sprite.Sprite.__init__(self)         meteor_img = pygame.image.load(path.join(img_dir, "Ship.png")).convert_alpha()  # Загружаем изображение врага         self.image = meteor_img #.convert() # Альтернатива convert_alpha()         self.image.set_colorkey(BLACK)         self.rect = self.image.get_rect()  # Получаем прямоугольник врага         self.rect.x = random.randrange(WIDTH - self.rect.width)  # Случайная начальная позиция по x         self.rect.y = random.randrange(-100, -40)  # Начальная позиция по y (за пределами экрана)         self.speedy = random.randrange(1, 8)  # Случайная скорость падения         self.speedx = random.randrange(-3, 3)  # Случайная скорость по x      def update(self):  # Метод для обновления позиции врага         self.rect.x += self.speedx # Двигаем по x         self.rect.y += self.speedy  # Двигаем вниз         if self.rect.top > HEIGHT + 10 or self.rect.left < -25 or self.rect.right > WIDTH + 20:             # Если враг ушел за пределы экрана, перезапускаем его             self.rect.x = random.randrange(WIDTH - self.rect.width)             self.rect.y = random.randrange(-100, -40)             self.speedy = random.randrange(1, 8)
    • Что делает код?

      • init(): Загружает изображение врага, устанавливает случайную начальную позицию и скорость падения.

      • update(): Перемещает врага вниз, а также, если враг выходит за пределы экрана, переносит его обратно наверх.

  • Класс Bullet (создание пуль). Этот класс уже был представлен выше.

  • Обнаружение коллизий и логика уничтожения. Теперь добавим логику обнаружения коллизий в основной цикл игры (main.py):

    # main.py (внутри цикла run()) # ... (код игрового цикла) ...  # Проверка столкновений пуль с врагами (пули уничтожают врагов) hits = pygame.sprite.groupcollide(self.mobs, self.bullets, True, True)  # Проверяем столкновения. True, True = удаляем и врага и пулю for hit in hits:     m = Mob()  # Создаем нового врага     self.all_sprites.add(m)  # Добавляем его в общую группу     self.mobs.add(m)  # Добавляем его в группу врагов     self.score_counter.add(1)  # Увеличиваем счет  # Проверка столкновения игрока с врагами (проигрыш) hits = pygame.sprite.spritecollide(self.player, self.mobs, False)  # Проверяем столкновения игрока с врагами. False = враг не удаляется if hits:     self.game_over = True  # Если есть столкновение, устанавливаем флаг game_over     # Дополнительно: можно уменьшать здоровье игрока, а не сразу завершать игру  # ... (остальной код игрового цикла) ...
    • Что делает код?

      • pygame.sprite.groupcollide(self.mobs, self.bullets, True, True): Проверяет столкновения между группой врагов (self.mobs) и группой пуль (self.bullets). Если пуля попадает во врага, и пуля, и враг удаляются из своих групп (параметры True, True). После каждого столкновения создается новый враг и увеличивается счет.

      • pygame.sprite.spritecollide(self.player, self.mobs, False): Проверяет столкновения между игроком и врагами. Если происходит столкновение, устанавливается флаг self.game_over = True, что означает конец игры.

4. Босс: Создание финального противника

После набора определенного количества очков появляется босс.

  • Класс Boss (создание босса). Создадим класс Boss (в objects.py).

    # objects.py import pygame import random from config import *  class Boss(pygame.sprite.Sprite):     def __init__(self, x, y, all_sprites, boss_lasers):         pygame.sprite.Sprite.__init__(self)         boss_image = pygame.image.load(path.join(img_dir, "Boss.png")).convert_alpha()  # Загрузка изображения босса         self.image = pygame.transform.scale(boss_image, (90, 150))  # Масштабирование         self.image.set_colorkey(BLACK)         self.rect = self.image.get_rect()  # Получаем прямоугольник босса         self.rect.x = x  # Позиция по X         self.rect.y = y  # Позиция по Y         print("Boss created at:", self.rect.x, self.rect.y)         self.health = 50  # Здоровье босса         self.original_image = self.image  # Сохраняем оригинальное изображение (для анимации)         self.last_shot = pygame.time.get_ticks()  # Время последнего выстрела         self.shoot_delay = 2000  # Задержка между выстрелами (в миллисекундах)         self.all_sprites = all_sprites  # Группа всех спрайтов         self.boss_lasers = boss_lasers  # Группа лазеров босса      def update(self):         # Двигаем босса (просто для примера, можно сделать сложнее)         self.rect.x += random.choice([-2, -1, 0, 1, 2])  # Случайное движение по горизонтали         if self.rect.left < 0:             self.rect.left = 0  # Ограничиваем движение слева         if self.rect.right > WIDTH:             self.rect.right = WIDTH  # Ограничиваем движение справа          # Стреляем         now = pygame.time.get_ticks()  # Получаем текущее время         if now - self.last_shot > self.shoot_delay:  # Если прошло достаточно времени с последнего выстрела             self.last_shot = now  # Обновляем время последнего выстрела             laser = BossLaser(self.rect.centerx, self.rect.bottom)  # Создаем лазер             self.all_sprites.add(laser)  # Добавляем лазер в общую группу спрайтов             self.boss_lasers.add(laser)  # Добавляем лазер в группу лазеров босса      def draw(self, screen):         screen.blit(self.image, self.rect)      def damage(self, amount):         self.health -= amount         if self.health <= 0:             self.kill()  # Если здоровье <= 0, уничтожаем босса
    • Что делает код?

      • init(): Загружает изображение босса, масштабирует, устанавливает начальную позицию, здоровье, и другие параметры.

      • update(): Отвечает за перемещение босса и стрельбу лазерами.

      • draw(): Рисует изображение босса на экране.

      • damage(): Уменьшает здоровье босса. При достижении 0, босс удаляется.

  • Класс BossLaser (лазеры босса).

    # objects.py import pygame from config import *  class BossLaser(pygame.sprite.Sprite):     def __init__(self, x, y):         pygame.sprite.Sprite.__init__(self)         self.image = pygame.Surface((5, 20))  # Создаем поверхность для лазера (прямоугольник)         self.image.fill(BOSS_LASER_COLOR)  # Заливаем поверхность цветом лазера         self.image = self.image.convert_alpha() # Прозрачность         self.rect = self.image.get_rect()  # Получаем прямоугольник лазера         self.rect.centerx = x  # Центрируем лазер по x         self.rect.bottom = y  # Располагаем лазер под боссом         self.speedy = BOSS_LASER_SPEED  # Устанавливаем скорость падения      def update(self):         self.rect.y += self.speedy  # Двигаем лазер вниз         if self.rect.bottom > HEIGHT:             self.kill()  # Удаляем лазер, если он вышел за пределы экрана
  • Добавление босса в игру (main.py). Логика появления босса и столкновения с ним:

    # main.py (внутри run()) # ... (код игрового цикла) ...  # Проверка на появление босса if self.score_counter.get_score() % 100 == 0 and self.score_counter.get_score()!=0 and self.boss is None:     self.boss = Boss(WIDTH // 2 - 50, 50, self.all_sprites, self.boss_lasers)  # Создаем босса     self.all_sprites.add(self.boss)  # Добавляем босса в группу спрайтов  # Проверка столкновений пуль с боссом if self.boss:     for bullet in self.bullets:         if pygame.sprite.collide_rect(bullet, self.boss):  # Проверяем столкновение             self.boss.damage(3)  # Уменьшаем здоровье босса             self.bullets.remove(bullet)  # Удаляем пулю             self.all_sprites.remove(bullet)             break  # Важно: только одно попадание за раз      if self.boss.health <= 0:         self.boss.kill()  # Если здоровье босса меньше или равно 0, уничтожаем его         self.boss = None  # Проверка столкновения игрока с лазерами босса (проигрыш) hits = pygame.sprite.spritecollide(self.player, self.boss_lasers, True)  # True - лазеры удаляются if hits:     self.game_over = True  # Конец игры
    • Что делает код?

      • Проверяет, набрал ли игрок достаточно очков (self.score_counter.get_score() % 100 == 0 and self.score_counter.get_score()!=0) и отсутствует ли босс на экране (self.boss is None).

      • Если условия выполняются, создается новый экземпляр Boss.

      • Проверяет, произошло ли столкновение пуль с боссом. Если да, то босс получает урон, а пуля удаляется.

      • Проверяет, уничтожен ли босс (здоровье <= 0). Если да, то босс удаляется.

      • Проверяет столкновения игрока с лазерами босса. Если игрок столкнулся с лазером, игра заканчивается.

Уровни и игровой процесс: Организация игры (Упрощено)

В данной реализации используется простая структура уровней — игра переходит в game_over после коллизии игрока с мобов/пулей босса, а после отображается стартовый экран.

  • Класс ScoreCount (добавлено в objects.py). Класс, отображающий очки.

    # objects.py import pygame from config import *  class ScoreCount:     def __init__(self, initial_score=0):         self.score = initial_score         self._score = initial_score         self.font = pygame.font.Font(None, 36)  # Шрифт и размер         self.update_score_surface()      def add(self, points):         self.score += points         self.update_score_surface()      def get_score(self):         return self.score      def update_score_surface(self):         self.score_surface = self.font.render(f"Score: {int(self.score)}", True, (255, 255, 255))      def reset(self):         self.score = 0      def draw(self, screen, x=10, y=10):         if self.score_surface:             screen.blit(self.score_surface, (x, y))
  • Отображение счета в игре (main.py). Необходимо создать объект ScoreCount и отрисовывать его.

    # main.py # ... (внутри класса Application.__init__) ... self.score_counter = ScoreCount()  # ... (внутри игрового цикла run()) ... self.score_counter.draw(self.screen)

6. Завершение игры: Состояние игры и стартовый экран (main.py)

  • Состояние игры: Переменная self.game_over отслеживает состояние игры.

    # main.py (внутри класса Application) def start_screen(self):     start_image = pygame.image.load(path.join(img_dir, "start.png")).convert_alpha()     start_rect = start_image.get_rect(center=(WIDTH // 2, HEIGHT // 2))      font_name = pygame.font.match_font('arial')     font = pygame.font.Font(font_name, 30)      line1 = "Нажмите на любую кнопку,"     line2 = "чтобы начать игру, или Esc, чтобы выйти"      text_surface1 = font.render(line1, True, WHITE)     text_surface2 = font.render(line2, True, WHITE)      text_rect1 = text_surface1.get_rect(center=(WIDTH // 2, HEIGHT // 2 - 20))     text_rect2 = text_surface2.get_rect(center=(WIDTH // 2, HEIGHT // 2 + 20))      running = True     while running:         self.clock.tick(FPS)         for event in pygame.event.get():             if event.type == pygame.QUIT:                 pygame.quit()                 return False  # Возврат False для выхода из игры             if event.type == pygame.KEYDOWN:                 if event.key == pygame.K_ESCAPE:                     pygame.quit()                     return False  # Выход из игры                 else:                     running = False  # Нажата другая кнопка -> начинаем игру          self.screen.blit(start_image, start_rect)         self.screen.blit(text_surface1, text_rect1)         self.screen.blit(text_surface2, text_rect2)         pygame.display.flip()      self.game_over = False  # Игра началась     self.reset_game()  # Сброс параметров
  • Что делает код?

    • Отображает изображение стартового экрана.

    • Отображает текст.

    • Ожидает нажатия клавиши (любой, кроме ESC).

    • Если нажата клавиша — self.game_over = False, игра начинается.

    • Функция возвращает False, если был произведен выход из игры.

Итоги

  • Вот что получилось


ссылка на оригинал статьи https://habr.com/ru/articles/917276/