找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 807|回复: 8

[教程] Ren'Py实现动态循环显示背景

[复制链接]
发表于 2023-6-11 14:02:22 | 显示全部楼层 |阅读模式

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

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

×
本帖最后由 ZYKsslm 于 2023-11-10 23:14 编辑

Ren'Py教程:如何用多张背景图层动态循环显示背景


论坛发帖能不能支持Markdown啊!很不习惯,还是md写得舒服,而且还能图文并茂。我记得HTML是可以内嵌Markdown的。



相信很多人在制作游戏时都会去游戏网站itch找免费的游戏素材资源,就比如我自己。

在一次寻找背景素材时,我找到了一个看起来不错的免费资源。当我满心欢喜下载回来后,我傻眼了——作者竟然把背景分成了多个图层!虽说有PSD文件,我可以直接进PS导成PNG出来,但是此时我开始思考作者的用意了......


素材地址在下面,由于太大了无法添加附件,你们自己下载吧。


最开始时,我只是想利用线性运动做一个简单的循环背景,在把所有图层一股脑叠进去后,我发现虽然是成功了,但是非常生硬。

我起初的想法是这样的,利用两张相同的图片(一张覆盖全屏,一张在屏幕外面)同时水平运动来实现背景循环,(具体思路网上搜吧),但是发现非常生硬。于是乎,我看到这么多背景图层后,我灵光一闪:如果让每个图层都以不同的速度单独运动不就好了吗?


果然如我所料,在经历了巨大的工作量后,什么,你问什么工作?——定义n次图像并show个2n次,n为图层个数,手都要断啦!还好效果是好的,但是我又想如果有多个背景,而且每个背景都有很多背景图层,那我不要变成无情的复读机?


于是!在又经历了几个小时的编码与测试并根据坛友的建议,我终于写出了能够批量定义图像并实现动态循环显示背景的方法。好了,直接上代码吧!


这是转场:

[RenPy] 纯文本查看 复制代码
# transform

# 这几个转场搭配使用可以达到背景循环的效果
# 参数size是一个列表,为背景尺寸,默认为[1920, 1080]
# 参数t是一个整数或浮点数,为运动时间

# 从右向左
transform l_first_bg(size=[1920, 1080], t=50):
    xysize (size[0], size[1])
    pos (0, 0)
    linear t xpos -size[0]
    repeat
        
transform l_second_bg(size=[1920, 1080], t=50):
    xysize (size[0], size[1])
    pos (size[0], 0)
    linear t xpos 0
    repeat

# 从左向右
transform r_first_bg(size=[1920, 1080], t=50):
    xysize (size[0], size[1])
    pos (0, 0)
    linear t xpos size[0]
    repeat
        
transform r_second_bg(size=[1920, 1080], t=50):
    xysize (size[0], size[1])
    pos (-size[0], 0)
    linear t xpos 0
    repeat

# 从上到下
transform d_first_bg(size=[1920, 1080], t=50):
    xysize (size[0], size[1])
    pos (0, 0)
    linear t ypos -size[1]
    repeat 

transform d_second_bg(size=[1920, 1080], t=50):
    xysize (size[0], size[1])
    pos (0, size[1])
    linear t ypos 0
    repeat

# 从下到上
transform u_first_bg(size=[1920, 1080], t=50):
    xysize (size[0], size[1])
    pos (0, 0)
    linear t ypos size[1]
    repeat 

transform u_second_bg(size=[1920, 1080], t=50):
    xysize (size[0], size[1])
    pos (0, -size[1])
    linear t ypos 0
    repeat


这是功能:

[RenPy] 纯文本查看 复制代码
# The functions and init definitions are here

