找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 32|回复: 0

[教程] 高精度流程进度条与章节回放/选择功能(renpy进阶学习经验六)

[复制链接]
发表于 8 小时前 | 显示全部楼层 |阅读模式

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

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

×
本帖最后由 烈林凤 于 2024-10-23 00:26 编辑

本来不打算把这两个功能加进“renpy进阶学习经验”里的,但这种极为实用的功能不加入这个系列多少有点可惜,于是想想还是加进来了。

这个教程的课题早在10月10日便在交流群里提出了,当天晚上@Gemini菖蒲 便用gpt写了一个找出label中所有say语句数量的方法(ai难得写出正确的代码),为课题的攻破奠定基础,非常感谢!
10月17日我便将代码正式撰写完毕,课题也完全告破,但因为种种事情耽搁到现在才想起来写()
感谢@Aaron栩生阿龙 、@Gemini菖蒲 等各位交流群群u们提供的建议和想法!万分感谢!!!
如果需要转载本帖,请附上本人名称“烈林凤”,严禁将本帖中的代码或本帖以任何形式进行售卖,非商用场合无需通知本人或署名,直接使用就好,若需嵌入付费项目,请与我联系。
关于本帖中有任何不理解的地方都可以留言,但请先尝试运行之后再提问,谢谢!
我所使用的是8.2.1版本的renpy,请尽量使用高版本,谢谢!
接下来进入正题——


本期的主题为“高精度流程进度条与章节回放/选择功能(renpy进阶学习经验六)”
主要使用了“replay”和“renpy.is_seen”两个功能相关的代码
文档——
画廊、音乐空间和场景回放 — Ren'Py 中文文档
其他函数和配置变量 — Ren'Py 中文文档

若在测试本教程代码时发现遇到了bug,请先在renpy启动器中“删除持久化数据”并“强制重新编译”!再重新进行测试!

所有代码如下——
[RenPy] 纯文本查看 复制代码
## 设定一个持久化数据,用于记录label名称和say语句数量
default persistent.read_label_say_count = {"test_label":0,"test_label_1":0}

init python:

    def get_label_say_count_max(label_name=[]):
        '''获取label中say语句数量的最大值'''
        label_say_count_max = {}
        for i in label_name:
            ## 将label的say语句数量初始化为0
            label_say_count_max[i] = 0
            ## 遍历label名称中含有的所有节点,获取say语句数量
            for node in renpy.game.script.lookup(i).block:
                ## 如果节点是say语句,则将say语句数量+1
                if isinstance(node, renpy.ast.Say):
                    label_say_count_max[i] += 1
        ## 返回label名称和say语句数量的字典
        return label_say_count_max

    def get_read_label_say_count(label_name=None):
        '''获取当前label名称和say语句数量'''

        current_label = []
        current_label_name = ""

        ## 如果翻译文件标识符存在,且当前语句没有被阅读过
        if label_name and renpy.is_seen(ever=True) == False:
            ## 以下划线为间隔符,将获取到的l翻译文件标识符分割为列表
            current_label = label_name.rsplit("_")
            ## 若翻译文件标识符末尾是正整值(相同的语句末尾会额外添加出现的次数)
            if current_label[-1].isdigit():
                current_label.pop()
                current_label.pop()
            else:
                current_label.pop()
            ## 重新组合翻译文件标识符,拼合成当前label名称
            current_label_name = '_'.join(current_label)
            ## 若当前label名称存在于字典中,则将say语句数量+1
            if current_label_name in persistent.read_label_say_count:
                persistent.read_label_say_count[current_label_name] += 1

screen count_say_lines_in_label_screen():

    ## 将其改为和主菜单相同的UI风格
    tag menu
    style_prefix "count_say_lines_in_label_screen"
    if main_menu:
        add gui.main_menu_background
    else:
        add gui.game_menu_background
    frame:
        style "game_menu_outer_frame"
    use navigation

    textbutton "返回":
        align(0.9, 0.9)
        action Return()

    ## 获取所需的label名称和say语句数量,将label名和所需章节名一一对应
    default label_say_count_max = get_label_say_count_max(label_name=['test_label','test_label_1'])
    default label_name_dict = {"test_label":"测试章节","test_label_1":"测试章节2"}

    vbox:
        xycenter (0.5, 0.5)
        spacing 20
        for i in label_name_dict.items():
            vbox:
                hbox:
                    xsize 500
                    text "[i[1]]章节进度" align (0.0, 0.5)
                    ## 若当前没有在回放当中,并且当前label名称的say语句数量等于最大值,则显示开始回放按钮
                    if (_in_replay == None) and (persistent.read_label_say_count[i[0]] == label_say_count_max[i[0]]):
                        textbutton "开始回放":
                            align(1.0, 0.5)
                            action Replay(i[0], scope={}, locked=None)
                    ## 若当前在回放当中,则显示结束回放按钮
                    elif _in_replay == i[0]:
                        textbutton "结束回放":
                            align(1.0, 0.5)
                            action EndReplay(confirm=True)
                    else:
                        textbutton "不可回放":
                            align(1.0, 0.5)
                            action NullAction()

                hbox:
                    xycenter (0.5, 0.5)
                    bar:
                        xysize (500, 30)
                        value persistent.read_label_say_count[i[0]]
                        range label_say_count_max[i[0]]
                    ## 显示当前label名称的say语句已读数量与最大值
                    text str(persistent.read_label_say_count[i[0]]) + "/" + str(label_say_count_max[i[0]]) size 25

