文章

《烈幻入》背后的Ren'py

《烈幻入》背后的Ren'py

楔子

想不到竟然有机会写一篇renpy的“教程”。打引号是因为,虽然被讲说是教程,但完全达不到教程的水准——最初的我是为了练习renpy而写的,于是摘了《烈海王似乎打算在幻想乡挑战强者们的样子》(下称烈幻入)的一段来作为练手。就像学前端就要找个网页抄一样,我也找了些东西来练习renpy的各种功能。然而没想到,做着做着,王子海皇笔下的角色竟然在我脑袋里活了起来,于是我当即写了一篇的量(也就是虹龙洞前置回),然后向王子海皇申请能不能发在b站上——这也就是我的起点了。

那几期视频的简介都很长——因为途中真的有相当多的感悟和体会。三年过去,我再重复一遍实在是显得过于煽情了。但是当年的喜悦仍然滋味未减,因为,能够做自己喜欢做的事情真的是一件非常高兴的事情。虽然我本人做视频是仅仅是为了自己开心,但却能够得到大家的支持,尤其是听到像是“本来以为是小短片结果是剧场版”的评价时,内心还是收到了充实感。

然而同时,我也意识到,我写得东西实在是不够好,能实现的东西相较起脑海的里的逊色不少,脑海里的东西相较起王子海皇的文笔更加逊色一筹,这是我唯一的遗憾。今天既然写这篇文,算是一个契机,如果能再续一篇视频,那岂不是大家都开心?但是在那之前,我需要把我的代码稍作整理,记下心得,以备后用。然后就变成了——那样,不就也可以分享出来了吗?这并非教程,因为我的代码里充分暴露了我作为初学者蹒跚学步的弯路,在时隔三年后再在蜣螂堆成的代码上叠床架屋,无疑是自讨苦吃。但稍作整理也许能帮助到一些和我一样刚刚接触renpy的人——包括我自己。

楔子太长了,既然被这样说了,那就开始干吧。

renpy的基础

大多数内容其实都可以在文档里找到,并找到更细致的描述——文档是最重要的。虽然文档的检索功能一言难尽,但把文档细细看完的话,一定能收获良多吧。文档更加全面且细致,笔者这里记录的只是半生不熟的一些心得,椎轮大辂,我们先从简单的东西开始。

脚本标签

我最早是照着馆长海皇的视频开始下手的,我们就以此开始介绍renpy的基础吧。

renpy的脚本相当简单,我们需要一个start的label,之后,直接打字符串,就可以开始在游戏里显示对话了。

1
2
3
label start:
    "烈海王捡起了门口的报纸,准备开始今天的日常。"
    "在锻炼之前,他习惯性地扫了一眼今日的新闻头条。"

label是一个相当好理解的东西,剧本是自上而下执行的,label会标记一个位置,之后可以用jump或者call来跳到这里。jump是类似goto一样的语法,在编程语言中因为破坏了程序的结构,所以一般是不推荐的,但游戏中,要求跳到某一个地方的需求还是相当广泛的。call则简单了,它会像函数一样(压栈)调这个label,并在return后返回原位置。如果要组织章节,那就把所有章节挨个call一遍即可。

1
2
3
4
label start:
    call chapter1
    call chapter2
    call chapter3

标签是可以带参数的,这让标签像函数一样。另外,到文件的末尾也算是return。我在不厌其烦地复制了好多遍之后,终于把投骰子的地方封装成了label。

1
2
3
4
5
6
7
8
9
label start:
    "烈使用了烈车拳!"
    call fail_message("敌人太强了") from start_call_fail
    "从扎马步重新练起吧。"
    return

label fail_message(why):
    "因为[why],烈倒下了!"
    return

start是一个特殊的标签,它是剧本的入口。除此之外还有quitafter_loadmain_menu等。main_menu会显示主菜单,像我制作烈幻入视频时,其实不需要主菜单,所以可以在main_menu里直接返回,跳过主菜单。

renpy推荐在call后接from,便于调试。(不过renpy好像有一键添加from)。

变量

renpy里当然有变量系统,这就涉及到我们要在renpy里写python。在对话里使用[变量名]可以显示变量的值。

需要玩家输入的的话,可以使用renpy自带的input函数。

1
2
$ player_name = renpy.input("你的名字是?")
"[player_name]哟,去打败魔王,拯救世界吧。"
1
2
3
4
5
6
7
8
9
10
11
init -5 python:
    import random

label start:
    "你投了一个骰子:"
    $ dice_value = random.randint(1, 6)
    "得点为 [dice_value]。"
    "你投了20个骰子,得点总和为:"
    python:
        dice_sum = sum(random.randint(1, 6) for _ in range(20))
    "[dice_sum]。"

python:是调用python代码的起手式,在里面你可以自由地写代码了。$是单行python代码的简写形式。

有时候你需要一些在剧本开始前(初始化阶段)就运行的代码,init python后的语句就是如此。可以再这里定一些类或者函数。-5是优先级,越低越先执行。没有就是默认0。

define用于定义常量,不应修改常量。default用于定义变量的默认值,比直接用python的好处在,游戏会维护后面的变量,这样能保证不会因为重开游戏或者存档读档变量不一致产生奇奇怪怪的问题。

1
2
define DICE_SIDES = 6
default dice_count = 20

python$是随着脚本(Script)进行而执行的,default在剧本开始前执行。而初始化(Init)阶段要再之前:init pythondefineimagetransformstylescreen在初始化阶段中按顺序执行。导入文件系统的图片在0优先级,所有image在500优先级,其他语法都默认0优先级。可以用init offset来设置优先级,效果保持到遇到下一个init offset或文件的末尾。例如下面的脚本执行后,foo的值最终是2。

1
2
3
4
5
init offset = 2
define foo = 2

init offset = 1
define foo = 1

比初始化阶段更早期的是脚本处理(Early)阶段,这一段的代码由python early引导,用户可以在这里自定义一些renpy语法以及缓动函数。

角色

1
2
3
4
5
define r = Character("", image="", who_color="#fff", who_outlines=[(2, "#000", 0, 0)])
define nvl_r = Character("", image="", who_color="#fff", who_outlines=[(2, "#000", 0, 0)], kind=nvl)

default chimata_name = "千亦"
define chimata = DynamicCharacter('chimata_name', image='chimata', what_outlines=[(1, "#04afff", 0, 0)], what_color = "#f8d6f4", who_color="#774695", who_outlines = [(1, "#ccc", 0, 0)])

对话框里显示的话其实都是某个角色说出的话,我们叫做say语句。角色是Character()构造出的实例,可以设置名字、类型、图片等等。这里贴一下实现。动态角色对象允许在剧本里动态地改变名字,每次对话前,DynamicCharacter都会计算name_expr的值并修改名字。

