本文内容
- subsurface() 子 surface的学习
- 实现一个精灵动画
Note:本文代码基于上一小节,可以直接在上文文末下载工程文件。
在上一节中,我们只是让坦克移动起来,但不是真正的动画,因为我们只是对一个图片进行旋转。这一节我们介绍如何播放一个动画,本节我们需要的素材是一个爆炸的序列图片,用来做坦克子弹爆炸的效果:
这是一张图片,这个图片就已经包含了整个爆炸效果的序列。我们以上文介绍的方法,在原有的工程中添加代码,测试一下整体显示效果。将图片添加到工程中,并命名为explosion_01。然后使用screen.blit()将图片在屏幕上绘画出来,以下是具体代码片段:
# 子弹图片
import pygame
... #省略代码
explosion01_image = pygame.image.load('explosion_01.png').convert_alpha() #载入子弹爆炸图片
explosion01_rect = explosion01_image.get_rect() #获取图片的Rect
while True:
framerate .tick(30)
... #省略代码
screen.blit(background, (0, 0))
screen.blit(explosion01_image, explosion01_rect) #绘制图片
my_tank.display(screen)
pygame.display.update()
程序运行后如下所示:
单个图像大小看起来还算比较合适,但是程序把整一个图片画出来了(当然后3列出界了)。所以我们需要切割这张图片,总共8列4行,我们可以按这个切割成32个图片。
subsurface() 的使用
我们用画图打开可以知道这张图片是1000x500像素的,经过计算可以知道每个图像的大小是125x125,这里以裁剪出第一个图像(即最左上角)为例:
explosion01sub_image = explosion01_image.subsurface((0, 0, 125, 125))
我们进一步的,在explosion01_image图像的(0,0)处裁剪出一个125x125像素大小的子图像出来(explosion01sub_image ),接着我们用绘图函数把它画出来,由于默认位置也是(0,0),所以测试时我们注意要把坦克移开,否则会遮挡到:
这里因为是测试函数,所以我们只是自己计算单一个帧(即一个图形)的大小,和位置,然后写进程序。在实际开发中,我们会写入算法代码,让程序变得更灵活,更易用。
序列的播放
我们已经知道如何裁剪并显示一个子表面了,接着需要让精灵每一次更新的时候都切换到下一帧,这样当程序运行的时候,就会看到一个动画了,我们可以定一个函数来实现这个功能:
def update(self, current_time, rate=0):
if current_time > self.last_time + rate:
self.frame += 1
if self.frame >self.last_frame:
self.frame = self.first_frame
self.last_time = current_time
我们还判断了了时间,这是为了控制精灵的update是否播放下一帧的变量,rate越大,我们播放下一帧的时间也需要越长。当时间合适,我们就把帧的值+1,由于根据这个变量来决定显示的帧需要,所以它每加1,我们就会显示下个帧,这样也就实现了播放动画。
Rect 的计算
在上文中,我们画出了第一帧,我们是直接计算的,如果我们要显示第二帧,我们就要计算第二章的起始位置,明显的,第一帧是(0,0),第二帧就是(125,0),它们的大小是一样的,所以我们可以通过以下代码来裁剪出第二帧:
explosion01sub_image = explosion01_image.subsurface((125, 0, 125, 125))
当然了,我们不可能每一帧都自己去计算,这样不仅麻烦,也容易出错,还需要很多的变量来存储,所以我们希望可以通过程序自动计算得出坐标以及每一帧像素的宽和高。
我们使用frame变量来表示帧序号(即第几帧),正如前文所述,当frame = 1时,subsurface 左上角坐标为(0,0),当frame = 2 的时候,该坐标则为(120,0),实际上,它遵循以下的数学关系:
帧宽度 = 图片宽度 / 列数
帧高度 = 图片高度 /行数
坐标x = (帧序号 % 列数 )* 帧宽度
坐标y = (帧序号 // 列数 )* 帧高度
当我们有了这些计算之后,我们只需要给程序传入图片,列数和行数就可以计算每一个帧的位置和大小了。了解了原理,接着来看下完整的子弹类的代码:
# 子弹精灵类
class Explode(pygame.sprite.Sprite):
def __init__(self):
self.frame_width = 0
self.frame_height = 0
self.rect = 0,0,0,0
self.columns = 0
self.last_frame = 1
self.last_time = 0
self.frame = 0
self.first_frame = 0
self.old_frame = 0
def load(self, filename, row, columns):
self.master_image = pygame.image.load(filename).convert_alpha() # 载入整张图片
self.master_rect = self.master_image.get_rect() # 获取图片的rect值
self.frame_width = self.master_rect.width//columns # 计算单一帧的宽度
self.frame_height = self.master_rect.height//row # 计算单一帧的高度
self.rect = 0, 0, self.frame_width, self.frame_height # 更新rect
self.columns = columns # 存储列的值(用以后续计算)
self.last_frame = row * columns - 1 # 计算是有0开始的,需要 -1
def update(self, screen):
self.frame += 1 # 帧序号 +1
if self.frame > self.last_frame:
self.frame = self.first_frame # 循环播放
if self.frame != self.old_frame:
frame_x = (self.frame % self.columns) * self.frame_width # 计算 subsurface 的 x 坐标
frame_y = (self.frame // self.columns) * self.frame_height # 计算 subsurface 的 y 坐标
rect = Rect(frame_x, frame_y, self.frame_width, self.frame_height) # 获取subsurface 的 rect
self.image = self.master_image.subsurface(rect) # 更新self.image
self.old_frame = self.frame # 更新self.old_frame
screen.blit(self.image, self.rect) # 显示图像
构造函数建立和初始化了一些需要用到的变量,load()函数用来载入图标,并计算帧的大小,接着update()函数就可以不断的更新像素了,接下来我们测试一下这个类,完整代码如下:
# 导入模块
import pygame
from pygame.locals import *
from sys import exit
# 初始化部分
pygame.init()
# 设置游戏窗口
screen = pygame.display.set_mode((640,480))
pygame.display.set_caption("My Game Window")
background = pygame.image.load("background_640x480.jpg").convert()
# 坦克精灵类
tank_image = pygame.image.load('tank.png').convert_alpha()
cannon1_image = pygame.image.load('cannon_1.png').convert_alpha()
class HeroTank(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.tank_image = tank_image
self.cannon1_image = cannon1_image
self.tank_rect = self.tank_image.get_rect()
self.cannon1_rect = self.cannon1_image.get_rect()
self.speed = 8
def moveLeft(self):
if self.tank_rect.left > 0:
self.tank_rect.x -= self.speed
self.rotate(270)
def moveRight(self):
if self.tank_rect.right < 640:
self.tank_rect.x += self.speed
self.rotate(90)
def moveUp(self):
if self.tank_rect.top > 0:
self.tank_rect.y -= self.speed
self.rotate(180)
def moveDown(self):
if self.tank_rect.bottom < 480:
self.tank_rect.y += self.speed
self.rotate(0)
def rotate(self, angle):
# 选择机身
self.tank_image = pygame.transform.rotate(tank_image, angle)
self.tank_rect = self.tank_image.get_rect(center=self.tank_rect.center)
# 旋转炮筒
self.cannon1_image = pygame.transform.rotate(cannon1_image, angle)
self.cannon1_rect = self.cannon1_image.get_rect(center=self.cannon1_rect.center)
def display(self, screen):
screen.blit(self.tank_image, self.tank_rect)
self.cannon1_rect.center = self.tank_rect.center
screen.blit(self.cannon1_image, self.cannon1_rect)
# 子弹图片
explosion01_image = pygame.image.load('explosion_01.png').convert_alpha()
explosion01_rect = explosion01_image.get_rect()
# 子弹精灵类
class Explode(pygame.sprite.Sprite):
def __init__(self):
self.frame_width = 0
self.frame_height = 0
self.rect = 0,0,0,0
self.columns = 0
self.last_frame = 1
self.last_time = 0
self.frame = 0
self.first_frame = 0
self.old_frame = 0
def load(self, filename, row, columns):
self.master_image = pygame.image.load(filename).convert_alpha() # 载入整张图片
self.master_rect = self.master_image.get_rect() # 获取图片的rect值
self.frame_width = self.master_rect.width//columns # 计算单一帧的宽度
self.frame_height = self.master_rect.height//row # 计算单一帧的高度
self.rect = 0, 0, self.frame_width, self.frame_height # 更新rect
self.columns = columns # 存储列的值(用以后续计算)
self.last_frame = row * columns - 1 # 计算是有0开始的,需要 -1
def update(self, screen):
self.frame += 1 # 帧序号 +1
if self.frame > self.last_frame:
self.frame = self.first_frame # 循环播放
if self.frame != self.old_frame:
frame_x = (self.frame % self.columns) * self.frame_width # 计算 subsurface 的 x 坐标
frame_y = (self.frame // self.columns) * self.frame_height # 计算 subsurface 的 y 坐标
rect = Rect(frame_x, frame_y, self.frame_width, self.frame_height) # 获取subsurface 的 rect
self.image = self.master_image.subsurface(rect) # 更新self.image
self.old_frame = self.frame # 更新self.old_frame
screen.blit(self.image, self.rect) # 显示图像
my_tank = HeroTank()
my_explode = Explode()
my_explode.load('explosion_01.png', 4, 8)
framerate = pygame.time.Clock()
while True:
framerate.tick(30)
for event in pygame.event.get():
if event.type == QUIT:
exit()
key_press = pygame.key.get_pressed()
if key_press[K_w]:
my_tank.moveUp()
elif key_press[K_s]:
my_tank.moveDown()
elif key_press[K_a]:
my_tank.moveLeft()
elif key_press[K_d]:
my_tank.moveRight()
screen.blit(background, (0, 0))
my_tank.display(screen)
my_explode.update(screen,)
pygame.display.update()
运行程序,这里调整了一下绘图顺序,想绘制坦克,再绘制子弹爆炸图像,所以不会遮挡。不过我们还是移开坦克,测试一下效果:
我们发现它会不停的循环播放,这是upadte()函数决定的,当播放到最后一帧的时候,序列号自动复位为第一帧。而且是在(0,0)处播放,这是因为我们在初始化该类的rect的时候,只写入了计算后宽度和高度,而x,y的值保持0。
最后是本章资源的下载链接:
链接:资源下载
提取码:234g