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

от автора


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

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

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, go Y):         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. Уровень, созданный выше, вполне проходимый, дерзайте:-)

ссылка на оригинал статьи http://habrahabr.ru/post/196432/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *