Привет, друзья! Сегодня мы наконец-то доделаем нашего мариобоя. Начало тут и тут. Вот только мы не будем изобретать свой велосипед в виде редактора уровней, а воспользуемся готовым мощным инструментом. За знакомство с которым я благодарен господам(товарищам) sourcerer и Tarvitz
Почему так?
На это есть несколько причин
- Удобный редактор уровней не пишется за 5 минут, лучше потратим это время на допиливание самой игры
- Более легкий способ добавления в игру разных на вид типов блоков
- Tiled map editor является универсальным инструментом для 2d игр, разобравшись с ним единожды, мы приобретаем навык генерации уровней для разных игр, написанных на разных языках и технологиях
Создаём неприятности и преграды нашему герою
Про работу с Tiled map editor можно почитать, например, тут.
Я же опишу основные моменты создания уровня именно для нашей игры.
Наша карта состоит из минимум 5-ти слоёв:
- BackGround — фон
- Platforms — блоки, по которым можно бегать
- DieBlocks -блоки, соприкосновение с которыми вызывает у героя моментальную смерть
- Monsters — слой объектов, тут наши монстрики, а так же, принцесса и сам герой
- 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