少量图片制作动画精灵

17

我一直在寻找有关使用Pygame在Python中从几个图像制作简单精灵动画的好教程。但我还没有找到我想要的。

我的问题很简单:如何从几个图像制作动画精灵(例如:制作几个爆炸图像,尺寸为20x20像素,组合成一个动画)?

有什么好的想法吗?


2
你想要一个精灵表。你可以从多个图像加载,也可以从单个图像加载,并在blit时设置源Rect。这里有另一个例子:http://www.pygame.org/wiki/Spritesheet?parent=CookBook - ninMonkey
4个回答

29

有两种动画类型:frame-dependenttime-dependent。它们的工作方式相似。


在主循环之前

  1. 将所有图像加载到列表中。
  2. 创建三个变量:
    1. index,用于跟踪图像列表的当前索引。
    2. current_timecurrent_frame,用于跟踪自上次索引切换以来的当前时间或当前帧。
    3. animation_timeanimation_frames,定义多少秒或帧应该在切换图像之前经过。

在主循环期间

  1. current_time增加已经过去的秒数,或将current_frame增加1。
  2. 检查current_time >= animation_timecurrent_frame >= animation_frames。如果为真则继续执行步骤3-5。
  3. 重置current_time = 0current_frame = 0
  4. 增加索引,除非它将等于或大于图像数量。在这种情况下,重置index = 0
  5. 相应地更改精灵的图像。

一个完整的工作示例

import os
import pygame
pygame.init()

SIZE = WIDTH, HEIGHT = 720, 480
BACKGROUND_COLOR = pygame.Color('black')
FPS = 60

screen = pygame.display.set_mode(SIZE)
clock = pygame.time.Clock()


def load_images(path):
    """
    Loads all images in directory. The directory must only contain images.

    Args:
        path: The relative or absolute path to the directory to load images from.

    Returns:
        List of images.
    """
    images = []
    for file_name in os.listdir(path):
        image = pygame.image.load(path + os.sep + file_name).convert()
        images.append(image)
    return images


class AnimatedSprite(pygame.sprite.Sprite):

    def __init__(self, position, images):
        """
        Animated sprite object.

        Args:
            position: x, y coordinate on the screen to place the AnimatedSprite.
            images: Images to use in the animation.
        """
        super(AnimatedSprite, self).__init__()

        size = (32, 32)  # This should match the size of the images.

        self.rect = pygame.Rect(position, size)
        self.images = images
        self.images_right = images
        self.images_left = [pygame.transform.flip(image, True, False) for image in images]  # Flipping every image.
        self.index = 0
        self.image = images[self.index]  # 'image' is the current image of the animation.

        self.velocity = pygame.math.Vector2(0, 0)

        self.animation_time = 0.1
        self.current_time = 0

        self.animation_frames = 6
        self.current_frame = 0

    def update_time_dependent(self, dt):
        """
        Updates the image of Sprite approximately every 0.1 second.

        Args:
            dt: Time elapsed between each frame.
        """
        if self.velocity.x > 0:  # Use the right images if sprite is moving right.
            self.images = self.images_right
        elif self.velocity.x < 0:
            self.images = self.images_left

        self.current_time += dt
        if self.current_time >= self.animation_time:
            self.current_time = 0
            self.index = (self.index + 1) % len(self.images)
            self.image = self.images[self.index]

        self.rect.move_ip(*self.velocity)

    def update_frame_dependent(self):
        """
        Updates the image of Sprite every 6 frame (approximately every 0.1 second if frame rate is 60).
        """
        if self.velocity.x > 0:  # Use the right images if sprite is moving right.
            self.images = self.images_right
        elif self.velocity.x < 0:
            self.images = self.images_left

        self.current_frame += 1
        if self.current_frame >= self.animation_frames:
            self.current_frame = 0
            self.index = (self.index + 1) % len(self.images)
            self.image = self.images[self.index]

        self.rect.move_ip(*self.velocity)

    def update(self, dt):
        """This is the method that's being called when 'all_sprites.update(dt)' is called."""
        # Switch between the two update methods by commenting/uncommenting.
        self.update_time_dependent(dt)
        # self.update_frame_dependent()


def main():
    images = load_images(path='temp')  # Make sure to provide the relative or full path to the images directory.
    player = AnimatedSprite(position=(100, 100), images=images)
    all_sprites = pygame.sprite.Group(player)  # Creates a sprite group and adds 'player' to it.

    running = True
    while running:

        dt = clock.tick(FPS) / 1000  # Amount of seconds between each loop.

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RIGHT:
                    player.velocity.x = 4
                elif event.key == pygame.K_LEFT:
                    player.velocity.x = -4
                elif event.key == pygame.K_DOWN:
                    player.velocity.y = 4
                elif event.key == pygame.K_UP:
                    player.velocity.y = -4
            elif event.type == pygame.KEYUP:
                if event.key == pygame.K_RIGHT or event.key == pygame.K_LEFT:
                    player.velocity.x = 0
                elif event.key == pygame.K_DOWN or event.key == pygame.K_UP:
                    player.velocity.y = 0

        all_sprites.update(dt)  # Calls the 'update' method on all sprites in the list (currently just the player).

        screen.fill(BACKGROUND_COLOR)
        all_sprites.draw(screen)
        pygame.display.update()