1
2
3
4
5
6
7
8
9
def Character(name=NotSet, kind=None, **properties):
    if kind is None:
        kind = renpy.store.adv
    kind = getattr(kind, "character", kind)
    return type(kind)(name, kind=kind, **properties)


def DynamicCharacter(name_expr, **properties):
    return Character(name_expr, dynamic=True, **properties)

这里的adv是最基础的角色类ADVCharacter,另一种模式是NVL模式,对应的类是其子类的NVLCharacter。ADV就是通常的一个角色说一句话的格式,NVL则是把整个屏幕都占着的对话模式,一行一行地刷着也特别有感觉。nvl clear可以清空所有nvl内容。

1
2
3
4
5
6
7
8
9
10
11
12
nvl_narrator "原理十分简单。\n利用生命力的技术……他在过去的一段时间内经常使用。"
nvl_narrator "急救技术。\n汇聚大量的生命力,令自身在极度不利的状况下起死回生。"
nvl_narrator "那么,反其道而行之。\n如果将这份精炼出的强大生命力,在一开始的时候就就直接使用。"
nvl_narrator "并不是集于拳上而是分散到全身……\n并不是用于治疗而是应用于战斗……"
nvl_narrator "能够依靠的经验是存在的。\n四季异变时在他身后打开的生命力之门,那令他各位活跃的状态就是他可以借鉴的对象。"
nvl_narrator "于是武者开始尝试。"
nvl clear
nvl_r "呼……"
nvl_narrator "急救术的起手极快,不然根本无法起到及时救援的作用。"
nvl_narrator "那就想办法让它慢下来。\n将汇聚生命力的部分由拳转为心脏,让魔力与生命力通过血液而传递到全身。"
nvl_narrator "并非一瞬间的爆发,而是相对长久的强化。\n构筑理论后就开始尝试。就在现在……\n开始!"
nvl clear

注意到,有些文本是没有加角色直接说出来的,不带角色的对话其实也会由一个叫做narrator的角色来发出,如果想要修改旁白的样式,就要修改narrator。下面的源码也能看到centeredvcentered这些角色的定义。(我当时并不知道还自己定义了个centered,唉~)

1
2
3
4
5
6
7
8
9
10
11
12
13
init -1400 python:
    # The default narrator.
    _narrator = Character(None, kind=adv, what_style='say_thought')
    adv_narrator = _narrator

    # Centered characters.
    centered = Character(None, what_style="centered_text", window_style="centered_window", statement_name="say-centered")
    vcentered = Character(None, what_style="centered_vtext", window_style="centered_window", statement_name="say-centered")


init 1400 python:
    if not hasattr(store, 'narrator'):
        narrator = _narrator

除此之外还有一个角色叫做extend,它会动态地获取上一个说话人,并在在原有对话中再加一行对话。可以用于更改其他内容后继续对话。比如我想在两句话之间加一个音频播放,换一张角色的表情,又或者让屏幕震动。

1
2
3
4
5
6
7
k "对藤原妹红进行四次直☆接☆攻☆击。"
k "每一次攻击宣言都会让聚集夜莺的攻击力上升500,你所受到的伤害依次是——"
k "2500点!" with vpunch
extend "{size=+6}3000点!!{/size}" with vpunch
extend "{size=+12}3500点!!!{/size}" with vpunch
extend "{size=+18}4000点!!!!{/size}" with vpunch
k "赢了,真是一次有趣的决斗啊!"

1
2
3
4
5
6
ak "我在和皮克君的每日训练中掌握了高超的登山技巧,这种程度没问题!\n"
show 阿求:
    ease 0.6 yoffset 30
    pause 0.4
    ease 0.6 yoffset 0
extend "能办到这些也多亏了你提供的训练计划,谢谢了。"

最后我们说说角色的一些属性吧:

  • name:角色名。
  • kind:角色类型,可以以把另外一个角色的属性当做默认值,来构造新角色。比如之前的adv和nvl。
  • image:传入一个字符串,renpy会自动在文件系统中寻找以image开头的一系列图片,我们一会讲到图片时再细说。
  • dynamic:如前所述,为真时说明是动态角色。
  • what_prefix、what_suffix、who_prefix、who_suffix:这些属性设置了台词或角色名的前缀和后缀。比如如果你希望所有角色在说话时自动加引号,或者某个角色在每句话的末尾都自动加“喵”就可以用这个功能。(不过由于我们烈幻入的剧本相当固定,我没有使用这些功能)
  • callback:对话事件调的回调函数,详情参考
  • 以who_、what_、window_开头,后接各种样式的特性。这里我拿来设置角色名和对话的颜色和边框,也就是color和outlines,outlines的元组参数分别是(尺寸、颜色、x偏移、y偏移)。
1
2
define m = Character("妹红", image="妹红", what_outlines=[(2, "#FA2946", 0, 0)], who_color="#FA2946")
define k = Character("辉夜", image="辉夜", what_outlines=[(2, "#000", 0, 0)], what_color="#f69897", who_color="#f69897", who_outlines=[(2, "#000", 0, 0)])

三年前我一直有个遗憾,那就是没有给千亦搞一个彩虹色的轮廓线,这两天我在查资料时发现Ren’Py在8.3版本更新了文本着色器!我先留一个参考链接文档,后面好好研究一下。

  • ctc:也就是所谓的“点击继续”(click to continue),就是很多游戏里对话显示完毕后,右下角会出现的提示玩家点击以继续的东西。在《命运石之门》里是一个像是坏掉的齿轮一样的东西,《逆转裁判》里的话是向右的继续箭头,不过也有许多游戏没有这样的箭头。我们的载体是视频,不需要提示玩家点击屏幕,所以便没有使用。
  • ctc_position:默认是"nestled",ctc会在文本后面,如果想要设置在右下角,可以用"fixed",位置由ctc的样式决定。
  • screen:界面,我们后述。

在对话里是可以临时修改这些属性的。

1
ksz "她看上去好可怜,我刚刚是不是该输掉的啊……" (name="小铃(小声)")

文本标签