init python:

    import os


    def abs_path(dir):
        """该函数根据一个项目的相对路径返回一个项目的绝对路径。"""
        path = f"{config.gamedir}/{dir}"
        return path


    def images_define(dir, prefix):
        """
        该函数只能在init语句块中且在该函数定义后使用。

        该函数可批量定义图像并返回一个存储着图像名的列表。

        参数dir为图像文件夹的路径,prefix为要定义的图像名前缀,定义的图像名统一使用prefix_order的形式。
        """
        images = os.listdir(abs_path(dir))

        image_list = []
        for order, image in enumerate(images):
            image_path = f"{dir}/{image}"
            image_name = f"{prefix}_{order + 1}"
            renpy.image(image_name, image_path)

            image_list.append(image_name)
        
        return image_list


    class MovingBackground(object):
        """这是一个动态背景类。"""

        def __init__(self, layers: list, size=[1920, 1080], reverse=True, speeds=19.2, direction="l", state="gradual"):
            """
            参数layers应为一个列表,从里到外依次存储背景的各个图层。

            参数size为一个列表,为背景尺寸,默认为[1920, 1080]。

            若图层排列顺序为从最内层到最外层则reverse参数应为True,反之则应为False。

            若参数speeds为一个列表,则每个元素分别对应着每个图层的运动速度(像素/秒);若为一个数,则为整体线性均值运动速度,默认为19.2。

            参数direction为一个字符串,表示运动方向。"l"、"r"、"d"、"u"分别表示从右到左、从左到右、从上到下、从下到上。

            参数state为一个字符串,表示各图层运动状态,"gradual"表示每个图层运动速度不一,"certain"表示每个图层运动速度相同,默认为"gradual"。
            """
            self.layers = layers
            self.size = size
            self.reverse = reverse
            self.direction = direction

            # 判断speeds的类型
            if isinstance(speeds, list):
                times = [
                    size[0] // speed for speed in speeds
                ] if direction == "h" else [
                    size[1] // speed for speed in speeds
                ]
                self.times_l = True
            else:
                if state == "gradual":
                    if direction == "r" or direction == "l":
                        t = int(size[0] // speeds)
                        step = t // len(layers)

                        times = [
                            time for time in range(0, t + 1, step)
                        ]
                    elif direction == "d" or direction == "u":
                        t = int(size[1] // speeds)
                        step = t // len(layers)

                        times = [
                            time for time in range(0, t + 1, step)
                        ]
                    self.times_l = True
                elif state == "certain":
                    times = speeds
                    self.times_l = False

            if self.reverse is True and self.times_l is True:
                times.sort(reverse=True)

            self.times = times

        def show(self, d=None, zorder=0, behind=[], transition=None):
            """
            该方法可以根据多个背景图层动态显示背景。

            参数d为一个字符串,可以直接控制运动方向而免于重复定义对象。

            参数zorder为一个整数,等效于onlayer特性,默认为0。

            参数behind为一个字符串列表,等效于behind特性,默认为空列表。

            transition为一个转场,默认为None。
            """

            if d is None:
                d = self.direction

            if d == "l":
                first_transform_func = l_first_bg
                second_transform_func = l_second_bg
            elif d == "r":
                first_transform_func = r_first_bg
                second_transform_func = r_second_bg
            elif d == "d":
                first_transform_func = d_first_bg
                second_transform_func = d_second_bg
            else:
                first_transform_func = u_first_bg
                second_transform_func = u_second_bg

            # 显示图像
            self.alias_layers = []
            for order, layer in enumerate(self.layers):
                alias_layer = f"layer_{order}"
                renpy.show(
                    layer, 
                    [first_transform_func(size=self.size, t=self.times[order] if self.times_l else self.times)], 
                    zorder=zorder, 
                    tag=alias_layer, 
                    behind=behind
                )

                renpy.show(
                    layer, 
                    [second_transform_func(size=self.size, t=self.times[order] if self.times_l else self.times)], 
                    zorder=zorder, 
                    behind=behind
                )

                self.alias_layers.append(alias_layer)
            
            renpy.with_statement(transition)
        
        def hide(self, transition=None):
            """该方法可以隐藏背景(所有背景图层)。transition为一个转场,默认为None。"""
            for order, layer in enumerate(self.layers):
                renpy.hide(layer)
                renpy.hide(self.alias_layers[order])

            renpy.with_statement(transition)
    

    # definitions(定义的语句要写在这里!)


那么该如何使用呢?先把两段代码复制到你的rpy脚本文件中(推荐新建一个),然后再使用images_define函数批量定义你的图层并获得返回的图像名列表,然后再定义一个MovingBackground对象,这样所有的定义操作就完成了。所有定义的语句要写在#definitions下面!


下面是一个例子:


[RenPy] 纯文本查看 复制代码
# definitions(定义的语句要写在这里!)

    winter_forest_layers = images_define("images/Free Pixel Art Winter Forest", "layer_winter_forest")
    hill_layers = images_define("images/Free Pixel Art Hill", "layer_hill")

    hill_bg = MovingBackground(hill_layers)
    winter_forest_bg = MovingBackground(winter_forest_layers)



然后,就可以在label中使用show方法show你的背景,或用hide方法隐藏你的背景啦!


这是示范:


[RenPy] 纯文本查看 复制代码
label start:

    window hide

    $ winter_forest_bg.show(transition=Fade(0.5, 0.0, 0.5))
    pause
    
    $ winter_forest_bg.hide(Dissolve(1))

    $ hill_bg.show(transition=Dissolve(1))
    pause

    $ hill_bg.hide(Fade(0.5, 0.0, 0.5))

    "演示结束"

    return





背景图层

背景图层

评分

参与人数 1活力 +120 干货 +1 收起 理由
被诅咒的章鱼 + 120 + 1 鼓励原创!

查看全部评分

 楼主| 发表于 2023-6-11 14:04:38 | 显示全部楼层
本帖最后由 ZYKsslm 于 2023-6-11 14:27 编辑

链接没发出来,再发一遍:https://edermunizz.itch.io/free-pixel-art-winter-forest,https://edermunizz.itch.io/free-pixel-art-hill
回复 支持 抱歉

使用道具 举报

发表于 2023-6-12 09:50:41 | 显示全部楼层
本帖最后由 被诅咒的章鱼 于 2023-6-12 10:02 编辑

就……十分不好用……

这方案的通用性比较差。只考虑的背景图像从右往左平移的情况,无法实现背景从左往右平移。
各图层的速度根据图层总数线性均值采样的方式,既不灵活也不好看。如果把 speed 设置为负数,各图层速度列表 speeds 计算后就是个空列表。而且,虽然变量名是速度,实际上是动画持续时间啊……
建议楼主再修改一下方案。把 MovingBackground 构造入参改为两个列表:图片名称列表和各图片移动速度列表(速度值可以为负数,分别对应左右两个移动方向,绝对值越大图片移动速度越快),或者一个分别“以图片名称为键”、“移动速度为值”的字典。
回复 支持 1 抱歉 0

使用道具 举报

 楼主| 发表于 2023-6-11 14:13:16 | 显示全部楼层
本帖最后由 ZYKsslm 于 2023-6-16 00:49 编辑

[RenPy] 纯文本查看 复制代码
# The functions and init definitions are here

init python:

    import os


    def abs_path(dir):
        """该函数根据一个项目的相对路径返回一个项目的绝对路径。"""
        path = f"{config.gamedir}/{dir}"
        return path


    def images_define(dir, prefix):
        """
        该函数只能在init语句块中且在该函数定义后使用。

        该函数可批量定义图像并返回一个存储着图像名的列表。

        参数dir为图像文件夹的路径,prefix为要定义的图像名前缀,定义的图像名统一使用prefix_order的形式。
        """
        images = os.listdir(abs_path(dir))

        image_list = []
        for order, image in enumerate(images):
            image_path = f"{dir}/{image}"
            image_name = f"{prefix}_{order + 1}"
            renpy.image(image_name, image_path)

            image_list.append(image_name)
        
        return image_list


    class MovingBackground(object):
        """这是一个动态背景类。"""

        def __init__(self, layers: list, size=[1920, 1080], reverse=True, speeds=19.2, direction="l", state="gradual"):
            """
            参数layers应为一个列表,从里到外依次存储背景的各个图层。

            参数size为一个列表,为背景尺寸,默认为[1920, 1080]。

            若图层排列顺序为从最内层到最外层则reverse参数应为True,反之则应为False。

            若参数speeds为一个列表,则每个元素分别对应着每个图层的运动速度(像素/秒);若为一个数,则为整体线性均值运动速度,默认为19.2。

            参数direction为一个字符串,表示运动方向。"l"、"r"、"d"、"u"分别表示从右到左、从左到右、从上到下、从下到上。

            参数state为一个字符串,表示各图层运动状态,"gradual"表示每个图层运动速度不一,"certain"表示每个图层运动速度相同,默认为"gradual"。
            """
            self.layers = layers
            self.size = size
            self.reverse = reverse
            self.direction = direction

            # 判断speeds的类型
            if isinstance(speeds, list):
                times = [
                    size[0] // speed for speed in speeds
                ] if direction == "h" else [
                    size[1] // speed for speed in speeds
                ]
                self.times_l = True
            else:
                if state == "gradual":
                    if direction == "r" or direction == "l":
                        t = int(size[0] // speeds)
                        step = t // len(layers)

                        times = [
                            time for time in range(0, t + 1, step)
                        ]
                    elif direction == "d" or direction == "u":
                        t = int(size[1] // speeds)
                        step = t // len(layers)

                        times = [
                            time for time in range(0, t + 1, step)
                        ]
                    self.times_l = True
                elif state == "certain":
                    times = speeds
                    self.times_l = False

            if self.reverse is True and self.times_l is True:
                times.sort(reverse=True)

            self.times = times

        def show(self, d=None, zorder=0, behind=[], transition=None):
            """
            该方法可以根据多个背景图层动态显示背景。

            参数direction为一个字符串,可以直接控制运动方向而免于重复定义对象。

            参数zorder为一个整数,等效于onlayer特性,默认为0。

            参数behind为一个字符串列表,等效于behind特性,默认为空列表。

            transition为一个转场,默认为None。
            """

            if d is None:
                d = self.direction

            if d == "l":
                first_transform_func = l_first_bg
                second_transform_func = l_second_bg
            elif d == "r":
                first_transform_func = r_first_bg
                second_transform_func = r_second_bg
            elif d == "d":
                first_transform_func = d_first_bg
                second_transform_func = d_second_bg
            else:
                first_transform_func = u_first_bg
                second_transform_func = u_second_bg

            # 显示图像
            self.alias_layers = []
            for order, layer in enumerate(self.layers):
                alias_layer = f"layer_{order}"
                renpy.show(
                    layer, 
                    [first_transform_func(size=self.size, t=self.times[order] if self.times_l else self.times)], 
                    zorder=zorder, 
                    tag=alias_layer, 
                    behind=behind
                )

                renpy.show(
                    layer, 
                    [second_transform_func(size=self.size, t=self.times[order] if self.times_l else self.times)], 
                    zorder=zorder, 
                    behind=behind
                )

                self.alias_layers.append(alias_layer)
            
            renpy.with_statement(transition)
        
        def hide(self, transition=None):
            """该方法可以隐藏背景(所有背景图层)。transition为一个转场,默认为None。"""
            for order, layer in enumerate(self.layers):
                renpy.hide(layer)
                renpy.hide(self.alias_layers[order])

            renpy.with_statement(transition)
    

    # definitions(定义的语句要写在这里!)

回复 支持 抱歉

使用道具 举报

 楼主| 发表于 2023-6-15 22:54:54 | 显示全部楼层
本帖最后由 ZYKsslm 于 2023-6-16 18:12 编辑
被诅咒的章鱼 发表于 2023-6-12 09:50
就……十分不好用……

这方案的通用性比较差。只考虑的背景图像从右往左平移的情况,无法实现背景从左往右 ...

好的,感谢建议。线性均值移动速度的方案是我根据视觉效果设计的,因为一般来说离镜头更远处的景物相对于更近的景物来说移动速度会更慢,所以这样视觉效果会更好一点,不过我也觉得可以添加让用户自己设定速度的功能
回复 支持 抱歉

使用道具 举报

 楼主| 发表于 2023-6-16 00:41:14 | 显示全部楼层
被诅咒的章鱼 发表于 2023-6-12 09:50
就……十分不好用……

这方案的通用性比较差。只考虑的背景图像从右往左平移的情况,无法实现背景从左往右 ...

已经修改并完善功能了!
回复 支持 抱歉

使用道具 举报

发表于 2023-6-16 15:46:42 | 显示全部楼层
emmmmm……

楼主已经意识到还有垂直方向平移的需求,所以加了个 direction 参数来控制平移方向。
如果需求斜向平移的话,又怎么处理呢?用if-else语句往往容易陷入这种窘境……

我花了点时间重新写了个组件:
[RenPy] 纯文本查看 复制代码
init python:

    from renpy.uguu import GL_CLAMP_TO_EDGE, GL_MIRRORED_REPEAT, GL_REPEAT
    
    renpy.register_shader("shadertest.texturewarp", variables="""
    uniform float u_lod_bias;
    uniform sampler2D tex0;
    uniform vec2 u_model_size;
    uniform vec2 u_warp_speed;
    uniform float u_time;
    attribute vec2 a_tex_coord;
    varying vec2 v_tex_coord;
    """,vertex_300="""
        v_tex_coord = a_tex_coord + u_time * (u_warp_speed / u_model_size);
    """,fragment_300="""
        gl_FragColor = texture2D(tex0, v_tex_coord, u_lod_bias);
    """)

init python:

    class MovingBackground(renpy.Displayable):

        def __init__(self, size, image_list, speed_list, **kwargs):
        
            super(MovingBackground, self).__init__(**kwargs)
            self.size = size
            self.image_list = image_list
            self.speed_list = speed_list
            
        def render(self, width, height, st, at):

            render = renpy.Render(width, height)
            
            for image, speed in zip(self.image_list, self.speed_list):
                t = Transform(image, size=self.size)
                child_render = renpy.render(t, width, height, st, at)
                child_render.mesh = True
                child_render.add_shader('shadertest.texturewarp')
                child_render.add_property('gl_texture_wrap', (GL_REPEAT, GL_REPEAT))
                child_render.add_uniform('u_model_size', self.size)
                child_render.add_uniform('u_warp_speed', speed)
                child_render.add_uniform('u_time', st)

                render.blit(child_render, (0, 0))
                
            renpy.redraw(self, 0)
            return render


没有楼主的寻找图片路径和自定添加路径前缀的部分,可以自行添加,并不冲突。

在脚本中定义图片列表和速度列表,生成一个MovingBackground实例就能像普通image一样使用了:
[RenPy] 纯文本查看 复制代码
define bg_image_list = ['Pixel Art Winter Forest/1.png',
    'Pixel Art Winter Forest/2.png',
    'Pixel Art Winter Forest/3.png',
    'Pixel Art Winter Forest/4.png',
    'Pixel Art Winter Forest/5.png',
    'Pixel Art Winter Forest/6.png',
    'Pixel Art Winter Forest/7.png',
    'Pixel Art Winter Forest/8.png',
    'Pixel Art Winter Forest/9.png',
    'Pixel Art Winter Forest/10.png',
    'Pixel Art Winter Forest/11.png',
    'Pixel Art Winter Forest/12.png',
    'Pixel Art Winter Forest/13.png',
    'Pixel Art Winter Forest/14.png',
    'Pixel Art Winter Forest/15.png',
    'Pixel Art Winter Forest/16.png']
define bg_image_speed_list = [(0, 0),
    (10, 0),
    (20, 0),
    (30, 0),
    (40, 0),
    (50, 0),
    (60, 0),
    (70, 0),
    (80, 0),
    (90, 0),
    (100, 0),
    (110, 0),
    (120, 0),
    (130, 0),
    (140, 0),
    (150, 0)]

image movingbg = MovingBackground((1440, 720), bg_image_list, bg_image_speed_list)



大部分简单的转场应该都可以兼容。

用到的图片作为附件上传了。

Pixel Art Winter Forest.rar

59.73 KB, 下载次数: 4, 下载积分: 活力 100

回复 支持 抱歉

使用道具 举报

 楼主| 发表于 2023-6-16 17:32:58 | 显示全部楼层
本帖最后由 ZYKsslm 于 2023-6-16 17:50 编辑
被诅咒的章鱼 发表于 2023-6-16 15:46
emmmmm……

楼主已经意识到还有垂直方向平移的需求,所以加了个 direction 参数来控制平移方向。

说实话,我本来只是想做个背景过渡的,一般不会有斜向平移的要求吧。还有渲染器已经超出我的知识范围了
回复 支持 抱歉

使用道具 举报

发表于 2023-6-19 08:47:29 | 显示全部楼层
我不清楚楼主这个帖子是写给谁看的。
反正我写的东西并不是只给楼主看的。
回复 支持 抱歉

使用道具 举报

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

本版积分规则

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

GMT+8, 2024-4-27 14:50 , Processed in 0.082155 second(s), 20 queries , File On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

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