if __name__ == '__main__':
    main()

何时选择哪种动画方式

时间相关的动画使您能够以相同的速度播放动画,无论帧速率有多慢/快或计算机有多慢/快。这使您的程序可以自由更改帧速率而不影响动画,并且即使计算机无法跟上帧速率,动画也将保持一致。如果程序出现滞后,则动画将追赶到它应该处于的状态,就好像没有发生滞后一样。

虽然可能会发生动画循环与帧速率不同步的情况,使得动画循环看起来不规则。例如,假设我们将帧更新为每0.05秒,而动画切换图像每0.075秒,则循环如下:

  1. 第1帧;0.00秒;图像1
  2. 第2帧;0.05秒;图像1
  3. 第3帧;0.10秒;图像2
  4. 第4帧;0.15秒;图像1
  5. 第5帧;0.20秒;图像1
  6. 第6帧;0.25秒;图像2

等等...

基于帧的动画如果您的计算机可以始终处理帧速率,则看起来更平滑。如果出现滞后,它将暂停在当前状态并在滞后停止时重新启动,这会使滞后更加明显。此选择稍微更容易实现,因为您只需要在每次调用时将current_frame增加1,而不是处理增量时间(dt)并将其传递给每个对象。

精灵

enter image description here enter image description here enter image description here enter image description here enter image description here enter image description here

结果

enter image description here


好的回答。如果使用了包含的 pygame.sprite.Sprite 类会更好。 - martineau
1
@martineau 是的,我考虑过,但不确定是否这样做会让初学者更难理解答案,因为这会引入继承和精灵组。也许以后有时间了我会改变主意并实现它。 - Ted Klein Bergman
如果没有其他问题,我认为你应该更改类的名称,以便它不与pygame中的类完全相同。这可能会让那些刚开始学习pygame的人感到特别困惑。 - martineau
1
@martineau 这是一个很好的观点。我已经更改了名称,并添加了精灵组。它似乎仍然足够简单。 - Ted Klein Bergman
这对我无效,因为 clock.tick(FPS) / 1000 始终返回0(整数)。相反,我使用了 dt = clock.tick(FPS) / 1000.0 强制使用浮点数。 - user2194299
@user2194299 是的,对于Python 2来说是正确的。但对于Python 3来说并不必要。 - Ted Klein Bergman

22

你可以尝试在update函数中修改精灵,使其替换成不同的图像。这样,在渲染精灵时,它将看起来是动画效果。

编辑:

下面是我草拟的一个快速示例:

import pygame
import sys

def load_image(name):
    image = pygame.image.load(name)
    return image

class TestSprite(pygame.sprite.Sprite):
    def __init__(self):
        super(TestSprite, self).__init__()
        self.images = []
        self.images.append(load_image('image1.png'))
        self.images.append(load_image('image2.png'))
        # assuming both images are 64x64 pixels

        self.index = 0
        self.image = self.images[self.index]
        self.rect = pygame.Rect(5, 5, 64, 64)

    def update(self):
        '''This method iterates through the elements inside self.images and 
        displays the next one each tick. For a slower animation, you may want to 
        consider using a timer of some sort so it updates slower.'''
        self.index += 1
        if self.index >= len(self.images):
            self.index = 0
        self.image = self.images[self.index]

def main():
    pygame.init()
    screen = pygame.display.set_mode((250, 250))

    my_sprite = TestSprite()
    my_group = pygame.sprite.Group(my_sprite)

    while True:
        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit(0)

        # Calling the 'my_group.update' function calls the 'update' function of all 
        # its member sprites. Calling the 'my_group.draw' function uses the 'image'
        # and 'rect' attributes of its member sprites to draw the sprite.
        my_group.update()
        my_group.draw(screen)
        pygame.display.flip()

if __name__ == '__main__':
    main()

它假设您在代码所在的同一文件夹内有两个名为 image1.pngimage2.png 的图像。


嘿...你知道怎么减慢动画吗?问题是如果我使用pygame.time.Clock,整个游戏都会变慢,而不仅仅是动画。 - lbartolic
@lucro93:你可以尝试在__init__中添加self.counter = 0。然后,在update中递增self.counter。如果它等于某个较大的数字(也许是99),那么将其重置为零并将self.index递增一。这样,动画每100个tick只更新一次。我相信还有更好的方法,但这种方法非常简单,可能适合你。 - Michael0x2a
晚了点,但我做的方法是使用 self.image = self.images[math.floor(self.index)] 然后将 self.index 增加0.08左右。 - gusg21