现在来看一下如何给对话文本添加样式。首先是转义字符。

  • \" 双引号
  • \' 单引号
  • \\ 反斜杠
  • \n 换行
  • \ 空格
  • \%%% 百分号
  • [[ 左方括号
  • {{ 左花括号

任意长的空白字符都会变成一个空格,如果想要保留空白字符,可以使用\转义。你可能会像python一样使用三引号,但是三引号里的一个换行符也会被当做空格,更多的换行符则会分割整段话为多个say语句,这倒是方便我们。下面两种写法的效果是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
k "可在这一成不变的幻想乡中,新事物总意味着骚动。"
k "在他所不知晓的地方,这些轻巧又便宜的能力卡牌以超乎想象的速度开始在幻想乡的居民之间流通。"
k "大家纷纷猜测着,这是某位大妖怪心血来潮的恶作剧?是又一种被外界遗忘之物?而不管原因如何,此地的住民们总习惯对这些新奇的事件
冠以统一的称呼。"
k "于是,就在这个夏天的开头。\n有关于金钱、交易与卡牌的异变开始了。"

k """可在这一成不变的幻想乡中,新事物总意味着骚动。

在他所不知晓的地方,这些轻巧又便宜的能力卡牌以超乎想象的速度开始在幻想乡的居民之间流通。

大家纷纷猜测着,这是某位大妖怪心血来潮的恶作剧?是又一种被外界遗忘之物?而不管原因如何,此地的住民们总习惯对这些新奇的事件冠以统一的称呼。

于是,就在这个夏天的开头。\n有关于金钱、交易与卡牌的异变开始了。"""

下面是文本标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
k """{b}粗体{/b} {i}斜体{/i} {s}删除线{/s} {u}下划线{/u}

{color=#f00}红色{/color} {outlinecolor=#0f0}绿色边框{/outlinecolor} {alpha=0.5}半透明{/alpha}

200像素的空格{space=200}{size=30}30号字{/size} {k=5}5像素的字间距{/k}

{font=SourceCodePro-Regular-12.ttf}change the font{/font}

Ruby:{rb}東 京{/rb}{rt}とうきょう{/rt}

一张{image=mallet}{alt}万宝槌{/alt} {noalt}<3{/noalt}{alt}heart{/alt}

{cps=*2}两倍速显示{/cps}

这里说一下alt,alt是替代文本的意思。在网页中如果当图片无法显示时,alt会显示在图片的位置,也可以用于显示图片的说明。在Ren’Py中,alt用作TTS系统的朗读文本。比如这里就会把图片读成“万宝槌”,把”<3”读成“heart”。

需要赋值的标签里,可以用加减乘除,表示在原有基础上的操作,比如cps=*2就表示播放速度翻倍。

1
2
3
4
5
k "玩家不点击也会立马跳到下一句{nw}"
k "读到我停顿两秒钟{w=2.0},停顿结束。"
k "读到我暂停两秒钟并换行。{p=2.0}暂停结束。"
k "此前内容直接显示{fast},之后内容继续打出。"
k "此后内容不再显示。{done}不再显示的内容"

众所周知,Ren’Py是个游戏引擎,是需要玩家点击才会进入下一句话的(大嘘)。那么这里的若干标签是控制和玩家的交互的。譬如在需要停顿的地方停顿。done出现后,这一句话不会在历史信息里显示,所以可以用于在句子读到一半时出去做什么事,之后用fast接同样的一句话,并且在历史记录里也看不出破绽:

1
2
3
4
5
6
"【1d60: 】分钟后,{w=2.0}{done}"
"【1d60:5】分钟后,{fast}烈海王以最快速度飞到了人里。"

suwako 生气 "早苗,你怎么能这样对待你的神明,\n又不是什么大不了的事——{w=0.2}{nw}{done}"
show sanae 阴险
suwako 惊讶 "早苗,你怎么能这样对待你的神明,\n又不是什么大不了的事——{fast}咿呀!"

有些时候我们可能会需要自定义一些文本标签,譬如我们可能常用红色作为骰子的颜色,那么就可以定义一个标签:

1
2
3
4
init python:
    def red_tag(tag, argument, contents):
        return [(renpy.TEXT_TAG, u"color=#f00")] + contents + [(renpy.TEXT_TAG, u"/color")]
    config.custom_text_tags["red"] = red_tag

这里的“red_tag”是自定义文本标签函数,tag是其自身,argument是本标签的参数,contents则是其包裹的内容(如果是自闭合文本标签,则不写这个参数)。包裹的内容是一个内容元组的列表。内容元组是(type, value)的形式,type是内容类型,value是内容值。type可以是以下值:

  • renpy.TEXT_TEXT:文本
  • renpy.TEXT_TAG:文本标签,不包含花括号
  • renpy.TEXT_PARAGRAPH:换行,第二部分始终为空。
  • renpy.TEXT_DISPLAYABLE:嵌入文本的可视组件

以为例:

1
k "{red}测试一段文本,\n测试一个换行,{cps=*2.0}测试一些文本标签,{/cps}{image=mallet}{/red}"

得到的是:

1
2
3
4
5
6
7
8
9
contents = [
    (renpy.TEXT, '测试一段文本,'),
    (renpy.TEXT_PARAGRAPH, ''),
    (renpy.TEXT, 'n测试一个换行,'),
    (renpy.TEXT_TAG, 'cps=*2.0'),
    (renpy.TEXT, '测试一些文本标签,'),
    (renpy.TEXT_TAG, '/cps'),
    (renpy.TEXT_TAG, 'image=mallet'),
]

那么我们要做的其实相当简单,根据标签的需要,把内容元组列表里需要修改的部分修改掉即可。config.custom_text_tags是所有自定义的文本标签,用标签名作为键,函数作为值。config.self_closing_custom_text_tags是所有自定义的自闭合文本标签。

我们现在写一个自动投骰子的功能吧:。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
label easy_dice(cts, ans, cha=None, clr="#f00", flag=0, contents_append=""):
    # cha是要说话的角色 默认是旁白
    # cts表示要说的文本 其中用{}表示要填入ans的地方
    # flag为1表示大成功 2表示大失败 影响音效
    # 如果cts里留的空比ans多一个 且flag为1或2 则会自动填入大成功或大失败
    # contents_append表示在骰点出来后,再之后附加显示的内容
    window show
    python:
        if isinstance(ans, int):
            ans = (ans,)
        alpha_ans = [f"{a}" for a in ans] # 透明的骰点 用于占位
        color_ans = [f"}{a}" for a in ans] # 带颜色的骰点
        contents_before = cts.format(*alpha_ans, "")
        contents_before = contents_before + "{done}"

        if flag:
            contents_after = cts.format(*color_ans, "{color=%s}%s{/color}"%(clr, "大成功" if flag == 1 else "大失败"))
        else:
            contents_after = cts.format(*color_ans)
        contents_after = contents_after + "{fast}" + contents_append
    $ renpy.say(cha, contents_before) # say语句
    if flag == 0:
        play sound soundDice # 骰子音效
    elif flag == 1:
        play sound soundSuccess # 大成功音效
    else:
        play sound soundFail # 大失败音效
    $ renpy.say(cha, contents_after) # 等待玩家点击后的第二句say语句
    $ del(contents_before)
    $ del(contents_after)
    $ del(alpha_ans)
    $ del(color_ans)
    window auto
    return

调用方式是:

1
2
3
4
call easy_dice("~这件事发生在烈海王来到幻想乡的第【1230+1d30:{}={}】天~",(22, 1252), cha=centered)
call easy_dice("烈的好奇心【1d100:{}】{}(50以上询问详细情况)",3 , flag=2)
call easy_dice("于是这里过个少女们的同情心【1d70:{}+30={}】(50以上就把钱还回去,基础的同理心+30)", (66, 96), flag=1)
call easy_dice("【1d30:{}】分钟后", 27, contents_append=",一边用治疗术吊着武术家的命一边在迷途竹林中迷路到快发狂的神明大人总算找到了永远亭。")

对话气泡

动画和变换

图片

renpy的资源素材里,图片应该以“标签(tag)+若干属性(attribute)”(也可以没有属性)的格式命名,文件系统images文件夹及其子文件夹中以这个格式命名的图片会自动被加载,例如:

1
2
3
4
5
6
7
show 千亦 闭眼
cmt "都说了我的目的是开设集市而不是赚钱。\n虽说是会受到点“影响”,不过一张卡牌的程度无所谓啦。"
cmt "你当时的建议让我少走了很多弯路哦,这个就算是一点谢礼。\n再说我也没什么好送礼物的朋友,白狐又不需要这个……"
show  疑惑
r "(白狐是谁啊?)\n那我就不客气地收下了。\n我也能理解,你的社交力的确是到了可称之为灾难的级别。"
show 千亦 腹黑
cmt "闭嘴你这天邪鬼。"

显示图片时,相同标签的图片会互相替换,一行代码就可以替换表情。

上面的代码和下面是等效的(因为烈和千亦这两个角色都各自定义了image)。

1
2
3
4
cmt 闭眼 "都说了我的目的是开设集市而不是赚钱。\n虽说是会受到点“影响”,不过一张卡牌的程度无所谓啦。"
cmt "你当时的建议让我少走了很多弯路哦,这个就算是一点谢礼。\n再说我也没什么好送礼物的朋友,白狐又不需要这个……"
r 疑惑 "(白狐是谁啊?)\n那我就不客气地收下了。\n我也能理解,你的社交力的确是到了可称之为灾难的级别。"
cmt 腹黑 "闭嘴你这天邪鬼。"

所谓的标签是类似标识符一样的东西,而属性是可以有多个并且是无关顺序的,图片在show时,会尽可能地匹配标签,例如,如果我们定义了以下图片:

1
2
3
4
5
n 白天 腹黑
n 白天 笑
n 夜晚 腹黑
n 夜晚 笑
n 纯黑

在show的时候,会有如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
show n 白天
"找不到图片"
show n 腹黑
"n 白天 腹黑"
show n 夜晚
"n 夜晚 腹黑"
show n 
"n 夜晚 笑"
show n 腹黑 白天
"n 白天 腹黑"
show n 纯黑
"n 纯黑"
show n 
"找不到图片"
hide n
"图片销毁"

去除某个属性可以在属性前加减号-

show语句用于显示图像,hide语句用于移除图像、scene是清空图像后显示一张图像(比如用于开一新篇章时换背景)。

以下是show语句可以使用的特性:

  • as 图像标签别名,可以让同样的图片在屏幕上显示多次而不互相替换
  • at 对图片应用若干变换
  • behind 后接若干图片标签,表示当前图片应该在那些图片的后面,很方便
  • zorder 当想要更精准地控制图片的前后关系时可以使用这个特性,默认是0,数值大的图片会遮挡数值小的图片
  • onlayer 图片所绘制的图层,以下是默认的图层:[ ‘master’, ‘transient’, ‘screens’, ‘overlay’ ],一般的图片都显示在master层上。

想要增加新图层,可以使用renpy.add_layer(layer, above=None, below=None, menu_clear=True, sticky=None),下面介绍参数:

  • layer:字符串,图层名
  • above:字符串,在哪一层之上
  • below:字符串,在哪一层之下,above和below不能全为None
  • menu_clear:进入游戏菜单时隐藏,并在离开游戏菜单时恢复

如果希望使用更复杂的图片,可以用image定义图像。

1
2
3
4
5
6
image 注意点:
    "item/注意点 1.png"
    0.25
    "item/注意点 2.png"
    0.25
    repeat

1
2
3
4
5
6
7
8
9
show 注意点:
    xzoom -1
    xcenter 0.55
    ycenter 0.35
    zoom 0.75
    alpha 0.0
    ease 0.2 alpha 1.0
show 千亦 
cmt "正是。\n解放龙珠中的能力,将其制作为卡牌,并将卡牌复制,流通。\n负责这些的全都是我哦~"

你可能注意到了这里有大量表述图片位置、大小、甚至是动画的语句,我们把这些称为动画和变换语言(ATL),修改的这些属性我们叫做特性(property)。我们先从简单的开始。

变换特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
show 千亦 :
    xcenter 0.5
    ycenter 0.5
show 千亦 腹黑 as 千亦2:
    xpos 100
    ypos 100
    xanchor 0.0
    yanchor 0.0
show 千亦 眼泪 as 千亦3:
    xalign 1.0
    yalign 0.0
show 千亦 流汗 as 千亦4:
    align (0.0, 1.0)
    offset (100, -100)
show 千亦 惊讶 as 千亦5:
    xanchor 1.0
    yanchor 1.0
    xpos 0.9
    ypos 0.9

首先是位置,这里是图片常用的左手坐标系,向右为x正方向,向下为y正方向,所以(0.0, 0.0)为图片的左上角,而(1.0, 0.0)为图片的右上角。使用小数表示是百分比,而使用整数表示是像素。可以用x或y来分别设置x和y坐标,也可以用元组来一起赋值。

图片有一个锚点anchor,图片的pos即是把锚点放置在屏幕的某个位置上。例如anchor (0.0, 0.0)pos (100, 100)即意味着,把图片的左上角,放置在屏幕的(100, 100)像素位置上。anchor默认是(0.5, 0.5),即图片的中心。offset是图片在刚才所有的基础上,再进行的偏移量。offset只使用像素。

renpy还提供了center和align来同时修改anchor和pos,前者将图片中心放置在屏幕的某个位置上,后者图片的某个位置放置在屏幕的同样的位置上。例如xalign 0.0就意味将图片的左边放在屏幕的左边,也就是图片恰好贴着屏幕的左边,而xalign 1.0就意味着图片恰好贴着屏幕的右边。熟练使用锚点和位置的话,无论玩家怎样拖拽窗口的大小,图片也能显示在合适的位置上。

这里注意一点,这些renpy提供的特性和底层实现是两码事,修改特性会修改与之相关联的底层实现,例如centeralign都会同时修改锚点和位置,因此这两个同时使用是没有意义的。设置了xalign 0.4 xpos 0.8的效果和xanchor 0.4 xpos 0.8是一样的。

接下来是旋转和伸缩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
show 千亦 :
    xycenter (0.1, 0.5)
    rotate 45
show 千亦  as 千亦2o:
    xycenter (0.3, 0.5)
    yoffset -200
    alpha 0.5
show 千亦  as 千亦2:
    xycenter (0.3, 0.5)
    yoffset -200
    rotate 45
show 千亦  as 千亦3o:
    pos (0.5, 0.5)
    anchor (0.0, 0.0)
    alpha 0.5
    transform_anchor True
show 千亦  as 千亦3:
    pos (0.5, 0.5)
    anchor (0.0, 0.0)
    rotate 90
    transform_anchor True
show 千亦  as 千亦4o:
    pos (0.7, 0.5)
    anchor (0.0, 0.0)
    alpha 0.5
    rotate_pad False
show 千亦  as 千亦4:
    pos (0.7, 0.5)
    anchor (0.0, 0.0)
    rotate_pad False
    rotate 135
show 千亦  as 千亦5o:
    xycenter (0.9, 0.5)
    anchor (0.0, 1.0)
    alpha 0.5
    transform_anchor True
show 千亦  as 千亦5:
    xycenter (0.9, 0.5)
    anchor (0.0, 1.0)
    zoom 2
    xzoom -1
    transform_anchor True

可以用zoom来控制图片的大小,也可以用xzoom和yzoom来分别拉伸两个轴。和坐标不一样,这些值是乘在一起的,例如如果设置了zoom是2,xzoom和yzoom也是2的话,图片会被放大4倍。一般我们用xzoom -1来水平翻转一张图片。旋转以顺时针为正,单位用角度。

你可能注意到了,即使你设置了anchor,旋转和缩放的中心依然是图片的中心。如果你希望图片以anchor为中心旋转,需要设置transform_anchor True。那么renpy其实是没有以任意点旋转的方式的(当然你可以把anchor挪到那个位置,再算出挪动的距离,反向加在pos上),要实现这一点的话,就需要一些其他的技巧(比如拼接透明图片,或者直接写python代码)。

旋转里还有一个rotate_pad False,如果设置的话,图片会以“最小尺寸”旋转。与其说是旋转,更像是滑动。让图片动起来更好理解吧,这是完整旋转一周的样子:

1
2
3
4
5
show 千亦 :
    pos (0.5, 0.5)
    anchor (0.0, 1.0)
    rotate_pad False
    linear 5 rotate 360

其他的一些和尺寸有关的变换特性:

  • crop :裁剪图片,格式为(x, y, width, height)
  • xsize:缩放的宽度
  • ysize:缩放的高度
  • xysizexsizeysize的元组
  • fit:自适应地调整图片的大小,可选模式有
    • "contain":保证界面能装得下图片后尽可能地大,保持宽高比
    • "cover":保证图片完全填充界面,不留缝隙后尽可能地小,保持宽高比
    • "fill":拉伸并完全填充界面
    • "scale-down":和contain类似,但是不放大图片
    • "scale-up":和cover类似,但是不缩小图片
  • xtile:整数,水平平铺的次数
  • ytile:整数,垂直平铺的次数

这些会调整图片的大小,因而和zoom等特性可以叠加。由此我们可以方便地填充背景,例如这样会得到一个向左缓缓移动的背景,背景尽可能小但是又不会露出缝隙。

1
2
3
4
5
6
scene bg 永远亭 with wipeleft:
    fit "cover"
    zoom 1.2
    yalign 0.65
    xalign 0.0
    linear 40.0 xalign 1.0

还有些和图形有关的特性:

  • matrixcolor:矩阵,修改颜色,可以给图片加某种后期风格,后述
  • blur:模糊,数值越大图片越模糊

这些变换特性都可以在对应的文档中找到详细描述。

transform

一遍一遍地设置位置会不会太麻烦了?其实可以把若干ATL语句打包起来,随后用at调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
transform pos(x=0.2, y=0.7):
    # 设定位置
    anchor (0.0, 1.0)
    xcenter x ypos y

transform hop():
    # 跳一下
    easein 0.1 yoffset -30
    easeout 0.1 yoffset 0

show 千亦  at pos(x=0.8), hop:
    zoom 1.75
cmt "可以吗?那太好了。\n麻烦给我也来一碟~"

renpy其实内置了一些变换,主要是定义了常用的位置。我们可以在源码中找到这些变换的实现。默认的位置是最下方的中间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
init -1400:
    transform reset:
        alpha 1.0 rotate None zoom 1.0 xzoom 1.0 yzoom 1.0 align (0, 0) alignaround (0, 0) subpixel False
        xsize None ysize None fit None crop None

    # These are positions that can be used inside at clauses. We set
    # them up here so that they can be used throughout the program.
    transform left:
        xpos 0.0 xanchor 0.0 ypos 1.0 yanchor 1.0

    transform right:
        xpos 1.0 xanchor 1.0 ypos 1.0 yanchor 1.0

    transform center:
        xpos 0.5 xanchor 0.5 ypos 1.0 yanchor 1.0

    transform truecenter:
        xpos 0.5 xanchor 0.5 ypos 0.5 yanchor 0.5

    transform topleft:
        xpos 0.0 xanchor 0.0 ypos 0.0 yanchor 0.0

    transform topright:
        xpos 1.0 xanchor 1.0 ypos 0.0 yanchor 0.0

    transform top:
        xpos 0.5 xanchor 0.5 ypos 0.0 yanchor 0.0
1
2
3
4
5
6
7
8
9
10
11
12
13
             +-----------------------------------------------------------+
             |topleft, reset               top                   topright|
             |                                                           |
             |                                                           |
             |                                                           |
             |                                                           |
             |                          truecenter                       |
             |                                                           |
             |                                                           |
             |                                                           |
             |                                                           |
offscreenleft|left                   center, default                right|offscreenright
             +-----------------------------------------------------------+

动画

为了让图片动起来,我们需要设置“什么属性以怎样的速度变化到什么值”。比如上面的linear 5 rotate 360就表示,在5秒内,让图片的旋转角度线性地从当前值变化到360。

如果我们希望动画先快后慢或者先慢后快,就需要使用其他缓动函数(Easing)了,一般而言,easein是指先快后慢,easeout是指先慢后快,ease则是先慢后快再慢。

1
2
3
4
5
6
7
8
9
10
11
12
show 万宝槌:
    xycenter (0.2, 0.2)
    linear 2 xcenter 0.9
show 万宝槌 as 万宝槌2:
    xycenter (0.2, 0.4)
    easein 2 xcenter 0.9
show 万宝槌 as 万宝槌3:
    xycenter (0.2, 0.6)
    easeout 2 xcenter 0.9
show 万宝槌 as 万宝槌4:
    xycenter (0.2, 0.8)
    ease 2 xcenter 0.9

其他缓动函数可以通过翻看文档常见缓动函数来查找(注意这两个网站的in和out是反着的)。如果你想自定义缓动函数,需要在early阶段写python代码。看过上述两个网站后,不难理解缓动函数是定义在[0,1]上的函数:输入值0表示时间起点,1表示时间终点;输出值0表示起点,1表示终点。所以这些函数都是经过(0,0)点和(1,1)点的。

我们可以在源码中找到这些缓动函数的定义,仿照它不难写出自己的缓动函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
python early in _warper:

    from renpy.atl import pause, instant

    # pause is defined internally, but would look like:
    #
    # @renpy.atl_warper
    # def pause(t):
    #     if t >= 1.0:
    #         return 1.0
    #     else:
    #         return 0.0

    @renpy.atl_warper
    def linear(t):
        return t

    @renpy.atl_warper
    def easeout(x):
        import math
        return 1.0 - math.cos(x * math.pi / 2.0)

    @renpy.atl_warper
    def easein(x):
        import math
        return math.cos((1.0 - x) * math.pi / 2.0)

    @renpy.atl_warper
    def ease(x):
        import math
        return .5 - math.cos(math.pi * x) / 2.0

组合这些动画,就可以创造出足够复杂的动画。例如,从左边出现,在中间停顿,然后消失在右边。这三个按顺序执行。

1
2
3
4
5
6
show 万宝槌:
    xycenter (-0.2, 0.2)
    alpha 0.0
    linear 0.5 xcenter 0.5 alpha 1.0
    pause 0.5
    linear 0.5 xcenter 1.2 alpha 0.0

匀速逆时针旋转,这里旋转到-360后的repeat表示重复执行这一块,后面可以接整数表示次数,如果不接则会一直循环,rotate 0rotate -360是一模一样的,所以便可以一直旋转。

1
2
3
4
5
6
7
show 万宝槌:
    xcenter 0.8
    ycenter 0.4
    block:
        rotate 0
        linear 2.0 rotate -360
        repeat

我们想做一个平抛运动,那么它在水平方向上就是线性的,竖直方向上就是先慢后快的,具体而言是二次的,也就是quadparallel表示同时执行:

1
2
3
4
5
6
show 万宝槌:
    xycenter (0.2, 0.2)
    parallel:
        linear 2 xcenter 0.8
    parallel:
        easeout_quad 2 ycenter 1.2

这是小碗手里一直挥舞的万宝槌:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
show 万宝槌:
    xcenter 0.8
    ycenter 0.4
    alpha 0.0
    parallel:
        ease 0.5 alpha 1.0
    parallel:
        ease 0.4 yoffset -50
        ease 0.4 yoffset 0
        repeat
    parallel:
        rotate 0
        ease 2.0 rotate -360
        repeat

animationfunction

你可能希望用python来定义更复杂的变换,譬如圆周运动或者贝塞尔插值。这些变换或许拿ATL也写得出来,但是自己拿代码写的话,会有种尽在掌握的感觉(误)。

这里写一个稍微复杂些的例子。我想写一个一秒的动画,一个音符倏地出现,然后降低速度,拐个小弯,透明度降低,而后消失。拐个小弯要怎么拐呢?如果用多段easeineaseout拼接,在拼接处会有很强的违和感,至少我在试了两次之后果断放弃。或许renpy有自己的贝塞尔函数,但是自己实现一个也不难。这是我觉得不错的曲线:

而一个变换函数有三个参数:ATLTransform本身,本函数动画开始的秒数st,以及对象的动画时间轴开始的秒数at(就是说从这个对象一开始就计时,而不是本动画开始)。我们在函数内部修改trans的各项属性,进而操控对象。 函数返回None表示本函数动画执行完毕,会接着跳转到下一行ATL语句。函数返回数字表示下次调用本函数的时间,0表示尽可能快地调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
init python:
    def bezier_curve(t, pos_list):
        mt = 1.0 - t
        x = pos_list[0][0]*mt*mt*mt+3*pos_list[1][0]*mt*mt*t+3*pos_list[2][0]*mt*t*t+pos_list[3][0]*t*t*t
        y = pos_list[0][1]*mt*mt*mt+3*pos_list[1][1]*mt*mt*t+3*pos_list[2][1]*mt*t*t+pos_list[3][1]*t*t*t
        return (x, y)

    def note_move(trans, st, at):
        if st > 1.0:
            return None
        elif st < 0.1:
            trans.alpha = 10*st
        elif st > 0.75:
            trans.alpha = 4*(1-st)
        else:
            trans.alpha = 1.0
        trans.xoffset, trans.yoffset = bezier_curve(st, ((0, 0), (0, -250), (-100, -50), (-200,-350)))
        return 0

现在动画已经初具成型。我们再组合上旋转,并反复播放,就完成了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
show 八分音符 as musicnote1:
    xcenter 0.65
    ycenter 0.4
    zoom 0.08
    rotate 0
    parallel:
        function note_move
    parallel:
        0.5
        ease 0.5 rotate 10
    repeat
show 八分音符 as musicnote2:
    alpha 0.0
    zoom 0.08
    ycenter 0.32
    0.5
    block:
        rotate 40
        xcenter 0.8
        parallel:
            function note_move
        parallel:
            0.5
            easeout 0.5 xcenter 0.86
        parallel:
            0.5
            ease 0.5 rotate 30
        repeat
"土著神在沙发上“kerokero”地笑着。\n她从衣兜里掏出张新的卡牌,在众人眼前一晃,又将其收了起来。"

虽然可能并没有人想学,但是我展示一下玉造魅须丸那里的阴阳玉

事件和on

假设屏幕上有三位角色,常做的一件事是高亮正在说话的一位,或者着重强调刚刚变了表情的角色。这种小细节不会特意地给角色镜头,但是却能增加不少沉浸感。

并且,这些行动可以很好地用一个transform就写好。对于角色出现、隐藏、替换、被替换,都可以单独设定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
transform left(x=0.3, z=1.0):
    yanchor 0.6
    on show:
        ypos 0.8
        zoom z*0.95 alpha 0.0
        xcenter x yoffset -20
        easein .25 yoffset 0 zoom z*1.0 alpha 1.0
    on hide:
        zoom z*1.0 alpha 1.0
        easeout .25 yoffset -20 zoom z*0.95 alpha 0.0
    on replace:
        zoom z*1.0 alpha 1.0
        yoffset -20
        easeout .1 yoffset 0
    on replaced:
        pass

show chimata 平视 at left
""
show chimata 惊讶 at left
""
show chimata 平视 at left
""
hide chimata at left
""

将使用到的角色的图片大小归整,定义好预先的位置和缩放,提供聚焦和失焦的动画,这样在角色进行一般的对话时,能帮助我们节省很多时间。

可视组件

在可视组件上作用变换

现在我们知道,定义image时可以使用ATL,也可以包含transform;transform由若干ATL定义,也可以包含图片;show的时候可以用at接transform,也可以用冒号接transform、ATL或图片。听着有些复杂,我们捋一捋。

能够显示在屏幕上的东西,都是可视组件(displayable),可视组件的名称(name)也就是我们在脚本中使用的图片的标签(tag),图片是最常见的可视组件。

将可视组件显示在屏幕上需要变换(transform),变换里有若干操作,譬如调节可视组件的位置、旋转、拉伸、透明度等等,可视组件经过变换后显示在屏幕上。变换是对可视组件的调整,因此如果出现两条对同一特性的修,后作用的会覆盖前作用的。

此外,可视组件在变换后的结果可以定义为新的可视组件,就像图片经过PS后可以保存为新的图片。新的可视组件就可以继续再施加变换,仿佛有一种叠加的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
transform t1:
    zoom 2.0
    xycenter (0.2, 0.5)
    alpha 0.5
    rotate 45
    transform_anchor True
transform t2:
    zoom 1.5
    xalign 0.8
    alpha 0.5
    rotate 90
    transform_anchor True
image chimata = At("千亦 笑", t1)

show 千亦  as c1
show 千亦  as c2 at t1
show 千亦  at t1
show 千亦 at t2
show chimata at t2

下面的show一共五行,屏幕最下方的千亦是c1,它在默认位置上。c2在左边,它被应用了t1,也就是放大两倍,旋转45度,透明度减半。第三行的千亦首先应用了t1,接着又在第四行应用了t2,t2的属性会覆盖t1,因此是右侧较小的那个千亦——缩放1.5倍,透明度0.5,旋转90度。image语句定义了chimata这张图片,它是由“千亦 笑”这张图片应用t1变换得到后保存为chimata的,在那之后它再次应用了t2变换,那么它和第四行定义的千亦在视觉效果上就不一样了。它是chimata这张图片的缩放1.5倍,透明度0.5,旋转90度,和一开始的“千亦 笑”相比,已经是缩放3倍,旋转135度,透明度25了。

我们总结一下:可视组件是图片的类似物,ATL语句是对可视组件的操作,变换是若干ATL语句的集合。image语句可以将经过变换的可视组件定义为新的可视组件,可以通过At()或冒号来给出这一个或多个变换。

还有其他方式可以定义可视组件,例如contains语句,其可以把一个变换变为一个可视组件。在image的定义里,如果不用contains的话,会一直执行t1的repeat,因此i1的定义里第三行和第四行就永远也执行不到了。

1
2
3
4
5
6
7
8
9
10
11
12
13
transform t1:
    "item/注意点 1.png"
    0.25
    "item/注意点 2.png"
    0.25
    repeat
image i1:
    ycenter 0.5
    contains t1
    xalign 0.0
    linear 5 xalign 1.0
show i1:
    zoom 2

最后我们补充一下,当at后有不止一个transform时(也就是变换列表at_list),它们的作用和替换是怎样的吧。我们先引文档的描述。

某个ATL变换、内建变换或使用 Transform 定义的变换对象被同类变换替换时, 同名特性的值会从前一个变换继承到新的变换。不同类型的变换无法继承。 如果 show语句 中的at关键字后列出了多个变换待替换,则新变换列表从后往前依次替换,直到新替换变换列表全部换完。例如: e 变换替换了 c, d 变换替换了 b,而没有变换会替换 a。

1
2
3
show eileen happy at a, b, c
"我们稍等一下。"
show eileen happy at d, e

这是什么意思呢?至少我在读到这段话的时候是完全蒙头转向的。在搞懂之后回头来看,确实能发现他说的有道理,只是有道理得太迟了——我是自己琢磨出来的。

为方便大家理解,我们先明确,一个ATL语句相当于修改某一个特性,一个变换相当于多条打包的ATL语句,且这些语句修改的特性是不同的。如果有多条同名语句,后出现的会覆盖新出现的。 为了我表述方便,我们定义变换的拼合,如果后一个变换的ATL语句和前一个变换的ATL语句同名,那么后一个变换的ATL语句会覆盖前一个变换的ATL语句,否则只是合并。举例来说:

1
2
3
4
5
6
7
8
9
10
11
12
13
transform t1:
    rotate 30
    zoom 2.0
transform t2:
    alpha 0.5
    rotate 20
transform t1_cohere_t2:
    t1
    t2
transform t3:
    rotate 20
    alpha 0.5
    zoom 1.5

这里,t1和t2的拼合和t3是一样的。后出现的rotate 20覆盖掉了rotate 30。我们姑且记作t1+t2=t3吧。

其次,冒号定义的变换,相当于直接在at最末尾加一些变换,也就是下面两个show效果是一样的:

1
2
3
4
5
6
7
8
transform t4:
    matrixcolor BrightnessMatrix(0.5)
    rotate 30
show img at t1, t4

show img at t1:
    matrixcolor BrightnessMatrix(0.5)
    rotate 30

最后,在show末尾的若干变换会从左到右复合起来。这里的复合就是做完一个再做第二个,因此不会出现ATL语句的覆盖,我们姑且记作t1×t2=t5吧,以下两个show效果是一样的。(如果图片不以(0.5, 0.5)作为anchor的话,可能会有平移,但是角度是没错的)

1
2
3
4
5
6
7
8
9
transform r1:
    rotate 30
transform r2:
    rotate 20
transform r1_comp_r2:
    rotate 50

show img at r1, r2
show img at r1_comp_r2

那么我们现在表述一下变换列表的替换规律吧。如果替换后有若干变换,那么就从替换前的列表末尾取同样数量个变换,然后一一对应做拼合,成为本次show语句的变换列表。

还是以文档中给的例子为例,如果是a, b, cd, e去替换,得到的就是b+d, c+e。有时这会给出一些反直觉的结果来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
transform t1:
    zoom 2.0
    xcenter 0.5
    rotate 45
    alpha 0.5
show 千亦  at t1:
    xcenter 0.8
    zoom 2.0
    rotate 45
    alpha 0.5
"原图像的四倍 位置在0.8 旋转90度 透明度0.25"
show 千亦 帅气:
    zoom 2.0
    rotate 45
    alpha 0.5
"原图像的两倍 位置在0.8 旋转45度 透明度0.5"

第一个show中,t1和show的冒号后定义的两个zoom会相乘,最终的放大倍数是4倍。之后,当我们在第二个show中重新指定了zoom特性,它只有一个变换,因此t1会被丢弃,放大倍数会重新回到2倍。 如果这样写的话,前后就不会有变化了(位置得重新写一遍,也有点难理解):

1
2
3
4
5
6
transform nothing:
    xcenter 0.8
show 千亦 帅气 at nothing:
    zoom 2.0
    rotate 45
    alpha 0.5

这是笔者走过的弯路——在任何编程语言中,都不要写出让人类难读而感到疑惑的代码。这里干干净净地把transform拆分开比较好。觉得复杂的时候就该定义新的可视组件了。

类图像的可视组件

图片是一种最简单的可视组件,除此之外我们也有别的需求,比如图片的组合。这些“类图像”(imagelike)的可视组件类定义在renpy/display/imagelike.py下。像图像一样,它们有着自己的位置、尺寸等属性。

这种图片的组合可以被应用在立绘的拼接上,角色的立绘往往有着一套相似的样式并在细微之处有微小的差别。比如全身的动作都是一模一样的,只是把微笑改成哭脸或者其他表情。又或者右半身保持不变,只是举起左手竖大拇指等等。这样的一组图片一般称为一套差分。我们固然可以为每个差分都单独绘制一套立绘,但在游戏开始后再拼接好处也很多,比如差分要组合的东西很多——譬如三套衣服八套表情两套特效的时候,又或者眼睛五套嘴巴五套的时候,把每一张立绘都排列组合单独保存成文件会占用不少空间——毕竟这只是个小游戏,占用那么多磁盘空间是要做什么?(不过dairi老师的差分已经是拼接好的了)

我们再把立绘拆出来,png是无损压缩格式,表情的差分只需要几个KB。接下来在游戏里拼接它们,以下是几个示例:

1
2
3
4
5
6
7
8
9
10
11
12
image 天弓千亦 微笑 = Composite(
    (1084, 1220),
    (0, 0), "images/dairiComp/Dairi天弓千亦 身体.png",
    (0, 0), "images/dairiComp/Dairi天弓千亦 微笑.png",
)

image 天弓千亦 微笑 尾气 = Composite(
    (1084, 1220),
    (0, 0), "images/dairiComp/Dairi天弓千亦 身体.png",
    (0, 0), "images/dairiComp/Dairi天弓千亦 微笑.png",
    (0, 0), "images/dairiComp/Dairi天弓千亦 尾气.png",
)

这里第一个元组是图片的尺寸,之后的元组和图片名则是图片的位置和图片名。组合好后,便可以像一般的图片一样使用了。

renpy还提供了一种叫做层叠式图像的方法来组合图片。优点是图像的属性也会自动生成,而不需要我们把每种情况都单独写一个image语句。

Crop是裁剪,第一个元组是裁剪的左上角坐标,以及裁剪的宽和高。

1
image 千亦头像 微笑 = Crop((240, 40, 360, 360), "天弓千亦 微笑")

想要一张纯色的图片:

1
2
3
4
image bluegreen = Solid("#39c5bb")
show bluegreen:
    pos (0.25, 0.25)
    xysize (0.5, 0.5)

Frame是所谓的九宫格切图,一般用于UI界面,比如窗口、按钮、聊天气泡这种希望可以随意伸缩的组件。示意图如下:

在Renpy中的定义为:

1
Frame(image, left=0, top=0, right=None, bottom=None, tile=False, **properties)

其中image是图片,lefttop是左边界尺寸和上边界尺寸,rightbottom是右边界尺寸和下边界尺寸,为空时分别和左上相等,tile决定中键的部分是平铺还是拉伸。

有时会有这样的需求吧:登场人物是一个神秘角色——说是神秘角色但是大家心中都有些许猜测,但是直接把立绘放出来又太直白了,我们想要加一层黑色的阴影,而且最好是上半身纯黑,只露出一点下半身的亮色,就像这样。

1
2
3
4
5
6
7
8
show 天弓千亦 微笑 :
    zoom 1.25
    anchor (0.4, 0.32)
    pos (0.2, 0.45)
cmt "“副职业是侦探~”\n我记得你曾经这样自称过,怎么事到如今却问出了这样没品的问题?" (name = "")
r "我有说错什么吗?{p}天弓千亦小姐。"
show 天弓千亦 帅气 尾气 subete with dissolve
cmt "忘记了吗?我这段时间投资的项目,就是“集换式卡牌游戏”。\n这个交易系统本来就是以我的力量为基础设计的,我本人想绕过它还不是轻而易举!"

为此,我们需要一张上黑下透明的渐变图。

而后使用AlphaMask(child, mask, **properties),这种可视组件使用child作为底图,但它的透明通道要乘以mask。换句话说,经过AlphaMask后,我们的渐变图变成了一张保持大小,但是裁剪出了千亦的轮廓的图片。再把这张图片叠放在千亦的立绘上,就得到了我们想要的效果。

1
2
3
4
5
6
7
8
9
10
11
12
transform chimata_size:
    xysize (1084, 1220)
image 天弓千亦  = AlphaMask(
    At("gradient_mask_character", chimata_size),
    "天弓千亦 微笑"
)
image 天弓千亦 微笑 :
    chimata_size
    contains:
        "天弓千亦 微笑"
    contains:
        "天弓千亦 黑"

图像处理器

顾名思义,处理图像的。这里的很多类定义在renpy/display/im.py下。

im.AlphaMask(base, mask, **properties),使用base作为图像的RGB数据,mask的红色通道作为透明通道。注意和类图像的可视组件使用起来并不相同。 im.Crop(im, rect),裁剪图像,rect是裁剪的左上角坐标和宽高四元组。 im.Composite(size, *args, **properties)Composite用法一致,依次输入尺寸和若干要组合的图像处理器。 im.Scale(im, width, height, bilinear=True, **properties),缩放图像到指定尺寸。bilinear表示使用双线性插值算法,否则使用最近邻。 im.FactorScale(im, width, height=None, bilinear=True, **properties),和im.Scale类似,但默认保持宽高比。 im.Flip(im, horizontal=False, vertical=False, **properties),水平或垂直翻转图像。 im.Rotozoom(im, angle, zoom, **properties),旋转并缩放图像,逆时针为正,角度值。 im.Tile(im, size=None, **properties),平铺图像,size是平铺的尺寸,宽高的元组,为空则为屏幕宽度到屏幕高度。 im.Image(filename, **properties),要加载的文件名 im.Data(data, filename, **properties),以二进制数据关联图像名

Matrixcolor

我们在介绍变换特性的时候跳过了matrixcolor,现在来细细说一下和图片颜色有关的东西。

转场

本文由作者按照 CC BY 4.0 进行授权