Как стать автором
Обновить

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

Время на прочтение7 мин
Количество просмотров42K

Привет, друзья! Сегодня мы наконец-то доделаем нашего мариобоя. Начало тут и тут. Вот только мы не будем изобретать свой велосипед в виде редактора уровней, а воспользуемся готовым мощным инструментом. За знакомство с которым я благодарен господам(товарищам) 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
Теги:
Хабы:
Всего голосов 40: ↑38 и ↓2+36
Комментарии13

Публикации

Истории

Работа

Data Scientist
69 вакансий
Python разработчик
108 вакансий

Ближайшие события