label ceshi_24:
    "111在test_label中有个say语句。"
    "222在start_label中有个say语句。"
    jump test_label

label test_label:
    "This is a test label."
    "This is another test label."
    "This is a third test label."
    "test1"
    "test2"
    "test3"
    "123"
    $ renpy.end_replay()
    jump test_label_1

label test_label_1:
    "This is a test label."
    "This is another test label."
    "This is a third test label."
    "test11"
    "test22"
    "123"
    $ renpy.end_replay()
    return





接下来进行详解——
首先是第一个是如何实现高精度流程进度条
在这一部分中,首先最困难的便是如何获取到label语句下所有say语句的数量
如果只是以label是否被看完作为进度,那无需多考虑,只要通过renpy.seen_label()方法来判断label是否被执行过即可。

但是,如果需要以say语句是否被看过作为进度,那就非常麻烦了,因为renpy并没有原生的方法可以直接获取到所有label的所有say语句
这时候就需要调用一个内部方法——renpy.game.script.lookup()
这个方法来源于源码,文档中没有任何相关的描述,各位只需要知道,可以通过该方法可以获取到label中所有say语句数量即可
定义一个名为“get_label_say_count_max”的函数,用于获取label中say语句数量的最大值,入参为需要查询的所有label的名称的列表,遍历该label名称列表,调用renpy.game.script.lookup()方法,如果是say语句则将键值对的值+1
最后将字典返回,便完成了获取label中所有say语句数量的过程。
注意!这是源码的内部函数,以后renpy启动器更新可能会废除或改写该方法,更有可能编写成原生接口!若遇到8.4+版本及以上出现该处报错的情况,请及时留言联系我!

至此,便完成了该功能最困难的一步
其他的部分均可以通过原生的方法实现,后续一路畅通

接着我们来解决如何获取当前已读多少say语句的问题
这个问题其实可以分为两部分——获取需要记录的label名称和label下有多少say语句被已读
此时此刻,可能会有人问了:帖主帖主,是不是要使用renpy.count_newly_seen_dialogue_blocks()或者renpy.count_seen_dialogue_blocks()了啊?将所有已读say语句数量减去不需要的label中所有的say语句数量。
nonono,如果使用这两个方法,会产生问题——
1.比如说你不希望start等特殊label或其他自定义的label中的已读的say语句也被记录其中 ,那你必须将这些label的say语句数量全部获取一遍,再减去用该方法获取到的值
2.还有,已读say语句的数量可能为0,而减去的这些label中所有say语句数量,可能会导致最后的值为负数,这可能会导致额外的代码作为补丁才能使用,否则可能会导致后续功能报错(之后会讲到)
3.再然后,减去后获取到的值无法直接按label进行划分,导致我们需要大量额外的代码作为补丁进行章节划分
简单点来说,会导致代码维护困难、拓展性降低、堆砌成史山代码()

此时,我们隆重引进一个renpy原生方法——renpy.get_translation_identifier()
该方法会获取并返回返回当前语句的翻译文件标识符。
例如在label_test中的某个不重复的say语句,返回的参数大致如下——
test_label_db972873
但如果是重复的say语句,可能会返回如下参数——
test_label_8836ffa1_2
其中前面的test_label便是我们需要的内容,那该如何舍去后面的say语句编号呢?
这就需要调用python的rsplit方法了,从后往前查找下划线(“_”),以下划线为间隔分成成列表
最后用python的join方法将所需的字符串拼接在一起即可。

在此要额外穿插说明一下接下来会用到的一个字典——persistent.read_label_say_count
这个字典内储存着我们需要的所有label名称,与label对应的已读的say语句
同时,这个变量没有写在init python当中,而是单独写了一个default,因为它使用了持久化数据persistent,保证游戏开启时不会将我们记录的数据清除

