Pull to refresh

Пишем платформер на Python. Часть 2. Подчасть 1, подготовка к созданию редактора уровней

Reading time9 min
Views64K

Привет, друзья!

Продолжаем разбираться с нашим МариоБоем. Начало тут, продолжение тут. В этой подчасти второй части мы сделаем приготовление для создания редактора уровней, а именно: добавим турбо режим бега герою, смертельно опасные платформы, движущиеся монстры, телепортеры, принцессу и парсер уровней, дабы во второй подчасти не отвлекаться на всё это.

Upgrade героя


Добавим нашему герою возможность ускоряться. Для этого немного изменим код метода update.

Для начала, добавим констант
MOVE_EXTRA_SPEED = 2.5 # Ускорение
JUMP_EXTRA_POWER = 1 # дополнительная сила прыжка
ANIMATION_SUPER_SPEED_DELAY = 0.05 # скорость смены кадров при ускорении


Далее, добавим анимации движения влево — вправо в ускоренном режиме. Мы вставим те же картинки, но с другой скоростью смены кадров
#        Анимация движения вправо
        boltAnim = []
        boltAnimSuperSpeed = []
        for anim in ANIMATION_RIGHT:
            boltAnim.append((anim, ANIMATION_DELAY))
            boltAnimSuperSpeed.append((anim, ANIMATION_SUPER_SPEED_DELAY))
        self.boltAnimRight = pyganim.PygAnimation(boltAnim)
        self.boltAnimRight.play()
        self.boltAnimRightSuperSpeed = pyganim.PygAnimation(boltAnimSuperSpeed)
        self.boltAnimRightSuperSpeed.play()
#        Анимация движения влево        
        boltAnim = []
        boltAnimSuperSpeed = [] 
        for anim in ANIMATION_LEFT:
            boltAnim.append((anim, ANIMATION_DELAY))
            boltAnimSuperSpeed.append((anim, ANIMATION_SUPER_SPEED_DELAY))
        self.boltAnimLeft = pyganim.PygAnimation(boltAnim)
        self.boltAnimLeft.play()
        self.boltAnimLeftSuperSpeed = pyganim.PygAnimation(boltAnimSuperSpeed)
        self.boltAnimLeftSuperSpeed.play()

Добавили 2 набора анимаций при ускорении self.boltAnimRightSuperSpeed , self.boltAnimLeftSuperSpeed , отображать будем их чуть ниже

Теперь займемся самим методом update

Добавим входной параметр running
def update(self, left, right, up, running, platforms):


Изменим обработку движений персонажа, добавив поведение при ускорении.
if up:
      if self.onGround: # прыгаем, только когда можем оттолкнуться от земли
          self.yvel = -JUMP_POWER
          if running and (left or right): # если есть ускорение и мы движемся
                 self.yvel -= JUMP_EXTRA_POWER # то прыгаем выше
          self.image.fill(Color(COLOR))
          self.boltAnimJump.blit(self.image, (0, 0))
                       
if left:
      self.xvel = -MOVE_SPEED # Лево = x- n
      self.image.fill(Color(COLOR))
      if running: # если ускорение
            self.xvel-=MOVE_EXTRA_SPEED # то передвигаемся быстрее
            if not up: # и если не прыгаем
                self.boltAnimLeftSuperSpeed.blit(self.image, (0, 0)) # то отображаем быструю анимацию
        else: # если не бежим
            if not up: # и не прыгаем
                self.boltAnimLeft.blit(self.image, (0, 0)) # отображаем анимацию движения 
        if up: # если же прыгаем
                  self.boltAnimJumpLeft.blit(self.image, (0, 0)) # отображаем анимацию прыжка
 
if right:
         self.xvel = MOVE_SPEED # Право = x + n
         self.image.fill(Color(COLOR))
         if running:
             self.xvel+=MOVE_EXTRA_SPEED
             if not up:
                 self.boltAnimRightSuperSpeed.blit(self.image, (0, 0))
         else:
             if not up:
                 self.boltAnimRight.blit(self.image, (0, 0)) 
         if up:
                 self.boltAnimJumpRight.blit(self.image, (0, 0))
 


И в основном файле добавим обработку события нажатия левого шифта.
running = False
***
if e.type == KEYDOWN and e.key == K_LSHIFT:
           running = True
***
 if e.type == KEYUP and e.key == K_LSHIFT:
           running = False

Все коды клавиш тут

