Пишем платформер на python, используя pygame. Часть 2 подчасть 2. Редактор уровней

  • Tutorial

Привет, друзья! Сегодня мы наконец-то доделаем нашего мариобоя. Начало тут и тут. Вот только мы не будем изобретать свой велосипед в виде редактора уровней, а воспользуемся готовым мощным инструментом. За знакомство с которым я благодарен господам(товарищам) sourcerer и Tarvitz

Почему так?


На это есть несколько причин
  • Удобный редактор уровней не пишется за 5 минут, лучше потратим это время на допиливание самой игры
  • Более легкий способ добавления в игру разных на вид типов блоков
  • Tiled map editor является универсальным инструментом для 2d игр, разобравшись с ним единожды, мы приобретаем навык генерации уровней для разных игр, написанных на разных языках и технологиях


Создаём неприятности и преграды нашему герою


Про работу с Tiled map editor можно почитать, например, тут.
Я же опишу основные моменты создания уровня именно для нашей игры.
Наша карта состоит из минимум 5-ти слоёв:
  1. BackGround — фон
  2. Platforms — блоки, по которым можно бегать
  3. DieBlocks -блоки, соприкосновение с которыми вызывает у героя моментальную смерть
  4. Monsters — слой объектов, тут наши монстрики, а так же, принцесса и сам герой
  5. Teleports -слой объектов, для чего — понятно по названию



Фон

Тут можете рисовать что угодно и как угодно, тайлы с этого слоя ни как не влияют на героя или игровой процесс, разве что на эстетический вид игры :)


Блоки, по которым можно бегать

На этом слое располагаются тайлы, которые в игре создают объекты класса Platform


Смертельно опасные блоки

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


Монстры

Это слой объектов, а значит, он не отображает в игре тайлы и каждый объект, добавленный на него, должен обладать какими — нибудь свойствами.
Объекты класса Monster, чей конструктор имеет следующий вид
class Monster(sprite.Sprite):
    def __init__(self, x, y, left, up, maxLengthLeft,maxLengthUp):

Обязательно должны иметь такие свойства, как: left, maxLeft, up, maxUp — заполняемые вручную и x, y — передающиеся по расположению объекта.


Объект персонажа должен иметь имя Player


Объект принцессы должен иметь имя Princess


Вид слоя:


Телепорты

Объекты этого слоя должны иметь свойства конечного назначения перемещения героя: goX и goY

Слой:

Как узнать конечные координаты?

Легко! Навести курсор мыши на то место, куда хотите, чтобы герой телепортировался и посмотреть слева снизу координаты места


Карту в игру


НемногоИзменим основной файл игры для того, чтобы открыть в ней выше созданную карту.
Для начала скачиваем необходимые библиотеки от сюда и кидаем их в папку с исходными кодами игры.
И импортируем их
import tmxreader # Может загружать tmx файлы
import helperspygame # Преобразует tmx карты в формат  спрайтов pygame

Далее, очищаем процедуру loadLevel(), мы её перепишем.
loadlevel
def loadLevel(name):
    global playerX, playerY # объявляем глобальные переменные, это координаты героя
    global total_level_height, total_level_width
    global sprite_layers # все слои карты

    world_map = tmxreader.TileMapParser().parse_decode('%s/%s.tmx' % (FILE_DIR, name)) # загружаем карту
    resources = helperspygame.ResourceLoaderPygame() # инициируем преобразователь карты 
    resources.load(world_map) # и преобразуем карту в понятный pygame формат
    
    sprite_layers = helperspygame.get_layers_from_map(resources) # получаем все слои карты
    
    # берем слои по порядку 0 - слой фона, 1- слой блоков, 2 - слой смертельных блоков
    # 3 - слой объектов монстров, 4 - слой объектов телепортов
    platforms_layer = sprite_layers[1] 
    dieBlocks_layer = sprite_layers[2]

    for row in range(0, platforms_layer.num_tiles_x): # перебираем все координаты тайлов
        for col in range(0, platforms_layer.num_tiles_y):
            if platforms_layer.content2D[col][row] is not None:
                pf = Platform(row * PLATFORM_WIDTH, col * PLATFORM_WIDTH)# как и прежде создаем объкты класса Platform
                platforms.append(pf)
            if dieBlocks_layer.content2D[col][row] is not None:
                bd = BlockDie(row * PLATFORM_WIDTH, col * PLATFORM_WIDTH)
                platforms.append(bd)

    teleports_layer = sprite_layers[4]
    for teleport in teleports_layer.objects:
        try: # если произойдет ошибка на слое телепортов
            goX = int(teleport.properties["goX"]) * PLATFORM_WIDTH
            goY = int (teleport.properties["goY"]) * PLATFORM_HEIGHT
            x = teleport.x
            y = teleport.y - PLATFORM_HEIGHT
            tp = BlockTeleport(x, y, goX, goY)
            entities.add(tp)
            platforms.append(tp)
            animatedEntities.add(tp)
        except: # то игра не вылетает, а просто выводит сообщение о неудаче
            print(u"Ошибка на слое телепортов")

    monsters_layer = sprite_layers[3]
    for monster in monsters_layer.objects:
        try:
            x = monster.x
            y = monster.y
            if monster.name == "Player":
                playerX = x
                playerY = y - PLATFORM_HEIGHT
            elif monster.name == "Princess":
                pr = Princess(x, y - PLATFORM_HEIGHT)
                platforms.append(pr)
                entities.add(pr)
                animatedEntities.add(pr)
            else:
                up = int(monster.properties["up"])
                maxUp = int(monster.properties["maxUp"])
                left = int(monster.properties["left"])
                maxLeft = int(monster.properties["maxLeft"])
                mn = Monster(x, y - PLATFORM_HEIGHT, left, up, maxLeft, maxUp)
                entities.add(mn)
                platforms.append(mn)
                monsters.add(mn)
        except:
            print(u"Ошибка на слое монстров")

    total_level_width = platforms_layer.num_tiles_x * PLATFORM_WIDTH # Высчитываем фактическую ширину уровня
    total_level_height = platforms_layer.num_tiles_y * PLATFORM_HEIGHT   # высоту
 


Что мы тут видим?

Начнем с того, что теперь процедура принимает входной параметр name, который используется для загрузки карты уровня. Это сделали для того, чтобы сделать переход между уровнями.
Далее идёт загрузка и преобразование карты, и по тому же принципу, что мы парсили массив с картой, парсим слои с тайлами. Обратите внимание, что теперь созданные объекты классов Platform и BlockDie не помещаются в группу entities, а значит, мы их не будет отображать т.е. они будут существовать, но не отображаться. Вместо них мы будет отображать тайлы со слоёв карты.

Продолжим

Теперь займемся процедурой main
Добавим визуализатор(рендерер) слоёв карты
renderer = helperspygame.RendererPygame() # визуализатор

Для чего он — увидим чуть ниже

Изменим вызов процедуры loadLevel
for lvl in range(1,4):
        loadLevel("map_%s" % lvl)

И далее, весь код будет в этом цикле

В блоке вывода изображений на экран добавим работу визуализатора
for sprite_layer in sprite_layers: # перебираем все слои
      if not sprite_layer.is_object_group: # и если это не слой объектов
           renderer.render_layer(screen, sprite_layer) # отображаем его
***
 center_offset = camera.reverse(CENTER_OF_SCREEN) # получаем координаты внутри длинного уровня
 renderer.set_camera_position_and_size(center_offset[0], center_offset[1], \
                                                  WIN_WIDTH, WIN_HEIGHT, "center")

Обратите внимание, что renderer выводит свои изображения по центру экрана, внутри передвигающегося фокуса камеры, для этого нам нужно было добавить процедуру в класс Camera
Camera
class Camera(object):
    def __init__(self, camera_func, width, height):
        self.camera_func = camera_func
        self.state = Rect(0, 0, width, height)

    def apply(self, target):
        return target.rect.move(self.state.topleft)

    def update(self, target):
        self.state = self.camera_func(self.state, target.rect)

    def reverse(self, pos):# получение внутренних координат из глобальных
        return pos[0] - self.state.left, pos[1] - self.state.top



Уберем то, что перенесли в процедуру loadlevel, добавим немного нового и получим следующий вид:
main
def main():
    pygame.init() # Инициация PyGame, обязательная строчка
    screen = pygame.display.set_mode(DISPLAY) # Создаем окошко
    pygame.display.set_caption("Super Mario Boy") # Пишем в шапку
    bg = Surface((WIN_WIDTH, WIN_HEIGHT)) # Создание видимой поверхности
    # будем использовать как фон

    renderer = helperspygame.RendererPygame() # визуализатор
    for lvl in range(1,4):
        loadLevel("levels/map_%s" % lvl)
        bg.fill(Color(BACKGROUND_COLOR))     # Заливаем поверхность сплошным цветом

        left = right = False # по умолчанию - стоим
        up = False
        running = False
        try:
            hero = Player(playerX, playerY) # создаем героя по (x,y) координатам
            entities.add(hero)
        except:
            print (u"Не удалось на карте найти героя, взяты координаты по-умолчанию")
            hero = Player(65, 65)
        entities.add(hero)

        timer = pygame.time.Clock()

        camera = Camera(camera_configure, total_level_width, total_level_height)

        while not hero.winner: # Основной цикл программы
            timer.tick(60)
            for e in pygame.event.get(): # Обрабатываем события
                if e.type == QUIT:
                    raise SystemExit, "QUIT"
                if e.type == KEYDOWN and e.key == K_UP:
                    up = True
                if e.type == KEYDOWN and e.key == K_LEFT:
                    left = True
                if e.type == KEYDOWN and e.key == K_RIGHT:
                    right = True
                if e.type == KEYDOWN and e.key == K_LSHIFT:
                    running = True

                if e.type == KEYUP and e.key == K_UP:
                    up = False
                if e.type == KEYUP and e.key == K_RIGHT:
                    right = False
                if e.type == KEYUP and e.key == K_LEFT:
                    left = False
                if e.type == KEYUP and e.key == K_LSHIFT:
                    running = False
            for sprite_layer in sprite_layers: # перебираем все слои
                if not sprite_layer.is_object_group: # и если это не слой объектов
                   renderer.render_layer(screen, sprite_layer) # отображаем его

            for e in entities:
                screen.blit(e.image, camera.apply(e))
            animatedEntities.update() # показываеaм анимацию
            monsters.update(platforms) # передвигаем всех монстров
            camera.update(hero) # центризируем камеру относительно персонаж
            center_offset = camera.reverse(CENTER_OF_SCREEN)
            renderer.set_camera_position_and_size(center_offset[0], center_offset[1], \
                                                  WIN_WIDTH, WIN_HEIGHT, "center")
            hero.update(left, right, up, running, platforms) # передвижение
            pygame.display.update()     # обновление и вывод всех изменений на экран
            screen.blit(bg, (0, 0))      # Каждую итерацию необходимо всё перерисовывать
        for sprite_layer in sprite_layers:
            if not sprite_layer.is_object_group:
                renderer.render_layer(screen, sprite_layer)
        # когда заканчиваем уровень
        for e in entities:
            screen.blit(e.image, camera.apply(e)) # еще раз все перерисовываем
        font=pygame.font.Font(None,38) 
        text=font.render(("Thank you MarioBoy! but our princess is in another level!"), 1,(255,255,255))# выводим надпись
        screen.blit(text, (10,100))
        pygame.display.update()
        time.wait(10000) # ждем 10 секунд и после - переходим на следующий уровень


Что тут интересного?

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

Вот и всё. Вот так, легко и быстро мы переделали игру для загрузки уровней из tmx файлов.

Исходники на github
Поделиться публикацией
Похожие публикации
Ммм, длинные выходные!
Самое время просмотреть заказы на Фрилансим.
Мне повезёт!
Реклама
Комментарии 13
  • 0
    Спасибо за ссылку на мою статью :)
    Не ожидал, что для питона существует библиотека для таких карт
    Статья хорошая, спасибо!
    • 0
      Спасибо!
      Для питона, и не только, тут представлено несколько библиотек
    • +1
      Спасибо за хороший и полезный тутор.
      • +1
        А кстати что там с дружбой pygame и Андроида на данный момент известно?
        • +1
          Есть pgs4a, но лично у меня работает лишь на одном из трёх моих устройств на Android.
          • 0
            И тот наверное на 2й версии Андроида?
            • 0
              Да. На четвертом работает только в эмуляторе.
      • 0
        Velese, как обычно занес в букмарки, чтобы почитать на досуге (когда тот меня настигнет). Вопрос в следующем, если ли интерес продолжать улучшать платформер? К примеру есть box2d движок для физических обсчетов, который также имеет биндинги для python'а и вполне адекватно встраивается в PyGame.

        На мой взгляд было бы весьма не дурно эту тему продолжить.
        • 0
          Я начинаю изучать юнити, хочется попробовать под мобильные платформы что-нибудь написать, поэтому небольшой цикл постов о платформере закончен.
          • 0
            В Юнити в какую сторону планируете идти? 2Д или 3Д?
            • 0
              с 2d игрался на java + libGDX. Понравилось, довольно таки крутой фреймворк. Теперь хочу в 3д себя попробовать
              • 0
                С libGDX я тоже только игрался, тоже понравилось, но как-то не увлекло. Вот интересно про Юнити и 2Д почитать.

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