让我们将这些整合到一个名为get_read_label_say_count的函数当中——
这个函数有一个入参,是当前语句的翻译文件标识符。
因为当前语句有可能不是say语句,会返回None,因此需要用if判断获取到的值是否存在,且使用renpy原生方法renpy.is_seen判断当前语句是否被看过
通过判断后,将获取到的翻译文件标识符以下划线为间隔,将字符全部存进current_label列表当中
判断列表最后的一个元素是否为整值,这里使用了python的isdigit方法,如果是整值则代表末尾存在重复次数的标识。
若不是整值则只需要去掉一次最后的元素,即可得到保存了正常label名称的列表,如果是整值则需要去掉两次即可。
通过join方法,将列表的所有元素以下划线为间隔再次组成字符串,便得到了最终需要使用的参数——当前label的名称
最后,我们判断该label是否是我们需要的(即是否存在字典当中),如果是需要的,则将对应的值+1

在say screen中使用on,当对话出现时便调用该函数,并以renpy.get_translation_identifier()作为入参

至此,我们便完成了最核心的,获取所需label已读say语句数的函数

而想要做进度条的方法则非常简单了,比如说,在自定义的screen中这么写——
[RenPy] 纯文本查看 复制代码
screen bar_screen():
    ## 定义一个列表,调用get_read_label_say_count函数,获取当前label名称和say语句数量
    default label_say_count_max = get_label_say_count_max(label_name=['test_label','test_label_1'])
    for i in label_name_dict.items():
        bar:
            xysize (500, 30)
            value persistent.read_label_say_count[i[0]]
            range label_say_count_max[i[0]]


[RenPy] 纯文本查看 复制代码
init python:

    def get_label_say_count_max(label_name=[]):
        '''获取label中say语句数量的最大值'''
        label_say_count_max = {}
        for i in label_name:
            ## 将label的say语句数量初始化为0
            label_say_count_max = 0
            ## 遍历label名称中含有的所有节点,获取say语句数量
            for node in renpy.game.script.lookup(i).block:
                ## 如果节点是say语句,则将say语句数量+1
                if isinstance(node, renpy.ast.Say):
                    label_say_count_max += 1
        ## 返回label名称和say语句数量的字典
        return label_say_count_max

    def get_read_label_say_count(label_name=None):
        '''获取当前label名称和say语句数量'''

        current_label = []
        current_label_name = ""

        ## 如果翻译文件标识符存在,且当前语句没有被阅读过
        if label_name and renpy.is_seen(ever=True) == False:
            ## 以下划线为间隔符,将获取到的l翻译文件标识符分割为列表
            current_label = label_name.rsplit("_")
            ## 若翻译文件标识符末尾是正整值(相同的语句末尾会额外添加出现的次数)
            if current_label[-1].isdigit():
                current_label.pop()
                current_label.pop()
            else:
                current_label.pop()
            ## 重新组合翻译文件标识符,拼合成当前label名称
            current_label_name = '_'.join(current_label)
            ## 若当前label名称存在于字典中,则将say语句数量+1
            if current_label_name in persistent.read_label_say_count:
                persistent.read_label_say_count[current_label_name] += 1


至此,便完成了本教程的第一部分内容



接下来是第二部分,章节回放/选择功能

这一部分与上面的功能结合起来编写
首先,我们先仿写一个默认菜单的界面——
[RenPy] 纯文本查看 复制代码
screen count_say_lines_in_label_screen():

    ## 将其改为和主菜单相同的UI风格
    tag menu
    style_prefix "count_say_lines_in_label_screen"
    if main_menu:
        add gui.main_menu_background
    else:
        add gui.game_menu_background
    frame:
        style "game_menu_outer_frame"
    use navigation

    textbutton "返回":
        align(0.9, 0.9)
        action Return()

接下来再在screen.rpy中修改两处——
[RenPy] 纯文本查看 复制代码
screen quick_menu():

    ## 确保该菜单出现在其他屏幕之上,
    zorder 100

    if quick_menu:

        hbox:
            style_prefix "quick"

            xalign 0.5
            yalign 1.0
            textbutton _("{size=30}进度") action ShowMenu('count_say_lines_in_label_screen')

[RenPy] 纯文本查看 复制代码
## 导航屏幕 ########################################################################
##
## 该屏幕包含在标题菜单和游戏菜单中,并提供导航到其他菜单,以及启动游戏。

screen navigation():

    vbox:
        textbutton _("进度") action ShowMenu('count_say_lines_in_label_screen')

保证随时可以查看章节游玩情况

