from ][akep #127
Юзаем библиотеку PyGame на примере игры "Лестница"
Игры - одно с лучших изобретений человечества. В них все мечты сбываются, в них мы рыцари, короли, боги! За десятки лет существования игр сценаристы придумали для нас множество миров, но порой так хочется создать что-то свое, тот мир, где будут царить только твои правила. В рамках этой статьи я постараюсь научить тебя использовать волшебную палочку для создания игр - PyGame.
Год назад я познакомился с PyGame и влюбился в него с первых строк документации. Сразу же я вспоминал все свои мучения, связанные с программированием на С++ в связке с DirectX, вспоминал как все жутко тормозило, и я мучился над оптимизацией… а оно все равно тормозило. Вспоминал, как для элементарных вещей нужно было писать десятки строк.
PyGame же берет все заботы на себя, нам остается лишь написать саму игру, а не думать, к примеру, как правильно загрузить картинку... А если ты не собираешься писать игры, то можешь заюзать PyGame для создания оригинальных интерфейсов в своих прогах или визуализации какой-либо информации.
PyGame - это кроссплатформенный набор модулей, построенный поверх SDL библиотеки и предназначенный для написания видеоигр. Он включает в себя библиотеки для работы с графикой и звуком, реализованные с использованием язык Python. Автор этого чуда - Pete Shinners.
Но чтобы все сказанное о PyGame не было лишь теорией, разберем написание простой игры "Лестница". Выбор игры был обусловлен редактором рубрики, который прямо сказал, что или я напишу про эту игру, или он не отдаст мне ящик минералки, который он проспорил мне на последней "научной конференции" в баре. Однако, наша "Лестница" уже не будет текстовой игрушкой. У нас будет хакерский Колобок, который должен будет пройти снизу вверх по лестницам к двери. А сверху вниз будут падать камни, так и норовящие подвергнуть нашего Колобка кровавому прессингу. Сперва я хотел рассказать тебе о каждой строчке с этой игре, но, к сожалению, игруха получилась аж на 300 строчек кода, поэтому я буду рассказывать основные моменты, которые позволят тебе понять принципы работы с PyGame, а полный код игры будет ждать тебя на диске.
Погружение
Установка PyGame в Windows проходит в несколько кликов с инсталяхи, которую можно взять с http://www.pygame.org или с нашего диска. Для Linux PyGame находится в репозитариях. Я же, как ламер :), пишу под виндой, и юзаю версию Python 2.5 и соответствующую ей версию библиотеки, поскольку так советует поступать сам автор библиотеки из соображений скорости. Но хватит лирики, давай же скорее перейдем к кодингу. Как всегда в Python'e, использование PyGame начинается с подключения библиотек:
import pygame
from pygame.locals import *
Замечу, что pygame.locals мы полностью включили в область глобальной видимости, потому что это рекомендуют на официальном сайте, ведь именно там собраны основные константы, к которым мы будем часто обращаться, например, константы клавиш клавиатуры.
Теперь проинициализируем PyGame, и создадим окно размером 640x480 и с заглавием '][акер':
pygame.init()
pygame.display.set_mode((640, 480))
pygame.display.set_caption('][akep')
Посредством этого небольшого куска кода мы создали не только окно, но и главную поверхность(surface) для рисования, которую в любой точке программы можно получить командой pygame.display.get_surface(). Для самого же рисования реализовано множество функций, которые описаны в документации на официальном сайте. Для примера разберем рисование линии:
pygame.draw.line(window,(10,100,100),(10,200),(20,300),2)
Здесь window – поверхность, на которой рисуется линия, затем идет цвет линии в формате (R,G,B), начальные и конечные точки линии в формате (X,Y), и, наконец, ширина линии. Кстати, эта линия будет не сглажена, чтобы нарисовать сглаженную линию, нужно line изменить на aaline. Но это все не очень важно, ведь красивую графику таким образом не нарисуешь, нам придется подгружать картинки с файлов и их отображать, но об этом позже.
Если мы попробуем сейчас нарисовать линию на главной поверхности, то все равно ничего не увидим - PyGame автоматически использует двойную буферизацию, и для того, чтобы все это хозяйство перебросить с главной поверхности на поверхность, которая проецируется на видеокарту, нужно вызвать pygame.display.flip().
import pygame
from pygame.locals import *
pygame.init()
window = pygame.display.set_mode((640, 480))
pygame.display.set_caption('][akep')
pygame.draw.aaline(window,(10,100,100),(10,200),(20,300),2)
pygame.display.flip()
while 1:
pass
У этого тестового примера есть небольшой минус: он не обрабатывает события, а ведь нам нужно хотя бы научить его закрываться при нажатии на «крестик». Для получения событий существует функция pygame.event.get(), которая возвращает список всех событий, которые возникли с момента последнего вызова этой функции. Изменим последний бесконечный цикл на вот такой:
while 1:
for event in pygame.event.get():
if event.type == QUIT:
sys.exit()
Теперь приложение нормально отображается, не кажется зависшим, и даже закрывается ;), потому что мы принимаем события и корректно обрабатываем их.
Среди типов событий имеют место и события, связанные с клавишами – например, KEYDOWN и KEYUP. И если мы захотим, чтобы наше приложение закрывалось еще и при нажатии клавишу Esc, то можно добавить условие:
if event.type == KEYDOWN:
if event.key == K_ESCAPE:
sys.exit()
Здесь мы видим, что нажатая клавиша сохраняется в event.key, а мы сравниваем ее с константой клавиши Esc и должным образом на это реагируем. Все эти константы перечислены в документации, правда, мне почему-то не удалось найти константу на Enter, поэтому в этой ситуации я сравнивал key с числом 13 - кодом Enter'a.
Архитектура
Овладев этими базовыми знаниями, мы становимся готовы к гейм-кодингу. Начнем с самого трудного - архитектуры, ведь при неправильно выбранной архитектуре разработка превращается в ад, и последующее изменения каждой строчке кода можно приравнять к прослушиванию песни Димы Билана.
Архитектура "Лестницы" полностью основана на объектах. Основной объект - general порожден от класса General. Он проводит первоначальную инициализацию, и впоследствии обрабатывает глобальные события.
class General():
level = 0
def __init__(self):
pygame.init()
pygame.display.set_mode((640, 480))
pygame.display.set_caption('][akep')
def event(self,event):
if event.type == QUIT:
sys.exit()
if event.type == KEYUP:
if event.key == K_ESCAPE:
self.location = exit_location
Как видно из вышеуказанного кода, в конструкторе он создает окно, и обрабатывает события, ведущие к переходу к локации exit_location.
Локации - это объекты наследованные от класса Location:
class Location(object):
def __init__(self):
self.window = pygame.display.get_surface()
def event(self,event):
pass
def draw(self):
pass
Локации имеют конструктор, функцию по обработке событий, и функцию по прорисовке экрана. Все выглядит достаточно запутано, но давай посмотрим на всю программу целиком (без самих объектов):
general = General()
start_location = Start_location()
game_location = Game_location()
exit_location = Exit_location()
general.location = start_location
clock = pygame.time.Clock()
while 1:
for event in pygame.event.get():
general.location.event(event)
general.event(event)
general.location.draw()
pygame.display.flip()
clock.tick(30)
Как видим, сначала создается general объект. Далее создаются три локации: start_location - показывает юзеру приглашения начать игру, game_location - непосредственно игра, exit_location - показывает юзеру набранный уровень. Дальше в переменную general.location сохраняется стартовая локация, то есть, она как бы становится активной, и затем в бесконечном цикле мы получаем список событий и передаем их на обработку как текущей локации, так и на глобальную обработку. Затем вызывается метод draw активной локации, что прорисовывает экран. Как видим, удачно выбранная архитектура дает возможность разделить код, относящий к разным локациям.
Но, как ты заметил, я пропустил объяснение строчки clock.tick(30). А эта очень важная часть, которая не дает 4-х ядерному CoreDuo проиграть игру пользователю еще до того, как он что-то успеет увидеть ;). Эта строчка делает FPS статическим и равным 30. Иначе говоря, она оценивает разницу между последними вызовами, и следит, чтобы эта разница была равна 1/30 частью секунды. Конечно, современные игры обычно имеют динамический FPS, но это несколько сложнее в реализации, да и в аркадах это бессмысленно.
Посмотрим, как же реализована первая локация:
class Start_location(Location):
def __init__(self):
Location.__init__(self)
self.background = pygame.image.load('f.png')
def draw(self):
self.window.blit(self.background, (0, 0))
def event(self,event):
if event.type == KEYDOWN:
if event.key == 13:
general.location = game_location
В конструкторе вызывается функция pygame.image.load('f.png'), которая считывает рисунок, переданный в параметре и возвращает поверхность с ним. Реализация функции draw также состоит из одной функции главной поверхности. Функция blit копирует на свою поверхность (которая передана в первом параметре), начиная с позиции, которая передается во втором параметре. Обработка событий обрабатывает лишь нажатия Enter, после которого ставит локацию игры текущей локацией. Выбранная архитектура достаточно просто дает возможность разбрасывать код в отдельные обособленные объекты.
Локация Exit_location сделана аналогично этой и ее мы не будем рассматривать (смотри код на диске), но переходить к Game_location нам рановато, ведь мы еще ничего незнаем о спрайтах.
Спрайты
Спрайт - это графический объект в игре, который может перемещаться по игровому пространству. Спрайт - важнейшая деталь в 2D играх, хотя замечу, что и в 3D они также используются, например, при рисовании далеких объектов (при приближении они рисуются полигонами) и в процессе создания некоторых спецэффектов. То есть, спрайт - это объект, который содержит картинку или серию картинок (для анимации спрайта), координаты его нахождения и другие свойства, а также - логику движения спрайта и прочее. В PyGame спрайты нужно наследовать от pygame.sprite.Sprite.
Рассмотрим спрайт Камень:
class Kamen(pygame.sprite.Sprite):
speed = 1
status = 0 #0-down,1-left,2-right
def __init__(self):
pygame.sprite.Sprite.__init__(self)
image = pygame.image.load('kamen.png').convert()
image.set_colorkey(image.get_at((0,0)), RLEACCEL)
self.image = image
self.rect = image.get_rect()
def update(self,args):
#тут логика движения камня, ее смотри в исходнике на диске
self.rect.x = newX
self.rect.y = newY
Как видим, объект в себе содержит некоторые переменные, необходимые нам для логики - а именно, свою скорость и направление движения. Также здесь есть конструктор, подгружающий изображения. На процесс загрузки изображения нам придется обратить особое внимание - мы сначала загружаем картинку, а потом в ней, с помощью функции set_colorkey заменяем все пиксели, одинаковые по цвету с пикселем из левого верхнего угла на прозрачный. Конечно, это выглядит несколько дико, и на практике я советую использовать встроенную в png прозрачность, изменив convert() на convert_alpha(). Дело в том, что во время написания проги у меня не было денег на покупку лицензионного фотошопа ;), поэтому картинки я рисовал в Paint, а там прозрачности я не обнаружил.
Красота спрайтов особенно проявляется при их количестве, для их удобного объединения существует класс pygame.sprite.Group. Создадим три камня:
kamens = pygame.sprite.Group()
for i in xrange(0,3):
kamens.add( Kamen() )
Если нам нужно, чтобы камни немного продвинулись, вызовем kamens.update(args), и эта функция вызовет функцию update для каждого спрайта из группы. Для прорисовки всех спрайтов существует функция draw, которая принимает параметр «поверхность», на которой нужно прорисовать спрайты.
kamens.draw(window)
Теперь же мы готовы посмотреть на главную локацию - Game_location, а точнее - на функцию draw.
def draw(self):
self.window.blit(self.background, (0, 0))
self.kolobok.draw(self.window)
self.kamens.update()
self.kamens.draw(self.window)
for kamen in pygame.sprite.spritecollide(self.kolobok,self.kamens,0):
general.location = exit_location
В начале этой функции на главную поверхность отображается фоновое изображение, затем - рисуется спрайт колобка. Далее в группе спрайтов «Камни» появляются движения, которые рисуются на экране. И наконец, последние две строчки наиболее интересны, ведь с помощью одной простой функции pygame.sprite.spritecollide() мы проверяем, пересекся ли колобок с каким-то камнем. Эта функция возвращает булевый список, и если произошло столкновение, то мы изменяем локацию на exit_location.
Обработка клавиш
На этом этапе повествование можно было бы завершить, но в процессе рассказа я упустил два интересных момента. Сначала рассмотрим то, как двигается колобок. Ведь он не принимает входящих событий о нажатии клавиш! Объект колобка использует функцию pygame.key.get_pressed(), которая возвращает булевый список с состоянием нажатия каждой клавиши, которое мы потом можем проверить, используя константы. «Движущий» кусок кода колобка выглядит так:
keys = pygame.key.get_pressed()
if keys[K_LEFT]:
self.left()
if keys[K_RIGHT]:
self.right()
if keys[K_UP]:
self.up()
if keys[K_DOWN]:
self.down()
Музыка
Игра без музыки - это не игра. К тому же, поставить пластиночку можно очень просто:
pygame.mixer.music.load('s.mp3')
pygame.mixer.music.play()
Эти функции загружают mp3-файл и начинают его проигрывание. Далее мы можем процессом проигрывания управлять. Вот, например, пауза и снятие с паузы будут выглядеть так:
if event.type == KEYUP:
if event.key == K_m:
if self.music:
pygame.mixer.music.pause()
self.music = 0
else:
pygame.mixer.music.unpause()
self.music = 1
Кстати, музыка не будет играть, если в MP3-шном файле есть теги 2-рой версии. Это баг, а не фича.
Гейм овер
На этом ознакомление с PyGame можно считать завершенным, осталось лишь зайти на диск, найти там игру и поиграть в нее. Удачи тебе в создании собственных миров!
История PyGame. Рассказ создателя
Идея проекта PyGame родилась летом 2000 года. Будучи С-программистом с большим опытом, я обнаружил для себя Python и SDL практически в одно и тоже время. SDL расшифровывался как Simple Directmedia Library. Эта библиотека была создана Sam Lantinga, как кросплатформенная С-библиотека для контроля мультимедиа. Она использовалась в сотнях коммерческих и бесплатных игр. Я был под впечатлением от этой библиотеки и понял, что если совместить Python и SDL получиться очень интересная вещь. Работу над PyGame я начал в октябре 2000 года и через 6 месяцев была выпущена версия 1.0.
Вспоминания Александра Лозовского (из статьи «Шаг в прошлое», http://www.xakep.ru/magazine/xs/064/)
В далеком 1989 году я впервые увидел компьютер. Мне купили «Микрошу» — суперсоветскую ЭВМ, которая не может существовать без телевизора и магнитолы. На многократно зажеванной и разглаженной пленке кассеты МК60, помимо кучи полезных программ, были и игры, а среди них — та самая «Лестница». Идея ее проста: человечек идет снизу вверх по лабиринту (точнее, даже не по лабиринту, а просто по уровням, соединенным лестницами, причем игровое поле открывается взору полностью). Наверху же игрового поля находится один (несколько) источников, из которых вываливаются скачущие камни. Камни (в виде символа «0») катятся вниз по уровням и лестницам, подпрыгивают и норовят раздавить игрока. Цель — долезть до верха. Уровней, причем самого разного дизайна, было куча. Я, к примеру, дошел до 14-го и нисколько не растерял игровой интерес :).
Ниже прикрепил полные исходники написанной игры! ;)