И не забываем добавить аргументы при вызове метода hero.update()
hero.update(left, right, up, running, platforms) 


Смотрим результаты ( я изменил цвет фона на черный, брутальный цвет для брутального МариоБоя)
Без ускорения

Прыжок с ускорением


Смертельные шипы


Настоящему герою — настоящая опасность. Создадим новый вид блоков, при соприкосновении с которыми будет происходить моментальная смерть.

Создаем класс, наследующийся от Platform.
class BlockDie(Platform):
    def __init__(self, x, y):
        Platform.__init__(self, x, y)
        self.image = image.load("%s/blocks/dieBlock.png" % ICON_DIR)


Далее, добавим поведение героя при соприкосновении с ним. Для этого, добавим 2 метода в класс персонажа. Первый метод — поведение при смерти, второй — перемещение по указанным координатам(который пригодится нам еще раз чуть ниже)
def die(self):
        time.wait(500)
        self.teleporting(self.startX, self.startY) # перемещаемся в начальные координаты

def teleporting(self, goX, goY):
        self.rect.x = goX
        self.rect.y = goY

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

Ну и описываем само поведение при пересечении с блоком смерти в методе collide()
***
if isinstance(p, blocks.BlockDie): # если пересакаемый блок - blocks.BlockDie
     self.die()# умираем
***


Теперь, в основном классе изменим уровень
level = [
       "----------------------------------",
       "-                                -",
       "-                       --       -",
       "-        *                       -",
       "-                                -",
       "-            --                  -",
       "--                               -",
       "-                                -",
       "-                   ----     --- -",
       "-                                -",
       "--                               -",
       "-            *                   -",
       "-                            --- -",
       "-                                -",
       "-                                -",
       "-  *   ---                  *    -",
       "-                                -",
       "-   -------         ----         -",
       "-                                -",
       "-                         -      -",
       "-                            --  -",
       "-           ***                  -",
       "-                                -",
       "----------------------------------"]


И добавим создание блока смерти, если в уровне есть символ "*"
if col == "*":
   bd = BlockDie(x,y)
   entities.add(bd)
   platforms.append(bd)

Результат:


Порталы


Какой современный сантехник обходится без телепорта? Так давайте и нашего героя не будем делать белой вороной.

Создаём новый тип блока. Работаем в файле blocks.py

Cперва добавляем константы
ANIMATION_BLOCKTELEPORT = [
            ('%s/blocks/portal2.png' % ICON_DIR),
            ('%s/blocks/portal1.png' % ICON_DIR)]

Затем создаем новый класс.
class BlockTeleport(Platform):
    def __init__(self, x, y, goX,goY):
        Platform.__init__(self, x, y)
        self.goX = goX # координаты назначения перемещения
        self.goY = goY # координаты назначения перемещения
        boltAnim = []
        for anim in ANIMATION_BLOCKTELEPORT:
            boltAnim.append((anim, 0.3))
        self.boltAnim = pyganim.PygAnimation(boltAnim)
        self.boltAnim.play()
        
    def update(self):
        self.image.fill(Color(PLATFORM_COLOR))
        self.boltAnim.blit(self.image, (0, 0))

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

Далее, добавим нашему герою поведение при соприкосновении с порталом
***
elif isinstance(p, blocks.BlockTeleport):
	self.teleporting(p.goX, p.goY)
***

И добавим один портал на карту. Только теперь будем описывать координаты вручную. Когда сделаем редактор уровней — будет легче.
Добавим еще одну группу спрайтов, которая будет содержать анимированные блоки
animatedEntities = pygame.sprite.Group() # все анимированные объекты, за исключением героя

И создаем телепортер.
tp = BlockTeleport(128,512,800,64)
entities.add(tp)
platforms.append(tp)
animatedEntities.add(tp)

В конце, добавим вызов метода update() у всех анимированных спрайтов
animatedEntities.update() # показываем анимацию 


Как-то так


Монстры


Страшные, передвигающиеся, смертельно опасные огоньки.

Отличие монстров от смертельных блоков в том, что монстры могут двигаться.

Начнем, пожалуй.

Будем работать в новом файле, дабы не запутаться. Назовем его очень оригинально — monsters.py

Создадим новый класс Monster. В нём нет ничего такого, чего мы не применяли ранее.
Содержимое всего файла
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pygame import *
import pyganim
import os

MONSTER_WIDTH = 32
MONSTER_HEIGHT = 32
MONSTER_COLOR = "#2110FF"
ICON_DIR = os.path.dirname(__file__) #  Полный путь к каталогу с файлами


