找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 1194|回复: 2

[教程] 【演出组件】影像图片(时间切片问题、帧动画音声问题)

[复制链接]
发表于 2025-8-29 06:17:09 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
本帖最后由 Maz马 于 2026-8-30 22:48 编辑

26.8.29 更新

基于后文需求,新写了一个更为舒适封装内聚的组件

功能
1.能够手动重启的动画
2.同时提供了播放音声资源的功能,这个功能是基于“能够在image的序列帧动画里对应播放声音,而不用外部匹配计算”的想法。

[RenPy] 纯文本查看 复制代码
# ---------- 影像图片 ----------
init python:
    """
    Movie Image (Ver 1.0 Ren'py 8.3.6)
    Author: Maz
    """
    class MovieImage(renpy.Displayable):
        def __init__(self, frames, sounds=None, repeat=False, channel="sound", **kwargs):
            super().__init__(**kwargs)
            # 注意计算方式不同
            self.frames = frames     # 图片字典 frames 值 = 该帧持续时间
            self.sounds = sounds     # 音声字典 sounds 值 = 该帧发起时间
            self.repeat = repeat
            self.channel = channel   # 播放通道

            self.sounds_done = set() # 已经发起过的音声

            # 初始化标记
            self.inited = False

            # 组件尺寸
            self.w = 0
            self.h = 0
            
            # 时间轴
            self.total_time = sum(float(d) for d in self.frames.values()) # 动画时间
            self.now_time = 0                                             # 自体时间
            self.old_time = 0                                             # 上帧时间
            self.delta_time = 0                                           # 渲染间隔

        def restart(self):
            self.inited = False
            self.sounds_done = set()
            renpy.redraw(self, 0)
        def render(self, width, height, st, at):
            # 首次/重显 初始化
            if not self.inited or st < self.old_time:
                # 初始化渲染属性
                first = list(self.frames)[0]
                self.w, self.h = renpy.render(renpy.displayable(first), width, height, st, at).get_size()
                # 初始化时间属性
                self.delta_time = 0
                self.now_time = 0
                self.old_time = st
                self.inited = True
            else:
                # 自维护时间属性
                self.delta_time = st - self.old_time
                self.now_time += self.delta_time
                self.old_time = st
            
            render = renpy.Render(self.w, self.h)
            
            # 音效时间戳:到点且未播过才发起(sounds_done 防重复发起)
            t = 0.0
            for i, (name, dt) in enumerate(self.sounds.items()):
                t += float(dt)
                if self.now_time >= t:               # 已到触发点
                    cur_sound = name                 # 当前应发起的音效
                    if i not in self.sounds_done:    # 发起前判断:未播过才播
                        renpy.play(cur_sound, channel=self.channel, if_changed=True)
                        self.sounds_done.add(i)
            
            # 图片时间戳
            cur_img = None
            t = 0.0
            for name, dt in self.frames.items():
                if self.now_time < t + dt:
                    cur_img = name
                    break
                t += dt
            if cur_img is None:
                cur_img = list(self.frames)[-1]   # 超时用末帧
            
            render.blit(renpy.render(renpy.displayable(cur_img), self.w, self.h, st, at), (0, 0))

            # 循环重播
            if self.repeat and self.now_time >= self.total_time:
                self.restart()
            # 动画期间才更新
            if self.now_time < self.total_time:
                renpy.redraw(self, 0)
            return render

# 使用方法:
#
# 可以当成 image 显示,也可以在 screen 里直接 add,以下是参数含义
#
# frames   图片字典,格式是 {帧图: 该帧持续秒}
# sounds   音效字典,格式是 {音效: 播放时间戳}
# repeat   循环播放  默认 False)
# channel  播放通道  默认 "sound")
#
# 调用实例的 xx.restart() 即可重置动画
#
# image xx = MovieImage(
#     frames={"帧A": 0.06,
#             "帧B": 0.06,
#             "帧C": 0.06}, 
#
#     sounds={"音效A": 0.5,
#             "音效B": 0.3}, 
#     repeat=True,
#     channel="sound")


最开始的讨论点,仅记录,下文算是思路分享,新的组件已经更内聚,适用性更好了

[RenPy] 纯文本查看 复制代码
#用来刷新动画时间轴入点的动态可视组件
init python:
    #假设,image循环时间为2,当st为2.3时切换出image,image会处于2.3的循环位置,而不是在2.3重新唤起循环,这是renpy的图像时间轴机制
    #注意,这里说的是这个组件的显隐,而不是renpy线程下的调用,一般情况的调用,是会重新创建组件,重置st的
    #可以把这里的显隐理解为一张图片能否看到,但无论能否看到,实际都在后台存在,并未真正释放
    #因此需要让image时间轴和提取动画的时间轴相对异步,可以查询动画时间切片和滑窗算法来理解
    #
    # 用来操作动画切片入点的全局变量
    start_anim = 0.0

    # 重置帧动画入点的函数
    def reset_animation():
        global start_anim
        start_anim = 0.0

    # 帧动画函数
    def act_animation(st,at,frames,repeat=None):
        global start_anim
        # 初始化
        if start_anim == 0.0:
            start_anim = st   # 记录传入时间轴的当前时间(类似于剪辑中的入点)
        index = 0             # 帧序列
        meter_time = 0.0      # 当前动画帧播放的时间
        total_duration = sum(frame_time for (frame, frame_time) in frames)  # 计算动画总时长

        # 动画已播放时间 = 当前时间 - 动画开始时间(类似于剪辑中的出点)
        frame_end_time = st - start_anim

        # 如果不重复播放且动画已播放完毕
        if not repeat and frame_end_time >= total_duration:
            image, _ = frames[-1]  # 返回最后一帧
            return image, None    # 返回None表示不再更新

        # 如果重复播放,计算循环后的剩余时间
        if repeat:
            frame_end_time = frame_end_time % total_duration

        # 关键难点,函数本身是动态可视组件的循环调用,这里又进行循环,因此是循环嵌套

        for i,(frame,frame_time) in enumerate(frames):                # 因为用的是同一个时间轴,所以frame_end_time在这个循环中是另一种含义

            if frame_end_time >= frame_time:                          # “当前帧已播放时间”大于当前帧持有的时间时
                frame_end_time = (frame_end_time - frame_time)        # 减去当前帧持有的时间,相对化成下一帧的“当前帧已播放时间”,继续遍历

            else:                                                     # “当前帧已播放时间”小于当前帧持有的时间时

                index = i                                             # 记录当前帧的索引
                meter_time = frame_time - frame_end_time              # 用当前帧持有的时间减去遍历中的“当前帧已播放时间”
                break                                                 # 得到 当前帧索引 和 下一次调用这个函数的间隔

        # 当再次调用这个函数时,循环块外部的frame_end_time的含义会重新初始化成“动画已播放时间”
  
        image,_= frames[index]  # 获取当前帧的图像(忽略帧时间,用_接收)

        return image,meter_time # 返回 当前图像 和 下次更新时间/当前帧剩余时间

