04 января 2010

PyGame: Анимация в движении...

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

Начнем разумеется с подготовки ресурсов. Вот наша анимация.

Теперь, что бы заставить нашего робота бегать и при этом шевелить ножками, нужно разобраться как же оживает картинка. Магия? Просроченные лекарства? Божественное вмешательство? Нет. Из-за быстрой смены картинок нам кажется что статический объект двигается. Не верите? Посмотрите на изображение ниже.

Вот видите, оно движется. Как? Эта анимация состоит из четырех изображений. Изображения сменяют друг друга каждые 180 миллисекунд. В результате этого нехитрого действия вам кажется что это "живое" изображение.

А теперь давайте добьемся этого с помощью pygame. Тут надо понимать что может возникнуть две ситуации:

  1. Кадры меняются с постоянной частотой.
  2. Кадры меняются с разной частотой.

В первом случае все просто и тривиально и код наш будет выглядеть так:

class Animation:
    def __init__(self, sprites=None, time=100):
        self.sprites = sprites
        self.time = time
        self.work_time = 0
        self.skip_frame = 0
        self.frame = 0

    def update(self, dt):
        self.work_time += dt
        # Считаем сколько кадров надо перелистнуть
        self.skip_frame = self.work_time / self.time
        if self.skip_frame > 0:
            # Не забываем, что у нас, при смене кадров с частотой в
            # 100 мс, вполне могло уже пройти 133 мс, и важно не
            # забыть про эти 33 мс.
            self.work_time = self.work_time % self.time
            self.frame += self.skip_frame
            if self.frame >= len(self.sprites):
                self.frame = 0

    def get_sprite(self):
        return self.sprites[self.frame]

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

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

Для этого мы изменим класс Animation:

class Animation:
    def __init__(self, sprites=None, time=100):
        self.sprites = sprites
        self.time = time
        self.work_time = 0
        self.skip_frame = 0
        self.frame = 0
        # Как видите мы просто подменяем скрытый метод.
        # Это позволяет изменить логику работы класса,
        # не меняя интерфейс
        if type(time) == list:
            self.__update = self.__update_any_time
        else:
            self.__update = self.__update_const_time

    def update(self, dt):
        self.work_time += dt
        self.__update(dt)

    def __update_const_time(self, dt):
        self.skip_frame = self.work_time / self.time
        if self.skip_frame > 0:
            self.work_time = self.work_time % self.time
            self.frame += self.skip_frame
            if self.frame >= len(self.sprites):
                self.frame = 0

    def __update_any_time(self, dt):
        while self.work_time - self.time[self.frame] > 0:
            self.work_time -= self.time[self.frame]
            self.frame += 1
            if self.frame >= len(self.sprites):
                self.frame = 0

    def get_sprite(self):
        return self.sprites[self.frame]

Вообще первый случай можно рассматривать как частный случай второго случая. О чем это я? Да о том, что на самом деле можно оставить метод __update_any_time. Но мы же хотим почувствовать разницу?

Теперь давайте попробуем использовать код приведенный выше.

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

import pygame, os

class Animation:
    def __init__(self, sprites=None, time=100):
        self.sprites = sprites
        self.time = time
        self.work_time = 0
        self.skip_frame = 0
        self.frame = 0
        if type(time) == list:
            self.__update = self.__update_any_time
        else:
            self.__update = self.__update_const_time

    def update(self, dt):
        self.work_time += dt
        self.__update(dt)

    def __update_const_time(self, dt):
        self.skip_frame = self.work_time / self.time
        if self.skip_frame > 0:
            self.work_time = self.work_time % self.time
            self.frame += self.skip_frame
            if self.frame >= len(self.sprites):
                self.frame = 0

    def __update_any_time(self, dt):
        while self.work_time - self.time[self.frame] > 0:
            self.work_time -= self.time[self.frame]
            self.frame += 1
            if self.frame >= len(self.sprites):
                self.frame = 0

    def get_sprite(self):
        return self.sprites[self.frame]

if __name__ == '__main__':
    pygame.init()

    display = pygame.display.set_mode((32*5,32))

    screen = pygame.Surface((16*5,16))

    robots = list()
    time = 180
    sprite = pygame.image.load('robot.png').convert_alpha()

    # Создание анимации.
    for x in range(5):
        anim = list()
        offset = 16*x

        anim.append(sprite.subsurface((0, offset,16,16)))
        anim.append(sprite.subsurface((16,offset,16,16)))
        anim.append(sprite.subsurface((32,offset,16,16)))
        anim.append(sprite.subsurface((16,offset,16,16)))

        if x == 4:
            robots.append(Animation(anim, [2000, 150, 150, 150]))
        else:
            robots.append(Animation(anim, time))

    clock = pygame.time.Clock()

    done = False
    dt = 0
    while not done:
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                done = True

        # Обновление каждого объекта анимации
        for robot in robots:
            robot.update(dt)

        screen.fill((255,255,255))

        # Рисуем анимацию.
        x = 0
        for robot in robots:
            screen.blit(robot.get_sprite(), (x, 0))
            x += 16

        display.blit(pygame.transform.scale(screen,(32*5,32)),(0,0))
        pygame.display.flip()

        dt = clock.tick(40)

Вот таким не хитрым способом вы можем добавить немного жизни в вашу игру.

На сим все.