之后在自定义的界面中写以下代码——
[RenPy] 纯文本查看 复制代码
    ## 获取所需的label名称和say语句数量,将label名和所需章节名一一对应
    default label_say_count_max = get_label_say_count_max(label_name=['test_label','test_label_1'])
    default label_name_dict = {"test_label":"测试章节","test_label_1":"测试章节2"}

    vbox:
        xycenter (0.5, 0.5)
        spacing 20
        for i in label_name_dict.items():
            vbox:
                hbox:
                    xsize 500
                    text "[i[1]]章节进度" align (0.0, 0.5)
                    ## 若当前没有在回放当中,并且当前label名称的say语句数量等于最大值,则显示开始回放按钮
                    if (_in_replay == None) and (persistent.read_label_say_count[i[0]] == label_say_count_max[i[0]]):
                        textbutton "开始回放":
                            align(1.0, 0.5)
                            action Replay(i[0], scope={}, locked=None)
                    ## 若当前在回放当中,则显示结束回放按钮
                    elif _in_replay == i[0]:
                        textbutton "结束回放":
                            align(1.0, 0.5)
                            action EndReplay(confirm=True)
                    else:
                        textbutton "不可回放":
                            align(1.0, 0.5)
                            action NullAction()

                hbox:
                    xycenter (0.5, 0.5)
                    bar:
                        xysize (500, 30)
                        value persistent.read_label_say_count[i[0]]
                        range label_say_count_max[i[0]]
                    ## 显示当前label名称的say语句已读数量与最大值
                    text str(persistent.read_label_say_count[i[0]]) + "/" + str(label_say_count_max[i[0]]) size 25


强烈推荐各位使用“章节回放”而不是“章节选择”!!!
章节回放不可以存档,且独立于上下文,可以随时进行播放且不影响当前进程,可以随时退出,无任何副作用,相比“章节选择”,可以有效避免玩家通过这种方式刷结局、成就等!
在此引用文档的中的话——
场景回放也可以使用 Start() 行为。这两种模式的差别如下:

回放可以从任何界面启动,而Start只能使用在主菜单或者主菜单显示的界面。

当回放结束,主控流程会回到回放启动的点。那个点可能是在主菜单或者游戏菜单中。如果某个游戏运行过程中调用了回放,游戏状态是会被保留。

在回放模式下禁用存档。重新加载由于需要存档,也是禁用的。

在回放模式下,调用 renpy.end_replay() 会结束回放。在普通模式下,renpy.end_replay()不产生任何效果。

画廊、音乐空间和场景回放 — Ren'Py 中文文档

若需要使用章节回放,则要在label的末尾处(在jump或call跳转之前),使用renpy.end_replay(),写以下代码——
[RenPy] 纯文本查看 复制代码
$ renpy.end_replay()

这样,当回放运行到这一段时就会返回到原本的界面,且此代码对于正常游玩模式没有影响。

想要运行“章节回放”功能,需要在按钮上使用Replay()行为,第一个入参是你想要回放的label,比如说——
[RenPy] 纯文本查看 复制代码
textbutton "开始回放":
    align(1.0, 0.5)
    action Replay("test_label")

这样,点击按钮后变会从test_label处开始回放
再通过_in_replay来判断按钮显示文本和所使用的行为(开始回放Replay(),结束回放EndReplay(),不可回放/无行为NullAction()

如果是想要做“章节选择”功能,则只需要把Replay()换成Start()即可,第一个入参是你想要运行的label,比如说——
[RenPy] 纯文本查看 复制代码
textbutton "选择章节":
    align(1.0, 0.5)
    action Start("test_label")

若是使用章节选择,便不必再在label末尾处写上renpy.end_replay()了

结合之前写的“高精度流程进度条”,加入自身所需的功能,便能得到这个——
QQ截图20241022235508.jpg
若有非常多的章节,则在最外层的vbox外再套个viewport即可(实现上下滚动的效果)

至此,章节回放/选择功能的部分也全部讲完了




呀,努力写了一个晚上的教程,终于肝完了,这篇教程估计是本系列最详细,且目前难度最高的一篇了
“renpy进阶学习经验”从一年前更新到现在,我的技术飞速增长,已经到了不用python就活不下去的程度了,所以说想要遵循以前的想法“尽量不使用太难懂的python内容”多少有点不太现实了()
不过说到底,这些不过都是我学习时发现的经验而已嘛!觉得挺不错且实用的便写出一篇篇详细教程(懒得写详细教程的或者没有什么好讲的都叫“xxx的解决方法”)
今后我也依旧会保持这种随心的状态为各位更新教程的,承蒙各位厚爱!!!
友链:b站——爱发电——github

最终,本篇教程到此全部结束,祝各位使用愉快!





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

本版积分规则

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

GMT+8, 2024-10-23 08:39 , Processed in 0.143680 second(s), 31 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

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