Ren'Py引擎从入门到放弃(支线9)——使用CDD播放序列帧动画
世上无难事,只要肯放弃。
支线系列是独立于基础之外的内容,感觉有困难的同学可以暂时不(fang)看(qi)。
第一个问题:什么是序列帧动画?
答:序列帧动画的本质就是……动画:通过快速切换画面(帧),使观众产生画面内容在运动的感觉。
序列帧动画是大多数游戏引擎都具备的功能,Ren'Py也不例外。Ren'Py中最基础的序列帧播放方式,是使用ATL定义图像(Image)对象。例如,我们有一个8帧的动画序列,文件名分别为 seq_1.png ~ seq_8.png。那么我们使用这8张图片定义一个每秒8帧的动画往往这样写:
image SomeAnima:
"seq_1.png"
pause 0.12
"seq_2.png"
pause 0.12
"seq_3.png"
pause 0.12
"seq_4.png"
pause 0.12
"seq_5.png"
pause 0.12
"seq_6.png"
pause 0.12
"seq_7.png"
pause 0.12
"seq_8.png"
pause 0.12
repeat这样写的优点是清晰易懂,缺点是……非常蛋疼,不对,冗余信息过多。当序列数量不多时,直接列出所有序列图片还可以接受,如果序列数量有几十甚至上百,这种写法效率就很低下。
绝大多数序列帧动画使用的图片文件,往往具有相同的前缀和相同的播放时间(帧率),只需要根据某个递增的序列号顺序显示对应图片即可。所以我们可以使用CDD更高效地实现序列帧动画。
第二个问题:什么是CDD?
答:CDD全称是Creator-Defined Displayable,创作者定义的可视组件,本质是Ren'Py提供的一个基类(超类)和一组API。
详情可参考文档:
renpy.Displayable 是CDD是核心基类,所有CDD都必须是它的派生类。创建的CDD中必须包含 render 方法,返回类型是 renpy.Render (或其派生)类的实例。当然,如果没有 render 方法在语法上不会报错,只是不显示任何图像而已……event方法用于处理pygame事件,visit方法访问CDD子组件时返回子组件列表,播放序列帧动画时不需要。至此,我们可以实现一个空的CDD了:
init python:
class SquenceAnimator(renpy.Displayable):
def __init__(self, **kwargs):
super(SquenceAnimator, self).__init__(**kwargs)
def render(self, width, height, st, at):
render = renpy.Render(0, st, at)
renpy.redraw(self, 0)
return render
我们可以定义这个CDD的一个实例,并使用 show 语句显示(尽管实际不会显示任何内容):
image null_seq = SquenceAnimator()
label start:
show null_seq在 SquenceAnimator 实现具体功能之前,还需要先设计支持的“序列帧命名规范”,也就是图片文件名的规则。我们这里采用最简单的一种规则:
完整文件名 = 前缀 + 分隔符 +序列号 + 文件扩展名
举例来说,在第一个问题中提到的“seq_1.png~seq_8.png”,“seq”是前缀,下划线“_”是分隔符,数字1到8是序列号,“png”是文件扩展名。除了文件扩展名由Ren'Py预处理外,其他3项内容就是 SquenceAnimator 的3种必备入参。序列帧应是两个整型入参,或者一个二元元组,分别表示起始帧序列号和结束帧序列号(此处我使用的是2个整型入参)。我们有了这4个入参就可以找到符合规范的一组图片:
init python:
class SquenceAnimator(renpy.Displayable):
def __init__(self, prefix, separator, begin_index, end_index, **kwargs):
super(SquenceAnimator, self).__init__(**kwargs)
# 前缀
self.prefix = prefix
# 分隔符
self.separator = separator
# 起始帧序列号
self.begin_index = begin_index
# 结束帧序列号
self.end_index = end_index
# 序列帧长度
self.length = end_index - begin_index + 1
# 可视组件列表
self.sequence = []
for i in range(begin_index, end_index+1):
# 将前缀、分隔符和序列号拼接,对应名称的可视组件顺序添加到列表中
self.sequence.append(renpy.displayable(self.prefix + self.separator + str(i)))
# 当前帧在可视组件列表中的索引
self.current_index = 0
# 关键帧时间轴
self.show_timebase = 0找到图片并放置到可视组件列表中后,我们接着就可以在 render 方法中根据时间渲染对应的可视组件了:
def render(self, width, height, st, at):
# 暂时默认每帧持续时间0.12秒
interval = 0.12
if (st >= (self.show_timebase + interval)):
self.show_timebase = st
self.current_index += 1
if self.current_index >= self.length:
# 默认循环播放,将可视组件列表索引归零
self.current_index = 0
# 此处默认所有序列帧图片都具有相同尺寸
render = renpy.render(self.sequence[self.current_index], width, height, st, at)
renpy.redraw(self, 0)
return render此处可以先测试一下,是否可以播放序列帧动画。
回到上面的代码,每帧持续时间定死了0.12秒,并且是循环播放。如果需要自己定义每帧持续时间并控制是否循环,就需要给 SquenceAnimator 的构造函数添加两个入参,同时修改一下可视组件列表索引达到最大值的逻辑:
init python:
class SquenceAnimator(renpy.Displayable):
def __init__(self, prefix, separator, begin_index, end_index, interval, loop=True, **kwargs):
super(SquenceAnimator, self).__init__(**kwargs)
self.prefix = prefix
self.separator = separator
self.begin_index = begin_index
self.end_index = end_index
self.length = end_index - begin_index + 1
self.sequence = []
for i in range(begin_index, end_index+1):
self.sequence.append(renpy.displayable(self.prefix + self.separator + str(i)))
self.current_index = 0
self.show_timebase = 0
self.interval = interval
self.loop = loop
def render(self, width, height, st, at):
if (st >= (self.show_timebase + self.interval)):
self.show_timebase = st
self.current_index += 1
if self.current_index >= self.length:
if self.loop:
self.current_index = 0
else:
self.current_index = self.length - 1
render = renpy.render(self.sequence[self.current_index], width, height, st, at)
renpy.redraw(self, 0)
return render实例化并使用:
image SeqAnima = SquenceAnimator("seq", "_", 1, 8, 0.12)
label start:
show SeqAnima at truecenter第三个问题:有些序列帧动画会合并为一张图片,该如何处理?
答:单张图片的序列帧动画,本质是一个网格,序列索引通常是按行读取,从左往右顺序排列。比如一个8帧动画,图片可能是一个2×4的网格,顺序示意如下(开头的数字表示序列号,括号里的数字表示行、列的索引号):
┌────────┬────────┐
│ 1(0,0) │ 2(0,1) │
├────────┼────────┤
│ 3(1,0) │ 4(1,1) │
├────────┼────────┤
│ 5(2,0) │ 6(2,1) │
├────────┼────────┤
│ 7(3,0) │ 8(3,1) │
└────────┴────────┘因此,CDD的构造函数入参需要做修改,不再是图片索引编号,而是行数和列数。接着根据图片大小和行数、列数,计算单帧图像的宽、高,最后用Crop切割并顺序加入到图像列表中。render方法不需要做修改,可以沿用 SquenceAnimator 的:
init python:
class SquenceAnimator2(renpy.Displayable):
def __init__(self, im, row, column, interval, loop=True, **kwargs):
super(SquenceAnimator2, self).__init__(**kwargs)
# im入参是字符串,需要转为Image对象,获取尺寸信息
self.im = Image(im)
self.size = renpy.image_size(self.im)
# 行数
self.row = row
# 列数
self.column = column
# 单帧宽度
self.frame_width = self.size[0] / column
# 单帧高度
self.frame_height = self.size[1] / row
# 序列帧长度
self.length = row * column
self.sequence = []
# 循环嵌套切割单帧图像
for i in range(row):
for j in range(column):
self.sequence.append(Crop((self.frame_width*j, self.frame_height*i, self.frame_width, self.frame_height), self.im))
self.current_index = 0
self.show_timebase = 0
self.interval = interval
self.loop = loop
#省略render()可以使用下图测试(知乎直接上传带透明通道的png会转成纯白的jpg,所以加了黑色背景):