ANIMATION_MONSTERHORYSONTAL = [('%s/monsters/fire1.png' % ICON_DIR),
                      ('%s/monsters/fire2.png' % ICON_DIR )]

class Monster(sprite.Sprite):
    def __init__(self, x, y, left, up, maxLengthLeft,maxLengthUp):
        sprite.Sprite.__init__(self)
        self.image = Surface((MONSTER_WIDTH, MONSTER_HEIGHT))
        self.image.fill(Color(MONSTER_COLOR))
        self.rect = Rect(x, y, MONSTER_WIDTH, MONSTER_HEIGHT)
        self.image.set_colorkey(Color(MONSTER_COLOR))
        self.startX = x # начальные координаты
        self.startY = y
        self.maxLengthLeft = maxLengthLeft # максимальное расстояние, которое может пройти в одну сторону
        self.maxLengthUp= maxLengthUp # максимальное расстояние, которое может пройти в одну сторону, вертикаль
        self.xvel = left # cкорость передвижения по горизонтали, 0 - стоит на месте
        self.yvel = up # скорость движения по вертикали, 0 - не двигается
        boltAnim = []
        for anim in ANIMATION_MONSTERHORYSONTAL:
            boltAnim.append((anim, 0.3))
        self.boltAnim = pyganim.PygAnimation(boltAnim)
        self.boltAnim.play()
         
    def update(self, platforms): # по принципу героя
                    
        self.image.fill(Color(MONSTER_COLOR))
        self.boltAnim.blit(self.image, (0, 0))
       
        self.rect.y += self.yvel
        self.rect.x += self.xvel
 
        self.collide(platforms)
        
        if (abs(self.startX - self.rect.x) > self.maxLengthLeft):
            self.xvel =-self.xvel  # если прошли максимальное растояние, то идеи в обратную сторону
        if (abs(self.startY - self.rect.y) > self.maxLengthUp):
            self.yvel = -self.yvel # если прошли максимальное растояние, то идеи в обратную сторону, вертикаль

    def collide(self, platforms):
        for p in platforms:
            if sprite.collide_rect(self, p) and self != p: # если с чем-то или кем-то столкнулись
               self.xvel = - self.xvel # то поворачиваем в обратную сторону
               self.yvel = - self.yvel

При создании монстра необходимо указать 6 аргументов: х, y — координаты, left — скорость перемещения по горизонтали, up — скорость перемещения по вертикали, maxLengthLeft — максимальное расстояние в одну сторону, которое может пройти монстр, maxLengthUp — аналогично предыдущему, но по вертикали.

Теперь добавим смерть герою от соприкосновения с огнем.

Заменим строки
if isinstance(p, blocks.BlockDie): # если пересакаемый блок - blocks.BlockDie
           self.die()# умираем

На
 if isinstance(p, blocks.BlockDie) or isinstance(p, monsters.Monster): # если пересакаемый блок- blocks.BlockDie или Monster
            self.die()# умираем

И не забываем добавить импорт с файла monsters.py

И, конечно же, добавим создание монстра в основной файл.

Создадим еще одну группу спрайтов, в которую будем помещать наших монстриков.
monsters = pygame.sprite.Group() # Все передвигающиеся объекты

Вопрос: Для чего нам еще одна группа? Почему не хватило предыдущей? Ведь в группе спрайтов animatedEntities мы вызываем метод update()
Ответ: В предыдущей группе мы вызываем метод update()без аргументов, а в группе monsters этот метод будет вызывать с аргументом.

Создаем самого монстра.
mn = Monster(190,200,2,3,150,15)
entities.add(mn)
platforms.append(mn)
monsters.add(mn)

И двигаем его
monsters.update(platforms) # передвигаем всех монстров

Смотрим на результат.


Принцесса


Дело чести любого сантехника — спасти принцессу.

Класс принцессы не содержит что-либо нам интересное, поэтому код его показывать не буду. Кто заинтересуется — искать в файле blocks.py

Нашему персонажу добавим свойство winner, по которому будем судить, что пора завершать уровень.
self.winner = False

И внесем изменения в метод collide()
elif isinstance(p, blocks.Princess): # если коснулись принцессы
      self.winner = True # победили!!!


И далее, напишем код создания принцессы
if col == "P":
   pr = Princess(x,y)
   entities.add(pr)
   platforms.append(pr)
   animatedEntities.add(pr)


