Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #127, ИЮЛЬ 2009 г.

Веселая сторона Python'a. Юзаем библиотеку PyGame на примере игры «Лестница»

Лейтенант Роман «presidentua» Хоменко (http://tutamc.com)


Игры - одно из лучших изобретений человечества. В них все мечты сбываются, в них мы рыцари, короли, боги! За десятки лет существования игростроя сценаристы придумали для нас множество миров, но порой так хочется создать что-то свое – тот мир, где будут царить только твои правила. В рамках этой статьи я постараюсь научить тебя использовать волшебную палочку для создания игр - 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-шном файле есть теги второй версии. И это баг, а не фича.

Game over

На этом ознакомление с 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-го и нисколько не растерял игровой интерес :).

CD

На диске смотри игруху со всеми исходниками.

WWW

Содержание