4
你应该将所有的精灵动画放在一个大的“画布”上,因此对于3个20x20的爆炸精灵帧,您将拥有60x20的图像。现在,您可以通过加载图像区域来获取正确的帧。
在精灵类中,在update方法中,您应该有类似以下内容的代码(为简单起见硬编码,我更喜欢有一个单独的类负责选择正确的动画帧)。在__init__上,self.f = 0。
def update(self):
    images = [[0, 0], [20, 0], [40, 0]]
    self.f += 1 if self.f < len(images) else 0
    self.image = your_function_to_get_image_by_coordinates(images[i])

3

对于一个动画的 Sprite ,需要生成一系列图片(pygame.Surface 对象)。列表中的每一帧都显示不同的图片,就像电影中的图片一样。这样可以呈现出一个动态的物体。
获取图片列表的一种方法是加载一个动画 GIF (Graphics Interchange Format)。不幸的是,PyGame没有提供加载动画 GIF 帧的函数。但是,有几个 Stack Overflow 的答案解决了这个问题:

一种方法是使用流行的Pillow库(pip install Pillow)。下面的函数加载动画GIF的帧,并生成一个pygame.Surface对象列表:

from PIL import Image, ImageSequence

def loadGIF(filename):
    pilImage = Image.open(filename)
    frames = []
    for frame in ImageSequence.Iterator(pilImage):
        frame = frame.convert('RGBA')
        pygameImage = pygame.image.fromstring(
            frame.tobytes(), frame.size, frame.mode).convert_alpha()
        frames.append(pygameImage)
    return frames

创建一个pygame.sprite.Sprite类,该类维护一个图像列表。实现一个更新方法,在每一帧中选择不同的图像。
将图像列表传递给类构造函数。添加一个index属性,指示列表中当前图像的索引。在Update方法中增加索引。如果索引大于或等于图像列表的长度,则重置索引(或使用模运算符%)。通过下标从列表中获取当前图像:
class AnimatedSpriteObject(pygame.sprite.Sprite):
    def __init__(self, x, bottom, images):
        pygame.sprite.Sprite.__init__(self)
        self.images = images
        self.image = self.images[0]
        self.rect = self.image.get_rect(midbottom = (x, bottom))
        self.image_index = 0
    def update(self):
        self.image_index += 1
        if self.image_index >= len(self.images):
            self.image_index = 0
        self.image = self.images[self.image_index]

请参见加载动画GIFSprite

示例GIF(来自Animated Gifs, Animated Image):

最简示例: repl.it/@Rabbid76/PyGame-SpriteAnimation

import pygame
from PIL import Image, ImageSequence

def loadGIF(filename):
    pilImage = Image.open(filename)
    frames = []
    for frame in ImageSequence.Iterator(pilImage):
        frame = frame.convert('RGBA')
        pygameImage = pygame.image.fromstring(
            frame.tobytes(), frame.size, frame.mode).convert_alpha()
        frames.append(pygameImage)
    return frames
 
class AnimatedSpriteObject(pygame.sprite.Sprite):
    def __init__(self, x, bottom, images):
        pygame.sprite.Sprite.__init__(self)
        self.images = images
        self.image = self.images[0]
        self.rect = self.image.get_rect(midbottom = (x, bottom))
        self.image_index = 0
    def update(self):
        self.image_index += 1
        self.image = self.images[self.image_index % len(self.images)]
        self.rect.x -= 5
        if self.rect.right < 0:
            self.rect.left = pygame.display.get_surface().get_width()

pygame.init()
window = pygame.display.set_mode((300, 200))
clock = pygame.time.Clock()
ground = window.get_height() * 3 // 4

gifFrameList = loadGIF('stone_age.gif')
animated_sprite = AnimatedSpriteObject(window.get_width() // 2, ground, gifFrameList)    
all_sprites = pygame.sprite.Group(animated_sprite)

run = True
while run:
    clock.tick(20)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    all_sprites.update()

    window.fill((127, 192, 255), (0, 0, window.get_width(), ground))
    window.fill((255, 127, 64), (0, ground, window.get_width(), window.get_height() - ground))
    all_sprites.draw(window)
    pygame.display.flip()

pygame.quit()
exit()

对我来说,gif动画在第一帧后看起来非常混乱(gif链接)。没有找到原因,似乎是Pillow中的一个错误,但使用gifsicle in.gif --colors 256 out.gif进行转换解决了这个问题,除此之外颜色现在被限制在256种。您可以尝试调整抖动(-f)、优化(-O3)和--color-method,以使其更好。如果没有使用--colors 256,它将使用本地颜色映射,这似乎会弄乱事情。也许我应该留意使用文件夹中的jpgs/pngs(我记得是convert in.gif %d.png)。 - Luc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接