Не забыв вставить символ «P» в уровень.

Смотрим


Уровень


Наконец-то мы добрались до парсинга уровня. Их мы будем держать в каталоге levels. Привожу пример уровня из файла 1.txt
[
----------------------------------|
-               *                -|
-             *          *P      -|
----                    *--**   --|
-            --                  -|
-                                -|
-                                -|
-                                -|
--                   ----         |
-                                -|
--                               -|
-       **                       -|
-                            --- -|
-                                -|
-                                -|
-      ---                       -|
-                                -|
-   --------  *     ----         -|
-                                -|
-                         -      -|
-      **                    --  -|
-      *                         -|
-     **                         -|
---------------   ***        --  -|
-                                -|
-                                -|
----------------------------------|
]

player 55 44 
portal 128 512 900 35
portal 170 512 700 64
monster 190 250 2 1 150 10
monster 190 400 2 3 150 150
monster 150 200 1 2 150 100

/


Что мы тут видим? Ни чего такого, чего бы не рассматривали в этом посте (включая первую часть). Сперва генерирум статические платформы, посредствам символов "[","-", "*","]","|"
Где "[" — показывает парсеру начало уровня
"]" — соответсвенно, конец уровня
"|" — конец строки
"-" — обычная платформа
"*" — шипованная платформа

Затем, в строчке «player 55 44» мы указываем начальные координаты нашего героя
«portal 128 512 900 35» — первые два числа — координаты портала, вторые — координаты перемещения
«monster 150 200 1 2 150 100» — первые два числа, аналогично, координаты монстра, затем, вторые два — скорость горизонтальная и вертикальная, и последние — максимальное расстояние в одну сторону по горизонтали и вертикали.
Как вы уже заметили, как порталов, так и монстров может быть столько, сколько вам захочется.
Символ "/" означает конец файла. Все данные, после него, считаны не будут.

Теперь, давайте, напишем сам парсер.
Работаем в основном файле.

Для начала, перенесем все массивы и группы из функции main() в тело основной программы
***
level = []
entities = pygame.sprite.Group() # Все объекты
animatedEntities = pygame.sprite.Group() # все анимированные объекты, за исключением героя
monsters = pygame.sprite.Group() # Все передвигающиеся объекты
platforms = [] # то, во что мы будем врезаться или опираться
if __name__ == "__main__":
    main()

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

И добавляем новую функцию
def loadLevel():
    global playerX, playerY # объявляем глобальные переменные, это координаты героя

    levelFile = open('%s/levels/1.txt' % FILE_DIR)
    line = " "
    commands = []
    while line[0] != "/": # пока не нашли символ завершения файла
        line = levelFile.readline() #считываем построчно
        if line[0] == "[": # если нашли символ начала уровня
            while line[0] != "]": # то, пока не нашли символ конца уровня
                line = levelFile.readline() # считываем построчно уровень
                if line[0] != "]": # и если нет символа конца уровня
                    endLine = line.find("|") # то ищем символ конца строки
                    level.append(line[0: endLine]) # и добавляем в уровень строку от начала до символа "|"
                    
        if line[0] != "": # если строка не пустая
         commands = line.split() # разбиваем ее на отдельные команды
         if len(commands) > 1: # если количество команд > 1, то ищем эти команды
            if commands[0] == "player": # если первая команда - player
                playerX= int(commands[1]) # то записываем координаты героя
                playerY = int(commands[2])
            if commands[0] == "portal": # если первая команда portal, то создаем портал
                tp = BlockTeleport(int(commands[1]),int(commands[2]),int(commands[3]),int(commands[4]))
                entities.add(tp)
                platforms.append(tp)
                animatedEntities.add(tp)
            if commands[0] == "monster": # если первая команда monster, то создаем монстра
                mn = Monster(int(commands[1]),int(commands[2]),int(commands[3]),int(commands[4]),int(commands[5]),int(commands[6]))
                entities.add(mn)
                platforms.append(mn)
                monsters.add(mn)

Не забываем вызвать эту функцию и указать переменные startX и startY как стартовые координаты нашему герою.
def main():
    loadLevel()
***
hero = Player(playerX,playerY) # создаем героя по (x,y) координатам
***


Скачать результат.

Сейчас не очень интересно редактировать файл уровня руками, поэтому, в следующей подчасти мы напишем сам редактор уровней.
P.S. Уровень, созданный выше, вполне проходимый, дерзайте.
Tags:
Hubs:
+29
Comments17

Articles