#使用方法


# 抬头使用DynamicDisplayable()动态可视组件
#
# 入参repeat=True,是否循环。frames是一个帧序列表,每个元素是一个元组 (帧的路径,这一帧播放的时间)

image mon_walk_left:
    DynamicDisplayable(
    act_animation,repeat=True,
    frames=[
        ("images/anim/mon_1/walk/l0.png",0.05),
        ("images/anim/mon_1/walk/l1.png",0.05),
        ("images/anim/mon_1/walk/l2.png",0.05)
    ])


正题:
为什么我要写这个东西而不是使用image原生的写法,看起来有点画蛇添足?
[RenPy] 纯文本查看 复制代码
image mon_walk_left:
    "images/anim/mon_1/walk/l0.png"
    pause 0.1
    "images/anim/mon_1/walk/l1.png"
    pause 0.1
    "images/anim/mon_1/walk/l2.png"
    pause 0.1
    repeat

这里涉及到一个时间戳的问题,是我在写横板格斗的cdd轮子时出现的
所有的可视组件都会有隐含的参数st,at,也就是可视组件被显示的时间以及持续的时间
这里简单形容它为“时间入点”和“时间出点”(pr中的概念)



image本身的接口并不允许你调整时间戳,所以当你在展示这个循环动画并切换它时(也就是没有隐藏,仅仅是不显示)
image的时间轴仍然在执行,用图表展示就是:
[RenPy] 纯文本查看 复制代码
#时间轴
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
#假设你的动画在第3帧开始显示,那么这是你动画轴的位置,1234则是你循环的动画帧
    [1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,]
#假设你没有隐藏它重新显示,那么时间入点仍旧是你第一次显示这个组件的st,你只是在间断的切换它,那么实际上取得值是这样的
    [1,2,3,4]   [3,4,1,2]   [1,2,3,4]

可以看到,并不是动画重新开始,而是你截取了循环动画的一部分。[3,4,1,2]

而花里胡哨的CDD常常以常驻的形式展示在游戏中,当然,在其他情况下也有可能出现持续显示的可视组件
因此这会导致循环动画的失效或是不匹配,而这个帧动画的动态可视组件,就是提供了重新调整st的方法(并不是实际st而是相对的调整,这是时间切片的概念)
[RenPy] 纯文本查看 复制代码
reset_animation()

当你需要调整循环动画的st时,并不需要重新唤起可视组件,而是调用这个函数,那么循环动画就会重新计算
用图表展示就是:
[RenPy] 纯文本查看 复制代码
#时间轴
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
#
    [1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,]
#
    [1,2,3,4]   [3,4,1,2]   [1,2,3,4]
#当你开始截取片段时,调用函数,会相对调整st
    [1,2,3,4,1,][1,2,3,4,1,][1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4,]
#此时你截取的片段为
    [1,2,3,4]   [1,2,3,4]   [1,2,3,4]


#查看我写的更多屎

时间切片的帧动画动态可视组件
https://www.renpy.cn/forum.php?mod=viewthread&tid=1740

在回放中影响当前游戏存档数据的方法
https://www.renpy.cn/forum.php?mod=viewthread&tid=1739

她的心里话!隐藏文本!
https://www.renpy.cn/forum.php?mod=viewthread&tid=1713

能力雷达图
https://www.renpy.cn/forum.php?mod=viewthread&tid=1719

暴学CDD的十四天/附虚拟摇杆
https://www.renpy.cn/forum.php?mod=viewthread&tid=1675

简单rpg地图移动
https://www.renpy.cn/forum.php?mod=viewthread&tid=1652

摇骰子小游戏
https://www.renpy.cn/forum.php?mod=viewthread&tid=1653

猜球盅小游戏
https://www.renpy.cn/forum.php?mod=viewthread&tid=1574

FontGroup() 游戏里全局使用两种字体的方法
https://www.renpy.cn/forum.php?mod=viewthread&tid=1491

粉身碎骨浑不怕,要留答辩在人间





评分

参与人数 1活力 +300 干货 +3 收起 理由
烈林凤 + 300 + 3 感谢分享!

查看全部评分

发表于 2025-8-29 08:38:31 来自手机 | 显示全部楼层
老马好强!
回复

使用道具 举报

发表于 2026-8-16 17:11:35 | 显示全部楼层
太好了,我刚好需要这个
回复 支持 抱歉

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|RenPy中文空间 ( 苏ICP备17067825号 )

GMT+8, 2026-9-2 09:53 , Processed in 0.039264 second(s), 10 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表