18 ноября 2009

PyGame: Сцены и меню...

Приветствую тебя, человек умеющий читать.

В этом уроке мы с вами сделаем еще один шаг к игре! А именно, мы сделаем меню, это конечная цель нашего урока. Да-да, именно это наша цель, а не власть над миром, что конечно тоже является приоритетной задачей.

И начнем мы как всегда из далека. Давайте с начало создадим директорию в корне проекта и называем ее lib, после чего нужно создать в ней файл __init__.py. Теперь у нас есть пакет lib. Файл __init__.py пока оставим пустым.

Если вы читали урок 1, изучили код, написали на его основе свой (или, что более вероятно, просто скопировали), и он у вас заработал. Не смотрите в него, этот код вам не понадобится, ну или почти не понадобится.

Первым делом мы создадим класс Game. Он - верхушка айсберга. Его задача скрыть в себе всю техническую часть нашей игры. От него нам надо не много, для начало пусть просто создаст окно и запустит основной игровой цикл.

Немного теории, игровой цикл - это цикл в котором происходит всё. Когда я говорю "всё", я говорю "ВСЁ". В самом начале мы настраиваем нашу игру, говорим что откуда брать, описываем все объекты, создаем сцены (о них я мы поговорим попозже), а потом все отдаем на растерзание игровому циклу. Что же он делает? Ничего сложного, обновляет сцену, говоря ей сколько времени с прошлого раза прошло и говорит какие события попали в очередь (об этом тоже чуть попозже), сцена опираясь на эти данные рисует себя и обновляют своих актеров (объекты помещенные в сцену), после чего мы обновляем буфер кадра и начинаем все с начало, пока не закончится сцена.

Вспомните предыдущий урок, от туда нам понадобится класс ResManager, так же нам понадобится создать файл Game.py.

Наша директория lib выглядит так:

lib
|-- Game.py
`-- ResManager.py

Хотите увидеть что находится в Game.py? Все равно смотрите:

# -*- coding: utf-8 -*-

import pygame
from ResManager import ResManager

class Game:
    # ширина и высота окна,
    # цвет которым будет залит нарисованный экран,
    # максимальный fps
    def __init__(self,
                 width   = 640,
                 height  = 480,
                 color   = (255,255,255),
                 fps     = 40,
                 scene   = None,
                 manager = ResManager()):
        pygame.init()

        self.set_display(width, height)

        self.fps       = fps
        self.__manager = manager
        self.scene     = scene

        self.__display.fill(color)
        pygame.display.flip()

    # Создаем окно
    def set_display(self, width, height):
        self.__display = pygame.display.set_mode((width, height))

    def set_caption(self, title = None, icon = None):
        if title == None:
            pygame.display.set_caption("game")
        else:
            pygame.display.set_caption(title)

        if icon != None:
            pygame.display.set_icon(self.__manager.get_image(icon))

    def game_loop(self):
        # Если сцены нет, то все заканчивается.
        while self.scene != None:
            clock = pygame.time.Clock()
            dt    = 0

            # Инициализируем сцену, даем ей холст для рисования и ResManager.
            self.scene.start(self.__display, self.__manager)

            while not self.scene.is_end():
                # говорим сколько времени прошло, события получаются
                # стандартным для pygame образом через pygame.event
                self.scene.loop(dt)

                pygame.display.flip()

                dt = clock.tick(self.fps)

            # Сцена знает что будет после ее завершения.
            self.scene = self.scene.next()

Вы наверное заметили что имена некоторых переменных начинаются с двух подчеркиваний, это сделано для того что бы их не было видно снаружи класса, на самом деле интерпретатор Python просто добавит к имени переменной имя класса.

Выше я упомянул про сцены. Это особого рода классы имеющие общего предка в лице Scene. В родительском классе определенны базовые методы работы, а так же некоторые функции которые упрощают создание потомков. Я вам лучше покажу этот класс по имени Scene.

Создадим в lib файл Scene.py.

# -*- coding: utf-8 -*-

import pygame
import const

class Scene:
    def __init__(self, next_scene = None):
        self.__next_scene = next_scene

    def loop(self, dt):
        self.__event(pygame.event)
        self._update(dt)
        self._draw(dt)

    def start(self, display, manager):
        self.display = display
        self.manager = manager
        self._start()
        self.__end = False

    # Эту функцию стоит определит в потомке если в
    # сцене нужно что-то создать, например наш логотип.
    def _start(self):
        pass

    # Эта функция которая не должна вызываться вне этого класса,
    # ну и вы конечно поняли зачем нужно __.
    def __event(self, event):
        if len(event.get(pygame.QUIT)) > 0:
            self.__end = True
            self.set_next_scene(None)
            return

        self._event(event)

        # event.get() эквивалентен pygame.event.get()
        # передавая параметр в get мы говорим что именно
        # нас интересует из событий.
        for e in event.get(const.END_SCENE):
            if e.type == const.END_SCENE:
                self.__end = True

    # Эту функцию придется переопределить в потомке
    def _draw(self, dt):
        pass

    # и эту тоже
    def _event(self, event):
        pass

    # как и эту.
    def _update(self, dt):
        pass

    def next(self):
        return self.__next_scene

    def is_end(self):
        return self.__end

    def the_end(self):
        pygame.event.post(pygame.event.Event(const.END_SCENE))

    def set_next_scene(self, scene):
        self.__next_scene = scene

Для Scene нам понадобится создать в lib еще один файл const.py.

from pygame.locals import USEREVENT

END_SCENE =  USEREVENT + 1

А теперь давайте сделаем то же что и в прошлом уроке, создадим окно со своим заголовком и иконкой и покажем логотип. Только на этот раз будем использовать класс Game и Scene.

Для начала, давайте определимся что будет происходить на нашей сцене (время в секундах):

0 - 1
ждем
1 - 3
логотип появляется
3 - 4
ждем
4 - 6
логотип исчезает
6 - 6.5
ждем
6 . 5
сцена заканчивается.

Это был первый подход, все представляет из себя одну сцену. Можно сделать иначе, разбит эту сцену на более мелкие сцены:

  1. Сцена ожидания, причем сколько ждать можно сказать сцене при ее создании
  2. Появление логотипа анимация в одну сторону.
  3. Исчезновение логотипа, тоже анимация в одну сторону.

То есть тогда последовательность сцен будет следующей: 1, 2, 1, 3, 1. Мы конечно будем делать так, то есть разобьем сцену на несколько более мелких сцен.

Для начало мы вспомним про анимацию прозрачности из прошлого урока. Помните я спрашивал почему такой подход к анимации прозрачности был плох? Если бы вы использовали логотип с полупрозрачными элементами, вы бы поняли это сразу.

Полупрозрачные элементы не сохраняются при нашем методе анимации. Причина в том, что мы полностью вычитаем альфа канал из изображения, а потом последовательно его увеличиваем возвращая к прежнему значению, а так как в минус значения канала уйти не могло, мы получаем полностью не прозрачное изображение, так как уровняли значения альфа канала в самом начале. Не волнуйтесь, сейчас мы все поправим.

По традиции создадим в lib файл с названием Transparent.py.

# -*- coding: utf-8 -*-

import pygame

class Transparent:
    def __init__(self, time = 2000, show = True):
        self.show = show
        self.set_time(time)
        self.run = False

    def update(self, dt):
        if self.run:
            self.add += float(dt) * self.time
            if int(self.add) > 0:
                self.count += int(self.add)
                self.add = self.add - int(self.add)
                if self.count > 255:
                    self.count = 255
                    self.run = False

    def start(self):
        self.count = 0
        self.add = float(0)
        self.run = True

    def is_start(self):
        return self.run

    def stop(self):
        self.run = False

    def set_time(self, time = 2000):
        self.time = float(255)/float(time)

    # Как видите мы изменяем копию спрайта, да и сам спрайт не храним
    def get_sprite(self,sprite):
        sprite_copy = sprite.copy()
        if self.show:
            sprite_copy.fill((0,0,0,255-self.count), None, pygame.BLEND_RGBA_SUB)
        else:
            sprite_copy.fill((0,0,0,self.count), None, pygame.BLEND_RGBA_SUB)
        return sprite_copy

    def toggle(self):
        self.show = not self.show

Класс не особо изменился с прошлого урока, разве что стал проще.

Теперь займемся нашим основным кодом что находится в файле main.py. Только с начало кое-что добавим в файл __init__.py что в lib.

from const import *
from ResManager import ResManager
from Scene import Scene
from Transparent import Transparent
from Game import Game

А теперь к main.py.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time, pygame, lib

def get_center(surface, sprite):
    return (surface.w/2 - sprite.w/2,
            surface.h/2 - sprite.h/2)

# Эта сцена ожидания наследуется от нашей сцены из lib
class WaitScene(lib.Scene):
    def __init__(self, time = 1000, *argv):
        lib.Scene.__init__(self, *argv)
        self.run = 0
        self.time = time

    def _event(self, event):
        # Здесь нам нужно обработать все события.
        # Если есть объекты которым нужны события их нужно
        # оповестить здесь, так как event.get() отчищает очередь событий,
        # а в объектах нужно брать из очереди только нужные события.
        # Здесь можно обработать все.
        for e in event.get():
            if e.type == pygame.KEYDOWN:
                self.the_end()
                self.set_next_scene(None)

        if not self.run < self.time:
            self.the_end()

    def _update(self, dt):
        self.run += dt

# Эта сцена позволит показать наш логотип
class ShowScene(lib.Scene):
    # Как видите мы создаем при старте сцены наш логотип(загружая его из файла)
    # и анимацию.
    def _start(self):
        sprite = self.manager.get_image('logo.png')
        self.sprite = pygame.transform.scale(sprite, (sprite.get_rect().w * 5,
                                             sprite.get_rect().h * 5))

        self.plambir = lib.Transparent(3000)
        self.plambir.start()

    def _event(self, event):
        for e in event.get():
            if e.type == pygame.KEYDOWN:
                self.the_end()
                self.set_next_scene(None)

        if not self.plambir.is_start():
            self.the_end()

    def _update(self, dt):
        self.plambir.update(dt)

    def _draw(self, dt):
        self.display.fill((255,255,255))
        # Как видите мы рисуем логотип, сначала пропустив его через анимацию
        # прозрачности.
        self.display.blit(self.plambir.get_sprite(self.sprite),
                          get_center(self.display.get_rect(),
                                     self.sprite.get_rect()))

# Эта сцена исчезновения логотипа, так как она не особо от сцены
# появления отличается, мы просто кое что изменим в инициализации
# класса ShowScene.
class HideScene(ShowScene):
    def _start(self):
        ShowScene._start(self)

        self.plambir.toggle()
        self.plambir.set_time(1000)

if __name__ == '__main__':
    # Вот так хитро все и закрутилось.
    # ждем, показываем, ждем, скрываем, ждем.
    scene = WaitScene(1000, ShowScene(WaitScene(500, HideScene(WaitScene(1000)))))
    game = lib.Game(640, 480, scene = scene)
    game.set_caption("plambir", "icon.png")

    game.game_loop()

А теперь к обещанному еще в прошлом уроке меню. Мы попробуем сделать что-нибудь простое. Сделаем классическое меню, элементы располагаются друг над другом, и будет их всего 3:

  • Новая игра
  • Настройки
  • Выход

Сразу скажу, это просто пример, вы можете сделать сколь угодное длинное меню, принцип будет тот же. Все что нужно нам, это разделить выбранный(текущий элемент) и остальные, а также назначит функцию которая будет вызвана если пользователь выберет конкретный элемент. Вот так изменится наш main.py после добавления класса Menu и класса MenuScene.

...
                # Заменяем следующую сцену в WaitScene как MenuScene, а не None.
                self.set_next_scene(MenuScene())
...
                # Точно так же в ShowScene.
                self.set_next_scene(MenuScene())
...
class Menu:
    def __init__(self, position = (0,0), loop = True):
        self.index = 0
        self.x = position[0]
        self.y = position[1]
        self.menu = list()

    # Метод перемещающий нас в низ циклично по всем элементам.
    def down(self):
        self.index += 1
        if self.index >= len(self.menu):
            self.index = 0

    # Тоже самое но в вверх.
    def up(self):
        self.index -= 1
        if self.index < 0:
            self.index = len(self.menu)-1

    # Добавляет новый элемент, нужно передать 2 изображения.
    # На 1 не выбранный вид элемента.
    # На 2 выбранный элемент
    def add_menu_item(self, no_select, select, func):
        self.menu.append({ 'no select' : no_select,
                           'select' : select,
                           'func' : func })

    def call(self):
        self.menu[self.index]['func']()

    def draw(self, display):
        index = 0
        x = self.x
        y = self.y
        for item in self.menu:
            if self.index == index:
                display.blit(item['select'], (x, y))
                y += item['select'].get_rect().h
            else:
                display.blit(item['no select'], (x, y))
                y += item['no select'].get_rect().h
            index += 1

class MenuScene(lib.Scene):
    def item_call(self):
        print("item_call")
        self.the_end()

    def _start(self):
        self.menu = Menu((330,300))

        # Именно таким образом мы можем получить текст в pygame
        # В данном случае мы используем системный шрифт.
        font      = pygame.font.SysFont("Monospace", 40, bold=False, italic=False)
        font_bold = pygame.font.SysFont("Monospace", 40, bold=True, italic=False)
        item = u"Новая игра"
        self.menu.add_menu_item(font.render(item,True,(0,0,0)),
                                font_bold.render(item,True,(0,0,0)),
                                self.item_call)
        item = u"Настройки"
        self.menu.add_menu_item(font.render(item,True,(0,0,0)),
                                font_bold.render(item,True,(0,0,0)),
                                self.item_call)
        item = u"Выход"
        self.menu.add_menu_item(font.render(item,True,(0,0,0)),
                                font_bold.render(item,True,(0,0,0)),
                                self.item_call)

    def _event(self, event):
        for e in event.get():
            if e.type == pygame.KEYDOWN:
                if e.key == pygame.K_DOWN:
                    self.menu.down()
                elif e.key == pygame.K_UP:
                    self.menu.up()
                elif e.key == pygame.K_RETURN:
                    self.menu.call()

    def _draw(self, dt):
        self.display.fill((255,255,255))
        self.menu.draw(self.display)


if __name__ == '__main__':
    # Вот так хитро все и закрутилось.
    # ждем, показываем, ждем, скрываем, ждем, меню.
    scene = WaitScene(1000, ShowScene(WaitScene(500, HideScene(WaitScene(1000,MenuScene())))))
    game = lib.Game(640, 480, scene = scene)
    game.set_caption("plambir", "icon.png")

    game.game_loop()

Вот и меню. Правда все просто? Мы конечно в следующем уроке попробуем разбавить меню анимацией, но пока сойдет и так.

К следующему уроку попробуйте расположить меню по середине экрана.