测试代码:
image stars2 = SquenceAnimator2("sequence_stars.png", 5, 6, 0.07)
label start:
show stars2 at truecenterSquenceAnimator2 还有点缺陷。有些序列图的最后一行可能会有空白帧,也就是说“序列总数 < 行数 × 列数”。这就需要添加一个构造函数入参,指定序列总数。render方法依然不用修改:
class SquenceAnimator2(renpy.Displayable):
def __init__(self, im, row, column, interval, seq=0, loop=True, **kwargs):
super(SquenceAnimator2, self).__init__(**kwargs)
self.im = Image(im)
self.size = renpy.image_size(self.im)
self.row = row
self.column = column
self.frame_width = self.size[0] / column
self.frame_height = self.size[1] / row
# 如果seq为默认值,则序列总数依然是行数×列数
if(seq == 0):
self.length = row * column
# 根据seq入参决定序列总数
else:
self.length = seq
self.sequence = []
for i in range(row):
for j in range(column):
# 如果超过序列总数则不再向列表中添加元素
if((i * column + j + 1) <= self.length):
self.sequence.append(Crop((self.frame_width*j, self.frame_height*i, self.frame_width, self.frame_height), self.im))
else:
break
self.current_index = 0
self.show_timebase = 0
self.interval = interval
self.loop = loop
#省略render()测试代码:
image stars2 = SquenceAnimator2("sequence_stars.png", 5, 6, 0.07, seq=28)
label start:
show stars2 at truecenter至此,设计CDD播放序列帧动画的目标基本实现了。在少数应用场景下,存在图片四周有留白,或者各帧之间还有空隙的情况,我们在SquenceAnimator2的基础上做一点点修改就可以。
补充部分:
在遇到某些经常使用的特效,但引擎实时绘制特别困难的情况,序列帧动画是非常有效的实现方案。需要注意序列帧动画会占用Ren'Py的缓存,不适合尺寸较大的图像。角色和UI等画面元素比较合适。
