...

понедельник, 7 октября 2013 г.

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


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends:



Комментариев нет:

Отправить комментарий