Inky文档自翻中文版

Introduction

Ink是一种脚本语言,它围绕着用流(flow)标记纯文本以生成交互式脚本的思想而构建。

最基本的是,它可以用来编写一个Choose Your own style的故事,或者一个分支对话树。但它真正的优势在于编写带有大量选项和大量流程重组的对话。

Ink提供了几个特性,使没有技术能力的作者能够经常分支,并以或大或小的方式发挥这些分支的结果,而不用瞎忙一气。

脚本的目标是干净和逻辑有序,这样分支对话就可以简单地“目测”。流会尽可能以声明式的方式描述。

它的设计也考虑到了重新起草;因此,编辑流程应该是快速的。

在观看下面的文档时,最好安好inky,一边看一边可以把文档中的脚本复制到左边,可以实时在右边看到效果。

第一部分:基础

1) 内容

最简单的ink脚本

最简单的ink脚本就是在.ink文件中的文本

就是这么简单的一句话

在运行时,就会直接输出这句话,然后停止。

在不同行的文本会产生新的段落,下面的脚本:

这里是第一行
第二行
还有第三行

输出的结果看起来就和脚本完全相同

注释

默认情况下,文件中的所有文本都将出现在输出内容中,除非特别标记。

最简单的标记方法就是注释。Ink支持两种注释。一种是用于读取代码的人,编译器会忽略它:

那么什么是注释呢?

// 比如这两条斜线后一行的话会被注释

而这里的话又可以显示出来

/*
    这种注释方法可以注释一长段话
    所以这里的内容也会被注释掉
    这里的也是
*/

还有这种注释是为了提醒作者应该要在这里做些什么,编译器会把它显示出来进行提醒

TODO: 好好写这一段!

标签(Tags)

当引擎运行时,来自游戏的文本内容将“原样”呈现。然而,有时候用额外的信息标记一行内容,告诉游戏该如何处理这些内容也是有用的。

Ink提供了一个简单的系统,用标签标记内容行。

一行普通的文本 # colour it blue

这些内容不会出现在编辑时的主要文本流中,但可以被游戏读取,并根据你的需要加以使用。有关更多信息,请参阅运行ink

2) 选择

输入是通过文本选择提供给玩家的。文本选择由*字符表示。

如果没有给出其他流指令,一旦做出选择,故事将流到下一行文本中。

你好呀!
*    我很好!
    你好吗?

上面的脚本在游戏中会像下面这样显示:

你好呀!
1: 我很好!

> 1
我很好!
你好吗?

默认情况下,选择的文本会再次出现在输出中。

不显示选择文本

有些游戏将选择文本与其结果分开。在ink中,如果选择的文本在方括号[]中给出,则选择的文本将不会显示在选择后的文本中。

你好呀!
*    [也打招呼]
    我很好!你好吗?

上面的结果会如下显示:

你好呀!
1: 也打招呼

> 1
我很好!你好吗?

进阶:混合选择和输出文本

方括号实际上划分了选项内容。方括号之前的内容在选择和输出中都被打印出来,方括号内部的东西只显示在选择中。方括号后面的内容只显示在输出中。它提供了另一种有效地结束文本行的方式。

你好呀!
*    我 [很好!] 好得很!你好吗?
白白!

结果会是:

你好呀!
1: 我很好!
> 1
我好得很!你好吗?
白白!

这在编写对话选项时非常好用:

“你怎么了?”小朱问
*    “我好累[。"],”我重复着。
    “真的吗?”她回答,“哈哈!”

结果会是:

“你怎么了?”小朱问
1. "“我好累。”
> 1
“我好累,”我重复着。
“真的吗?”她回答,“哈哈!”

多个选择

为了做出真正的选择,我们需要提供选择。我们可以简单地列出它们:

“你怎么了?”小朱问
*    “我好累[。"],”我重复着。
    “真的吗?”她回答,“哈哈!”
*    “没什么事!”[]我回答她。
    “那就好”
*  “我感觉这次旅行糟透了[。“],我想回家了。”
“哈哈,”她嘲讽地回应,“现在我们坐在不知道停在哪里的大巴车上,怎么回?”

这些选项在游戏中会如下显示:

“你怎么了?”小朱问

1: “我好累。”
2: “没什么事!”
3: “我感觉这次旅行糟透了。”

> 3


“我感觉这次旅行糟透了,我想回家了。”

“哈哈,”她嘲讽地回应,“现在我们坐在不知道停在哪里的大巴车上,怎么回?”

上面的语法足以编写一组选项。在真正的游戏中,我们希望根据玩家的选择将流从一个点移动到另一个点。要做到这一点,我们需要引入更多的结构。

3) 结点(Knots)

内容的区块称为结点

为了让游戏分支,我们需要用名称标记内容的各个部分(就像老式游戏互动书的“第18段”那样)。

这些部分被称为“结点”,它们是ink内容的基本结构单位。

编写一个结点

一个结点的开始由两个或多个=表示,如下所示。

=== top_knot ===

(结尾的等号是可选的;而且名字必须是一个没有空格的单词,不支持使用汉字。)

结点的开始是一个头;接下来的内容将被包括在该结点内。

=== back_in_london ===

我们在早上10点到达了伦敦。

高级:一个使用结点的“打招呼”

当您启动一个ink文件时,结点之外的内容将自动运行。但结点不会。所以如果你开始使用结点来放置内容,你就需要告诉游戏该往哪里走。我们使用了一个转向箭头->,这将在下一节中详细介绍。

最简单的使用结点的脚本就像这样:

-> top_knot

=== top_knot ===
你好呀!

但是,ink不喜欢没有了结的结局,当它认为这种情况发生时,会在编译和/或运行时产生警告,如上面的脚本在编译时就会产生:

WARNING: Apparent loose end exists where the flow runs out. Do you need a '-> END' statement, choice or divert? on line 3 of tests/test.ink

在运行时会产生:

Runtime error in tests/test.ink line 3: ran out of content. Do you need a '-> DONE' or '-> END'?

下面的脚本在运行时就不会有问题:

=== top_knot ===
你好呀!
-> END

-> END 是编写器和编译器的标记,意味着“故事流现在应该停止”。

4) 跳转(diverts)

结点间的跳转

你可以使用->“转向箭头”从一个结点移动到另一个结点。转移立即发生,无需任何用户输入。

=== back_in_london ===

我们在早上10点到达了伦敦。
-> hurry_home

=== hurry_home ===
我们很快地冲回了家。

跳转是看不见的

跳转的目标是实现情节的无缝衔接,甚至可以发生在句子中间:

=== hurry_back ===
我们很快地冲回了 -> home

=== home ===
家。

上面的脚本会如下显示:

我们很快地冲回了 家。

胶水(glue)

ink默认在每一行新内容之前插入换行符。然而,在某些情况下,内容必须坚持不使用换行符,并且可以使用<>或“glue”来做到这一点。

=== hurry_back ===
我们很快地冲回 <>
-> home

=== home ===
家
-> as_fast_as_we_could

=== as_fast_as_we_could ===
<> ,一刻没停。

会如下显示:

我们很快地冲回家,一刻没停。

你不能用太多胶水:多个胶水放在一起没有额外的效果。(没有办法“取消”胶水,一旦一行文本粘了,它就会一直粘下去。)

5) 让故事流分支

基础分支

把结点,选项和跳转结合起来,我们就有了一个choose-your-own game的基本结构。

=== paragraph_1 ===
你站在商场的门口,手机屏幕上的健康码闪着红光
* [排队进去] -> paragraph_2
* [飞速地跑进去] -> paragraph_3
* [转身回家] -> paragraph_4

=== paragraph_2 ===
你跟着人流,手机上切换到一张绿色的健康码截图,

...

分支与合流

使用转移,作者可以分支流程,并再次将其连接起来,而不用向玩家显示流程已重新连接。

=== back_in_london ===

我们早上9点到达了伦敦。

*    "现在没时间可以浪费了!"[] 我喊着
    -> hurry_outside

*    "等等,应该先做好消杀!"[] 我喊着
    小朱不管那么多,把我拽了出去,
    -> dragged_outside

*    [我们重回了家] -> hurry_outside


=== hurry_outside ===
我们飞速地回到了 -> home


=== dragged_outside ===
她坚持要我们先回
-> home


=== home ===
<> 家

故事流

结和跳转结合起来创造了游戏的基本故事流。这个流是“平坦的”——没有调用堆栈,也没有从那里“返回”转移。

在大多数ink脚本中,故事流从顶部开始,在像毛线团一样乱七八糟绕了一大圈后,最终才能够到达-> END

这种非常松散的结构意味着作者可以继续写作,分支和重新连接,而不用担心他们正在创建的结构。不需要创建新的分支或转移,也不需要跟踪任何状态。

进阶:循环(Loops)

你可以使用跳转来创建循环的内容,ink有几个特性可以利用这一点,包括使内容自行变化的方法,以及控制选择选项的频率的方法。

有关更多信息,可以参阅可变文本和有条件的选择部分。

还有,下面这条脚本是合法的,但不是个好主意:

=== round ===
然后,继续
-> round

6) 包括和针脚(Includes and Stitches)

结点可以进一步细分

随着故事越来越长,如果没有额外地结构来保持组织,他们会变得越来越混乱。

结可以包括称为“针脚”(stitches)的子部分。针脚都是用一个=标记的。

=== the_orient_express ===
= in_first_class
    ...
= in_third_class
    ...
= in_the_guards_van
    ...
= missed_the_train
    ...

例如,你可以用一个结点表示一个场景,用不同的针脚表示场景中的事件。

针脚有独特的名字

一个针脚可以通过它的“地址”被跳转过去,通过在结点名后加.再加上针脚名就可以了。

*    [Travel in third class]
    -> the_orient_express.in_third_class

*    [Travel in the guard's van]
    -> the_orient_express.in_the_guards_van

第一个针脚就是默认的

跳转向包含针脚的结点,会自动跳转到结点的第一个针脚,所以下面的脚本:

*    [Travel in first class]
    “要买商务舱是吧,还有啥要注意的不?”
    -> the_orient_express

和下面的脚本是一样的效果:

*    [Travel in first class]
    "要买卧铺是吧,还有啥要注意的不?"
    -> the_orient_express.in_first_class

(.除非我们改变针脚在结点内的顺序)

你还可以在结点内部、所有针脚外的顶部包含内容。然而,你需要记住要跳转出去——当游戏通过结点的标题进入结点时,它不会自动进入节点下的第一个针脚。

=== the_orient_express ===

我们上了火车,但去哪里呢?
*    [卧铺] -> in_first_class
*    [硬卧] -> in_second_class

= in_first_class
    ...
= in_second_class
    ...

本地跳转

在节点内部,不需要输入完整的地址就可以直接跳转到针脚。

-> the_orient_express

=== the_orient_express ===
= in_first_class
    我把小朱安排在卧铺。
    *    [Move to third class]
        -> in_third_class

= in_third_class
    我自己去坐了硬座。

这意味着针脚和结点不能使用相同名称,但几个结点可以包含相同的针脚名称。(所以不同的火车都可以睡卧铺。)

如果使用了模棱两可的名称,编译器将发出警告。

脚本文件可以合并

你还可以将内容拆分到多个文件中,在需要时使用include语句调用。

INCLUDE newspaper.ink
INCLUDE cities/vienna.ink
INCLUDE journeys/orient_express.ink

Include语句应该始终位于文件的顶部,而不是在knot中。

没有规则规定一个结点必须在哪个文件中才能被跳转到。(换句话说,分离文件对游戏的命名空间没有影响)。

7) 不同的选项

只能被选择一次的选项

默认情况下,游戏中的每个选项只能被选择一次。如果你的故事中没有循环,你就永远不会注意到这种行为。但是如果你使用循环,你很快就会发现你的选项消失了……

=== find_help ===

    你的目光绝望地在人群中梭巡,试图找到一张友善的脸。
    *    戴着帽子的女人[?] 粗暴地推开了你 。-> find_help
    *    拿着公文包的男人[?] 在你经过他时露出嫌恶的表情。 -> find_help

上面的脚本在游戏中会如下显示:

你的目光绝望地在人群中梭巡,试图找到一张友善的脸。

1: 戴着帽子的女人?
2: 拿着公文包的男人?

> 1
戴着帽子的女人粗暴地推开了你。
你的目光绝望地在人群中梭巡,试图找到一张友善的脸。

1: 拿着公文包的男人?

>

而在下一次循环你就没有选项可选了。

后备选项(Fallback choices)

上面的例子停止在它应该停止的地方,因为循环中没有选项了,下一个选择结束在“超出内容”的运行时错误中。

> 1
拿着公文包的男人在你经过他时露出嫌恶的表情。
你的目光绝望地在人群中梭巡,试图找到一张友善的脸。

Runtime error in tests/test.ink line 6: ran out of content. Do you need a '-> DONE' or '-> END'?

我们可以用“后备选项”来解决这个问题。后备选项永远不会显示给玩家,但如果没有其他选项,则由游戏“选择”。

后备选项就是“没有选择文本的选项”:

*    -> out_of_options

并且,稍微滥用一点语法,我们可以使用“选择然后箭头”(* ->)来创建一个包含内容的默认选项:

*     ->
    他很难解释他是如何在红码的情况下 -> season_2

备用选项示例

在前面的例子中加入备用选项,我们得到:

=== find_help ===

    你的目光绝望地在人群中梭巡,试图找到一张友善的脸。
    *    戴着帽子的女人[?] 粗暴地推开了你 。-> find_help
    *    拿着公文包的男人[?] 在你经过他时露出嫌恶的表情。 -> find_help
    *    ->
        但是已经太晚了,你 无力地倒在站台入口处。结局。
        -> END

在游戏中会这样显示:

 你的目光绝望地在人群中梭巡,试图找到一张友善的脸。

1: 戴着帽子的女人[?
2: 拿着公文包的男人[?

> 1
戴着帽子的女人[?] 粗暴地推开了你 。
你的目光绝望地在人群中梭巡,试图找到一张友善的脸。

1: 拿着公文包的男人[?

> 1
拿着公文包的男人[?] 在你经过他时露出嫌恶的表情。
你的目光绝望地在人群中梭巡,试图找到一张友善的脸。
但是已经太晚了,你 无力地倒在站台入口处。结局。

持久选项(Sticky choices)

当然,“只做一次”的行为并不总是我们想要的,所以我们有第二种选择:“持久”选择。持久的选择就只是一个不会被用完的选择,并且用+符号标记。

=== homers_couch ===
    +    [再吃一块大福]
        你又吃了一块大福 -> homers_couch
    *    [离开沙发]
        你挣扎着离开沙发,开始创作自己的巨作。
        -> END

后备选项也可以是持久的

=== conversation_loop
    *    [谈谈天气] -> chat_weather
    *    [谈谈孩子] -> chat_children
    +    -> sit_in_silence_again

有条件的选择

您还可以手动打开或关闭选项。Ink有很多可用的逻辑,但最简单的检验逻辑是“玩家是否看到了特定的内容”。

游戏中的每个结点/针脚都有一个唯一的地址(所以它可以被跳转到),我们使用相同的地址来测试该内容是否被看到过。

*    { not visit_paris }     [Go to Paris] -> visit_paris
+     { visit_paris      }         [Return to Paris] -> visit_paris

*    { visit_paris.met_estelle } [ Telephone Mme Estelle ] -> phone_estelle

注意,只要结点内的任何一个针脚被访问过,{结点名}的结果都会是true。

还要注意,条件不会覆盖选项的一次性行为,所以对于可重复的选项,你仍然需要持久选项

进阶:多项条件

你可以对一个选项使用多条逻辑检验;如果是这样,则必须通过所有检验才能显示该选项。

*    { not visit_paris }     [Go to Paris] -> visit_paris
+     { visit_paris } { not bored_of_paris }
    [Return to Paris] -> visit_paris

逻辑运算符:AND和OR

上面的“多个条件”实际上只是带有通常编程与操作符的条件。Ink也以通常的方式支持and(也写为&&)和or(也写为||)、以及括号的使用。

*    { not (visit_paris or visit_rome) && (visit_london || visit_new_york) } [ Wait. Go where? I'm confused. ] -> visit_someplace

给不编程人的提示: X and Y 表示X 和Y 都需要是true, X or Y 表示其中一个需要是true。ink不支持异或(xor)。

你也可以使用标准的!来表示not,尽管它有时会混淆编译器认为{!Text}是一个只有一次的列表。我们推荐使用not,因为否定布尔检验感觉没什么意思。

进阶:结点/针脚的标签其实是阅读计数

下面的检验:

*    {seen_clue} [Accuse Mr Jefferson]

实际上检验的是一个整数,而不是一个真/假标志。以这种方式使用的结点或针脚实际上是一个整数变量,其中包含地址内容被玩家看到的次数。

如果它是非零,它将在上面的检验中返回true,但你也可以更具体地设置条件:

* {seen_clue > 3} [Flat-out arrest Mr Jefferson]

进阶:更多逻辑

Ink支持的逻辑和条件性比这里介绍的多得多——请参阅变量和逻辑部分。

8) 可变文本

文本可以变化

到目前为止,我们看到的所有内容都是静态的、固定的文本片段。但内容在输出时也可以发生变化。

序列,循环和其他替代方案

最简单的文本变体是由替代(alternatives)提供的,这些替代是根据某种规则进行选择的。Ink支持多种类型。替代内容写在花括号{…}内,其中的元素由垂直分隔线|符号分隔。

只有当一个内容被多次访问时,替代文本才能真正发挥作用!

替代的类型

序列(Sequences) (默认类型):

一个序列 (或者说”stopping block”)是一组备选项,它跟踪它被看到过的次数,并且每次都显示下一个元素。当它用完新内容时,它会继续显示最后一个元素。

广播伴随着电流声滋滋响了起来 {"三!"|"二!"|"一!"|一声爆炸后只剩下白噪声|但它没有发出别的声音}

{我花十英镑买了个汉堡吃|我给小朱也买了一个汉堡|我再也没钱买汉堡吃了}

自己做了实际可以游玩的示例,脚本如下:

我饿了,小朱说。->hungry

===hungry==
* [哦。]小朱说,我好饿!->hungry
* [那我去整点吃的吧。]到了商店,->food_shop

===food_shop==
{我花十英镑给自己买了个汉堡|我给小朱也买了一个汉堡|我再也没钱买汉堡吃了}
+[再买一个吧] ->food_shop
+[回去吧] ->seat

===seat===
回到了座位,汉堡好难吃
->DONE

循环(Cycles)(使用&标记):

循环和序列表现一样,不过它在用尽后会从头重复自己。

现在是星期 {&一|二|三|四|五|六|七} 。

一次性(Once-only) (使用!标记):

只有一次的替代方案就像序列一样,但是当它们没有新内容可显示时,它们什么也不显示。(你可以把只有一次的替代方案看作是最后一个条目为空的序列。)

他讲了一个笑话 {!我礼貌地笑了|我微笑了一下|我扯了扯嘴角|我无感了}

洗牌(Shuffles) (使~标记):

洗牌输出随机的结果

我丢了个硬币,是 {~正面|背面}。

替代文本的特点

替代文本中可以包括空内容。

我向前走了一步 {!||||然后灯灭了 -> eek}。

替代文本可以随意嵌套。

小朱 {&{反应很快地 |}拍打|抓住} {&打你的|中你的 {&手心|手背|大腿}}。

做了个实际可以玩的示例翻手背游戏,脚本如下:

我们来玩拍手背吧!
+ 好呀 ->play_hand

===play_hand==
小朱{~反应很快地|敏捷地|狡猾地|狠狠地|不小心|}{&拍|抓|扇}{~到你的弱点|中你的{~手心|手背|大腿}}。
+ 再来! ->play_hand

替代文本可以包含跳转声明

我 {等待着。|又等了一阵子。|陷入沉睡。|惊醒后又等了一会。|放弃,离开了。 -> leave_post_office}

替代文本还可以被放在选项文本中:

+     "你好啊 {&小朱|阿朱|朋友}!"[] 我喊着。

(…有一点需要注意:选项的文本不能以{开头,因为它看起来像一个条件。)

(…但是需要注意的是,如果你使用了\和空格进行转义,ink就会将花括号{}内的内容识别为文本。

+\    {&他们向着大海出发了|他们去往沙漠的方向|这群人顺着马路南下}

示例

我们可以在循环中使用替代方法去创造智能且能够追踪状态的游戏玩法。

下面一个打地鼠的单结点版本。请注意,这里使用了一次性选项和备用选项,以确保鼹鼠不会到处移动,并且游戏总是会结束:

=== whack_a_mole ===
    {我举起锤子|{~没打中!|什么都没又!|还是不行,它在哪里?|哈哈!打中了! -> END}}
    这只 {&老鼠|{&肮脏的|邪恶的|可恶的} {&生物|啮齿动物}}一定{就在这里|藏在那里|还在藏着|笑话我|还没被打中|完蛋了}!<>
    {我会给它厉害!|它这次逃不了了!}
    *     [{&攻击|砸|试试} 左上角]     -> whack_a_mole
    *  [{&朝着|打|砸} 右上角] -> whack_a_mole
    *  [{&轰击|锤} 中间] -> whack_a_mole
    *  [{&怒砸|狂打} 左下角]     -> whack_a_mole
    *  [{&打|试试} 右下角]     -> whack_a_mole
    *   ->
            你被饿死了,地鼠打败了你!
            -> END

可以点击体验打地鼠

这里有一些生活方式的建议。注意这个持续的选择——电视的诱惑永远不会消退:

=== turn_on_television ===
我{|第二次|又一次|再次}打开电视, 但 {没什么好看的,我关掉了电视|还是没什么有意思的东西|还不如之前有意思|只剩下些垃圾节目|一个关于牛油果的节目,而我不喜欢牛油果|什么也没在放}。
+    [再试试]             -> turn_on_television
*    [还是出门去吧]    -> go_outside_instead

=== go_outside_instead ===
-> END

预览:多行替代文本

Ink也有另一种格式来制作不同内容块的替代文本。详细信息请参阅多行区块部分。

有条件的文本

文本也可以根据逻辑检验而变化,就像选项一样。

{met_blofeld: "我见过他,但只是一小会儿。" }

还有

"他的真名是 {met_blofeld.learned_his_name: 小黑子|一个秘密}。"

条件文本可以显示为单独的行,也可以显示在内容的一个部分中。它们甚至可以嵌套,所以:

{met_blofeld: "我见过他,但只是一小会儿。他的真名是 {met_blofeld.learned_his_name: 小黑子|一个秘密}." | "我没见到他,他怎么了?" }

既可以产生:

"我见过他,但只是一小会儿。他的真名是小黑子。"

也可以是:

"我见过他,但只是一小会儿。他的真名是个秘密。"

或者:

"我没见到他,他怎么了?"

9) 游戏查询和函数(Game Queries and Functions)

Ink提供了一些关于游戏状态的有用的“游戏级别”查询,用于条件逻辑。它们不完全是语言的一部分,但它们总是可用的,而且作者不能编辑它们。在某种意义上,它们是该语言的“标准库函数”。

惯例是用全大写字母命名。

CHOICE_COUNT()

CHOICE_COUNT 返回目前到目前为止在当前块中创建的选项数量。举个例子:

*    {false} Option A
*     {true} Option B
*  {CHOICE_COUNT() == 1} Option C

上面的脚本生成两个选项B和C,这对于控制玩家在一个回合中获得多少选项非常有用。

TURNS()

这将返回游戏开始以来的游戏回合数。

TURNS_SINCE(-> knot)

TURNS_SINCE 返回自上次访问特定结点/针脚以来的移动次数(正式地,玩家输入)。

值为0表示“在当前区块的一部分被看到”。值为-1表示“从未见过该结点/针脚”。任何其他正值都意味着它已经被看到过那么多次了。

*    {TURNS_SINCE(-> sleeping.intro) > 10} 你觉得很累了...... -> sleeping
*     {TURNS_SINCE(-> laugh) == 0}  你努力想要停下大笑。

注意传递给TURNS_SINCE的参数是一个“跳转目标”,而不仅仅是knot地址本身(因为knot地址是一个数字-读取计数-而不是故事中的位置…)

Note that the parameter passed to TURNS_SINCE is a “divert target”, not simply the knot address itself (because the knot address is a number – the read count – not a location in the story…)

TODO: (requirement of passing -c to the compiler)

预览:在函数中使用TURNS_SINCE

TURNS_SINCE(->x) == 0 检验非常有用,通常值得把它包装成一个Ink函数。

=== function came_from(-> x)
    ~ return TURNS_SINCE(x) == 0

关于函数的部分更清楚地概述了这里的语法,但上面的脚本语句允许你这样说:

* {came_from(->  nice_welcome)} “到这里真好!”
* {came_from(->  nasty_welcome)}“咱们还是走快点吧!”

还能让游戏对玩家刚刚看到的内容做出反应。

SEED_RANDOM()

出于测试目的,通常需要固定随机数生成器,这样每次游戏时Ink都会产生相同的结果。你可以通过“种子”随机数系统来做到这一点。

~ SEED_RANDOM(235)

传递给种子函数的数是任意的,但是提供不同的种子将导致不同的结果序列。

进阶:更多查询

您可以创建自己的外部函数,尽管语法略有不同:请参阅下文中关于函数的部分。

Part 2: 编织(Weave)

到目前为止,我们一直在用最简单的方式构建分支故事,使用链接到“页面”的“选项”。

但这要求我们在故事中唯一地命名每个目的地,这可能会减慢写作速度,阻碍小分支的发展。

Ink拥有一个更强大的语法,旨在简化具有始终向前方向的故事流(正如大多数故事所做的,而大多数计算机程序所不做的那样)。

这种格式被称为“weave”,它建立在基本内容/选项语法的基础上,增添了两个新特性:集合标记-、还有选择和集合的嵌套。

1) 集合点(Gathers)

集合点,将故事流重新聚集在一起

让我们回到本文档第一次多选择的示例:

“你怎么了?”小朱问
*    “我好累[。"],”我重复着。
    “真的吗?”她回答,“哈哈!”
*    “没什么事!”[]我回答她。
    “那就好”
*  “我感觉这次旅行糟透了[。“],我想回家了。”
“哈哈,”她嘲讽地回应,“现在我们坐在不知道停在哪里的大巴车上,怎么回?”

在真正的游戏中,这三种选择都可能导致相同的结论——福克先生离开房间。我们可以使用一个集合点来做到这一点,而不需要创建任何新的结点,或添加任何跳转。

“你怎么了?”小朱问
*    “我好累[。"],”我重复着。
    “真的吗?”她回答,“哈哈!”
*    “没什么事!”[]我回答她。
    “那就好”
*  “我感觉这次旅行糟透了[。“],我想回家了。”
“哈哈,”她嘲讽地回应,“现在我们坐在不知道停在哪里的大巴车上,怎么回?”

-    我头抵着窗户看着外面的加油站,无言以对。

这就产生了如下的游戏过程:

“你怎么了?”小朱问

1: “我好累。”
2: “没什么事!”
3: “我感觉这次旅行糟透了。”

> 3


“我感觉这次旅行糟透了,我想回家了。”

“哈哈,”她嘲讽地回应,“现在我们坐在不知道停在哪里的大巴车上,怎么回?”
我头抵着窗户看着外面的加油站,无言以对。

选项和集合构成内容链

我们可以将这些集合-分支部分串在一起,形成始终向前运行的分支序列。

=== escape ===
我挤过人群,几个保安紧追我的脚步。

    *     我摸了摸口袋[] 里面鼓鼓囊囊的装满了N95,我心下一喜,跑得更快了 <>

    *  我集中精神[] 专心逃跑, <>

    *    我高兴地跳了起来。 <>

-     不远了!只要穿过前面的星巴克,保安们就再难追上了!

    *    我穿过星巴克的大门[]。怎么会这样?!
    *    我开始考虑之后的生活[]。这些口罩够我们用好久了,可是眼前的一幕让我呆滞。

-    街道上空无一人,甚至看不见一辆车。

这是最基本的编织方法。本节的其余部分详细介绍了允许编织嵌套、包含侧轨和跳转、在自身内部挑战的其他特性,二最重要的是,引用早期的选择来影响后面的选择的特性。

编织哲学(The weave philosophy)

编织不仅仅是分支流的方便封装;也是一种编写更稳定内容的方法。上面的逃跑示例已经有四条可能的路径,而更复杂的序列可能有更多可能路径。使用普通的跳转,作者必须通过逐点跟踪转向来检查链接,很容易出现错误。

有了编织,故事流可以保证从顶部开始,然后“下降”到底部。在基本的编织结构中,流的错误是不可能的,输出文本可以很容易地略读。这意味着我们不需要真正测试游戏中的所有分支,就可以确保它们能够正常工作。

编织还允许轻松地重新起草选择点;特别是,我们很容易因为多样性或节奏原因而打断一个句子并插入额外的选择,而不需要重新设计任何流程。

2) 嵌套的故事流Nested Flow

上面所示的编织是相当简单的“扁平”结构。无论玩家做什么,他们从上到下都需要相同的回合数。然而,有时某些选择需要更多的深度或复杂性。

为此,我们允许编织进行嵌套。

本节附带一个警告。嵌套编织非常强大,非常紧凑,但它们可能需要一点时间来适应!

可以被嵌套的选项

考虑下面的场景:

-     "好了,小斌,这是自杀还是他杀?"
*    "他杀!"
*     "自杀!"
-    小菲放下了她的笔记本,房间里别的人也都惊得张大了嘴巴。

第一个选项是“谋杀”或“自杀”。如果宣布自杀,就没有什么可做的了,但如果是谋杀,就需要一个后续问题——怀疑谁?

我们可以通过一组嵌套的子选项添加新选项。通过使用两个星号而不是一个星号,我们告诉脚本这些新选择是另一个选择的“一部分”。

-     "好了,小斌,这是自杀还是他杀?"
*    "他杀!"
   "那是谁干的呢?"
   **“是医生!”
   **“是小盐!”
   **“我自己!”
*     "自杀!"
-    小菲放下她的笔记本,房间里别的人也都惊得张大了嘴巴。

注意,使用缩进行来显示嵌套也是很好的风格,但编译器并不介意。)

如果我们想要向其他路线添加新的子选项,我们可以以类似的方式进行操作。

-     "好了,小斌,这是自杀还是他杀?"
*    "他杀!"
   "那是谁干的呢?"
   **“是医生!”
   **“是小盐!”
   **“我自己!”
*     "自杀!"
    “真的?小斌,你确定?”
   **“当然!”
   **“这还用说?”
-    小菲放下她的笔记本,房间里别的人也都惊得张大了嘴巴。

现在,最初选择的指责将导致具体的后续问题——但无论如何,在小菲客串出场的集合点上,故事流将重新聚集在一起。

但如果我们想要一个更大的子场景呢?

集合点也可以嵌套

有时候,问题不在于增加选择的数量,而在于增加一个以上的故事节奏。我们可以通过嵌套集合点和选项来做到这一点。

-     "好了,小斌,这是自杀还是他杀?"
*    "他杀!"
   "那是谁干的呢?"
   **“是医生!”
   **“是小盐!”
   **“我自己!”
   --“你开玩笑吧!”
   **“我很认真的。”
   **“哈哈”
*     "自杀!"
    “真的?小斌,你确定?”
   **“当然!”
   **“这还用说?”
-    小菲放下她的笔记本,房间里别的人也都惊得张大了嘴巴。

如果玩家选择了“谋杀”选项,他们将在他们的分支上有两个连续的选择-一个他们独享的完整的扁平编织。

进阶:集合点能做些什么

集合是直观的,但它们的行为有点难以用语言表达:一般来说,在做出一个选择后,故事会找到下一个不在更低级别的集合点,并跳转到它。

其基本思想是:选项将故事的路径分开,集合将它们重新组合在一起。(因此得名“编织”!)

你想嵌套多少层就可以嵌套多少层

上面,我们使用了两层嵌套:主故事流,和子故事流。但是你能下潜的深度是没有限制的。

-    "给我讲个故事吧,小斌!"
    *    "好吧,那我给你讲个故事..."
        * *     "从前有座山..."
                * * *     "...山里有座庙..."
                        * * * *  "... 庙里有个老和尚..."
                                * * * * *        "...小和尚让老和尚讲个故事..."
    *    "我不讲,睡吧!"
 -    夜还很长。

过一段时间,这个子嵌套就会变得难以阅读和操作,所以如果一个支线选择变得尾大不掉,把它转移到一个新的针脚会是个好的选择。

但是,至少在理论上,你可以把整个故事写成一个单一的编织。

示例:使用嵌套结点组成的对话

这是一个更长的例子:

- 我看着小朱
*    ... 然后我再也不能忍受了
    '我们这次出来是为了什么?'
    '我也不知道。' 她回复。
    * *     '不知道!'[]我惊讶地反问。
            她点点头。
            * * *     ”这怎么可以?!“
            * * *     ”好耶!“
            - - -     她再次微笑点头。
            * * *    ”那我们要先去哪里?“
                    “我们马上就会知道。”她回答。
            * * *    ”你笑什么?“
                    ”我想到开心的事。“
            * * *     我没再问了[。],而除了微笑,她什么也没有回应,<>
    * *     ”嗷[。”],“我回应一声,不知道该说些什么,
    - -     之后<>
*    ... 但我什么也没说[],然后 <>
- 我们在摇晃的车厢中沉默地结束了一天。
- -> END

如果用比较短的游玩路径:

我看着小朱

1: ... 然后我再也不能忍受了
2: ... 但我什么也没说

> 2
... 然后我们在摇晃的车厢中沉默地结束了一天。

比较长的路径:

我看着小朱

1: ... 然后我再也不能忍受了
2: ... 但我什么也没说

> 1
... 然后我再也不能忍受了
 '我们这次出来是为了什么?'
    '我也不知道。' 她回复。

1: ''不知道!'
2: '”嗷。”

> 1
 '不知道!'我惊讶地反问。
她点点头。

1: '”这怎么可以?!“
2: ”好耶!“

> 2
”好耶!“
她再次微笑点头。

1: ”那我们要先去哪里?“
2: ”你笑什么?“
3: 我没再问了。

> 2
”你笑什么?“
”我想到开心的事。“
之后,我们在摇晃的车厢中沉默地结束了一天。

希望这能够证明上面所阐述的理念:编织提供了一种紧凑的方式来提供大量分支和选择,但保证故事能够顺利地从头到尾结束!

3) 追踪一个编织

有时,编织结构是足够的。但当它不足够时,我们需要更多的控制。

编织内的大部分没有地址

默认情况下,编织中的内容行没有地址或标签,这意味着它们不能被跳转,也不能被检验。在最基本的编织结构中,选择会改变玩家走过的路径和看到的内容,但一旦编织完成,这些选择和路径就会被遗忘。

但是如果我们想要记住玩家所看到的内容,我们可以使用(label_name)语法在需要的地方添加标签。

But should we want to remember what the player has seen, we can – we add in labels where they’re needed using the (label_name) syntax.

集合点和选项可以添加标签

任何嵌套级别上的集合点都可以使用括号进行标记。

-  (top)

一旦被标记,集合点就可以被跳转到,或者在条件句中进行测试,就像结点和针脚一样。这意味着你可以使用之前的决策来改变织体内部的后续结果,同时仍然保持清晰、可靠的正向流的所有优势。

选项也可以标记,就像集合点一样,使用括号。标签括号放在一行中的条件之前。

这些地址可以在条件测试中使用,这对于创建由其他选项解锁的选项非常有用。

=== meet_guard ===
保安对着你的手机皱了皱眉头。

*     (greet) [友善地打招呼]
    “你好~”
*    (get_out) “让开[。”],“ 你粗暴地对保安说。

-     ”嗯...“保安沉吟着。

*    {greet}     ”你好~“ // 只在打招呼后出现这一选项

*     ”怎么了吗??“[] 你回复。

*    {get_out} [把保安推到一边]      //仅在你粗暴说话后
   你把保安推到一边,他紧盯着你,伸手去抓木棍。
    -> fight_guard             // 这条路线跳出这一编织。

-    ”没什么,“保安挥挥手,”你把口罩戴好,过去吧。“

使用范围

在同一块编织的内部,你可以简单地使用标签名称;从块的外面,你需要一个路径,就算是同一结内的不同针脚也需要:

=== knot ===
= stitch_one
    - (gatherpoint)随便一些内容
= stitch_two
    *    {stitch_one.gatherpoint} 经过随便的那些内容后才能选的选项

或者是指向另一个结点:

=== knot_one ===
-    (gather_one)
    * {knot_two.stitch_two.gather_two} 选项

=== knot_two ===
= stitch_two
    - (gather_two)
        *    {knot_one.gather_one} 选项

进阶:所有的选项都可以被打标签

事实上,所有的内容在Ink中都是编织,即使没有看到集合点。这意味着你可以用括号标记游戏中的任何选项,然后使用寻址语法引用它。特别是,这意味着你可以检验玩家采取过哪种选择来达到特定的结果。

=== fight_guard ===
...
= throw_something
*    (rock) [对保安扔石头] -> throw
*     (sand) [对保安撒沙子] -> throw

= throw
你朝保安的头扔了{throw_something.rock:一块石头|一把沙子} 。

进阶:编织中的循环

标签允许我们在编织中创建循环。以下是向NPC提问的标准模式。

- (opts)
    *    ”我能从哪拿一套制服吗?“[] 你向这个喜形于色的保安提问。
        ”当然,就在柜子里。“他大嘴一咧,”不过你肯定穿着不合身。“
    *    ”给我讲讲这里的安保系统。“
        ”很老了,“保安很确定地说,"比这老榆树还破。"
    *   ”这里有狗吗?“
       ”几百条吧“,保安的笑容突然一哆嗦,”个个都跟饿死鬼似的。“
    // 我们需要玩家必须选一个选项才出现这个说够了的选项
    *    {loop} [说够了]
        -> done
- (loop)
    //在保安不耐烦前循环好几次
    { -> opts | -> opts | }
    他挠挠头。
    ”好了好了,总不能在这说一整天话吧。“他摇摇头,看向别处。
- (done)
    你感谢了这位保安,离开了。

进阶:跳转到选项

选项也可以被跳转到:但会跳转到已经选择了那个选项后的输出,就好像这个选项已经被选择了一样。因此输出的内容将忽略方括号中的文本,如果选项是一次性的,它将被标记为已用尽。

- (opts)
*    [做个鬼脸]
    你做了个鬼脸,保安冲着你来了! -> shove

*    (shove) [推开保安] 你把保安推到一边,但他很快站定了追来。

*    {shove} [扭打] -> fight_the_guard

-     -> opts

会显示:

1: 做个鬼脸
2: 推开保安

> 1
你做了个鬼脸,保安冲着你来了!你把保安推到一边,但他很快站定了追来。

1: 扭打

>

进阶:在一个选项后直接进入集合点

下面的脚本是有效的,而且经常有用:

*     ”你怎么样了,小朱?“[] 我问。
    - - (quitewell) ”好得很,“她回答。
*    ”你玩成语接龙玩得怎么样,小朱?“[] 我问。
    -> quitewell
*    我什么也没说,[] 小朱也是。
-    我们又一次陷入了友好的沉默。

请注意第一个选项正下方的第2级集合点:实际上,这里没有什么可以集合的东西,但它为我们的第二个选项提供了一个方便跳转的地方。

第三部分:变量和逻辑

到目前为止,我们已经创造了条件文本和条件选择,并基于玩家目前所看到的内容进行测条件检验。

Ink还支持变量,包括临时变量全局变量,用于存储数值和内容数据,甚至故事流命令。它在逻辑方面功能齐全,并包含一些额外的结构,以帮助更好地组织分支故事的复杂逻辑。

1) 全局变量

最强大的一种变量,可以说是对故事最有用的变量,是存储关于游戏状态的一些独特属性的变量——从主角口袋里的钱的数量,到代表主角精神状态的值。

这种变量被称为“全局”,因为它可以从故事中的任何地方访问——设置和读取。(传统上,编程试图避免这种事情,因为它允许程序的一部分与另一个不相关的部分混淆。但故事就是故事,故事都是关于结果的:在拉斯维加斯发生的事情很少会停留在那里。)

定义全局变量

通过VAR语句,可以在任何地方定义全局变量。它们应该被赋予一个初始值,该值定义了它们是什么类型的变量——整数、浮点数(十进制)、内容或故事地址。

VAR knowledge_of_the_cure = false
VAR players_name = "小朱"
VAR number_of_infected_people = 521
VAR current_epilogue = -> they_all_die_of_the_plague

使用全局变量

我们可以检验全局变量来控制选项,并提供条件文本,这与我们之前看到的类似。

=== the_train ===
    火车震颤起来,发出吱吱响声。{ mood > 0:我心情还不错,所以没太在意|我根本不能忍受}。
    *    { not knows_about_reason }”但是,小朱,我们要去做什么呢?“[] 我问。
    *     { knows_about_reason}我预想着我们的奇妙冒险[]。这一切可能吗?

进阶:将跳转存储为变量

“divert”语句本身就是一种值类型,可以存储、修改和被跳转到。

VAR     current_epilogue = -> everybody_dies

=== continue_or_quit ===
现在放弃,还是继续努力拯救你的王国?
*  [继续努力!]     -> more_hopeless_introspection
*  [放弃]         -> current_epilogue

进阶:全局变量在外部是可见的

全局变量可以从运行时和故事中访问和改变,因此提供了一种在更广泛的游戏和故事之间进行交流的好方法。

Ink层通常是存储游戏玩法变量的好地方;不需要考虑保存/加载问题,故事本身可以对当前值做出反应

输出变量值

变量的值可以使用类似于序列的内联语法打印为内容,也可以打印为条件文本:

VAR friendly_name_of_player = "小斌"//这种变量内容支持中文,真不赖
VAR age = 26

我的名字是哆啦a孟,但我的朋友们会叫我 {friendly_name_of_player},我现在 {age}岁了。

这在调试中很有用。有关基于逻辑和变量的更复杂的输出,请参阅函数部分。

评估字符串

可能会注意到,上面我们提到的变量能够包含“内容(content)”,而不是“字符串(strings)”。这是有意为之,因为用Ink定义的字符串可以包含ink语法——尽管它总是会被计算为字符串。(哎呀!)

VAR a_colour = ""

~ a_colour = "{~红|蓝|绿|黄}"

{a_colour}

上面的脚本会输出红、蓝、绿、黄之一。

注意,一旦这样的内容被评估,它的值就是“粘性”的。(量子态坍缩了!)下面的脚本:

你被打中了,眼冒金星 ,{a_colour} 和 {a_colour}。

并不会产生一种很有趣的效果。(如果你真的想要这种效果,使用一个文本函数来输出颜色!)

这也是为什么:

VAR a_colour =  "{~红|蓝|绿|黄}"

这样的脚本是明确不允许的。它将根据故事的结构进行评估,这可能不是你想要的。

2) 逻辑

显然,我们的全局变量并不是常量,所以我们需要一种语法来修改它们。

因为默认情况下,Ink脚本中的任何文本都是直接输出到屏幕上的,所以我们使用标记符号来指示一行内容要做一些数值工作,我们使用~标记。

下面的语句都在为变量赋值:

=== set_some_variables ===
    ~ knows_about_reason = true
    ~ x = (x * x) - (y * y) + c
    ~ y = 2 * x * y

下面的脚本会检验变量:

{ x == 1.2 }
{ x / 2 > 4 }
{ y - 1 <= x * x }

数学运算

Ink支持四种基本的数学运算(+,-,*和/),以及%(或mod),它返回整数除法后的余数。还有POW表示乘方:

{POW(3, 2)} 是9。
{POW(16, 0.5)} 是4。

如果需要更复杂的操作,可以编写函数(必要时使用递归),或者调用外部游戏代码函数(对于更高级的操作)。

RANDOM(min, max)

如果需要,Ink可以使用random函数生成随机整数。RANDOM被编写成一个骰子(是的,我们说的是一个骰子),所以最小值和最大值都包括在内。

~ temp dice_roll = RANDOM(1, 6)

~ temp lazy_grading_for_test_paper = RANDOM(30, 75)

~ temp number_of_heads_the_serpent_has = RANDOM(3, 8)

随机数生成器可以使用随机种子以用于测试目的,参见上面的游戏查询和功能部分。

进阶:数值类型是隐性的

运算的结果——特别是除法运算的结果——是基于输入类型的。整数除法返回整数,而浮点除法返回浮点结果。

~ x = 2 / 3
~ y = 7 / 3
~ z = 1.2 / 0.5

指定x为0,y为2,z为2.4。

进阶:INT(), FLOOR() and FLOAT()

如果不需要隐式类型,或者希望对变量进行舍入,则可以直接对其进行强制转换。

{INT(3.2)}是3.
{FLOOR(4.8)} 是4.
{INT(-4.8)}是 -4.
{FLOOR(-4.8)} 是 -5.

{FLOAT(4)} ...还是 4.

字符串查询

奇怪的是,作为一个文本引擎,Ink没有太多的字符串处理方式:它假定你需要做的任何字符串转换都将由游戏代码处理(也许由外部函数处理)。但是我们支持三个基本查询——相等、不相等和子字符串(我们称之为?原因将在后面的章节中明确)。

以下脚本都返回true:

{ "是的,谢谢" == "是的,谢谢" }
{ "不,谢谢" != "是的,谢谢" }
{ "是的,谢谢" ? "谢" }

3) 条件区块(Conditional blocks) (if/else)

我们已经看到条件句用于控制选项和故事内容;Ink还提供了普通if/else-if/else结构的等效形式。

一个简单的‘if’

if语法从到目前为止使用的其他条件句中得到启示,包括{…}语法,表示某些东西正在被测试。

{ x > 0:
    ~ y = x - 1
}

加上Else的情况:

{ x > 0:
    ~ y = x - 1
- else:
    ~ y = x + 1
}

扩展if/else……if/else区块

上面的语法实际上是一个更一般的结构的特定情况,类似于另一种语言的“switch”语句:

{
    - x > 0:
        ~ y = x - 1
    - else:
        ~ y = x + 1
}

使用这种形式,我们可以包含’else-if’条件:

{
    - x == 0:
        ~ y = 0
    - x > 0:
        ~ y = x - 1
    - else:
        ~ y = x + 1
}

(注意,与其他内容一样,空白区域完全是为了可读性,没有语法意义。)

Switch区块

这里还有一个正儿八经的switch语句:

{ x:
- 0:     zero
- 1:     one
- 2:     two
- else: lots
}

示例:与上下文相关的内容

请注意,这些检验不一定是基于变量的,也可以像其他条件一样使用阅读计数。而要“做一些与当前游戏状态相关的内容”的时候,下面的结构会被非常频繁地使用:

=== dream ===
    {
        - visited_snakes && not dream_about_snakes:
            ~ fear++
            -> dream_about_snakes

        - visited_poland && not dream_about_polish_beer:
            ~ fear--
            -> dream_about_polish_beer

        - else:
            // 关于早餐的梦就没有特殊效果
            -> dream_about_marmalade
    }

这个语法的优点是易于扩展和确定优先级。

条件区块不局限于逻辑

条件块可以用来控制故事内容和逻辑:

我盯着小朱,
{ know_about_wager:
    <> ”你真的不是在开玩笑吗?“我问。
- else:
    <> ”咱们出来总得有个理由吧。“我说。
}
她没有回应,只是认真地看着书,火车咣当咣当。

你甚至可以把选项放在条件块中:

{ door_open:
    *     我大步离开车厢[],好像听到小朱在轻声地自言自语。             -> go_outside
- else:
    *    我请求离开[] ,小朱看起来很惊讶。     -> open_door
    *     我站起来去开门[]。小朱看起来泰然自若。-> open_door
}

...但是请注意,上述示例中缺乏编织语法和嵌套并不是偶然的:为了避免混淆工作中的各种嵌套,不允许在条件块中包含集合点。

多行区块

还有另一类多行块,它从上面扩展了替代系统。以下都是有效的,而且可能会做你期望的行为:

 // 序列:从头到尾轮流显示,最后一直显示最后一个
{ stopping:
    -    我进入赌场。
    -  我再次进入赌场。
    -  我又一次进入了赌场。
}

// 洗牌:随机显示一个
在牌桌上,我抽了一张牌:<>
{ shuffle:
    -     红心Ace
    -     黑桃K
    -     方块2
       “这次你输了!”赌场老板叫道。
}

// 循环:从头到尾轮流显示,到头后重新循环
{ cycle:
    - 我屏住呼吸。
    - 我不耐烦地等待。
    - 我呆住了。
}

// 一次性:轮流显示,直到每一个都显示过
{ once:
    - 我的好运会继续吗?
    - 我能赢吗?
}

进阶:修正后的洗牌(modified shuffles)

上面的洗牌块实际上是一个“洗牌循环”;这样它就会洗牌内容,播放一遍,然后重新洗牌,再次播放。

shuffle还有两个版本:

shuffle once 会将内容洗牌,播放它,然后什么都不做。

{ shuffle once:
-    阳光很辣。
-    天气很热。
}

shuffle stopping 将洗牌所有的内容(除了最后一个条目),一旦它被播放,它将坚持在最后一个条目。

{ shuffle stopping:
-     一辆靛青色的兰博基尼疾驰而过。
-    一辆亮黄色的悍马冲过。
-     这里有很多汽车。
}

4) 临时变量(Temporary Variables)

临时变量用于临时计算

有时,全局变量是笨拙的。Ink为快速计算提供了临时变量。

=== near_north_pole ===
    ~ temp number_of_warm_things = 0
    { blanket:
        ~ number_of_warm_things++
    }
    { ear_muffs:
        ~ number_of_warm_things++
    }
    { gloves:
        ~ number_of_warm_things++
    }
    { number_of_warm_things > 2:
        尽管身处冰天雪地,我还是感觉很暖和。
    - else:
        那是我有生以来最冷的一夜。
    }

在故事离开定义临时变量的针脚后,临时变量中的值将被丢弃。

结点和针脚可以采用参数

临时变量的一种特别有用的形式是参数。任何结点或针脚都可以被赋予一个值作为参数。

*    [指控医生]
        -> accuse("医生")
*    [指控小盐]
        -> accuse("小盐")
*    [指控我自己]
        -> accuse("我自己")

=== accuse(who) ===
    "我指控{who}!" 小斌喊道。
    ”真的?“ 小朱回问, "{who == "myself":你干的?|{who}?}"
    ”当然!“小斌回答。

... 如果你想将一个临时值从一个针脚传递到另一个针脚,你需要使用参数!

示例:一个递归的结点定义

在递归中使用临时变量是安全的(与全局变量不同),因此下面的方法可以正常运行:

-> add_one_to_one_hundred(0, 1)

=== add_one_to_one_hundred(total, x) ===
    ~ total = total + x
    { x == 100:
        -> finished(total)
    - else:
        -> add_one_to_one_hundred(total, x + 1)
    }

=== finished(total) ===
    "结果是{total}!" 你宣布。
    他恐怖地看着你。
    -> END

(事实上,这种定义就已经非常有用,Ink提供了一种特殊的结点,可以想象,它被称为函数(function),它具有一定的限制,可以返回一个值。请参阅下面的部分。)

进阶:将跳转目标作为参数传递

结点/针脚地址是一种类型的值,由->字符表示,可以存储和传递。因此,以下内容是合法的,而且通常是有用的:

=== sleeping_in_hut ===
    你扶着地躺好,闭上眼睛。
    -> generic_sleep (-> waking_in_the_hut)

===     generic_sleep (-> waking)
    你睡着,偶尔做梦。
    -> waking

=== waking_in_the_hut
    你从地上爬起来,准备继续你的旅程。

...但请注意generic_sleep定义中的->;这是Ink中需要输入参数的一种情况:因为它太容易意外地执行以下操作:

=== sleeping_in_hut ===
    你扶着地躺好,闭上眼睛。
    -> generic_sleep (waking_in_the_hut)

.. 它会将waking_in_the_hut的阅读计数发送到generic_sleep结点,然后尝试转向它。

5) 函数(Functions)

在knot上使用参数时,它们几乎就是通常意义上的函数了,但它们缺乏一个关键概念——调用堆栈和返回值的使用。

INK包括函数 :它们是结点,有以下限制和特点:

一个函数:

  • 不能包括针脚
  • 不能使用跳转或提供选项
  • 可以调用其他函数
  • 可以包括输出内容
  • 可以返回一个任意类型的值
  • 可以安全地递归

(其中一些可能看起来限制相当大,但要了解更多面向故事的调用堆栈风格(call-stack-style)的功能,请参阅隧道(Tunnels)一节。)

返回值通过~ Return语句提供。

定义和调用函数

要定义一个函数,只需将一个knot声明为:

=== function say_yes_to_everything ===
    ~ return true

=== function lerp(a, b, k) ===
    ~ return ((b - a) * k) + a

函数通过名称和括号调用,即使它们没有输入参数:

~ x = lerp(2, 8, 0.3)

*    {say_yes_to_everything()} 'Yes.'

与其他语言一样,函数一旦完成,就会将流返回到调用它的地方——尽管不允许跳转流,但函数仍然可以调用其他函数。

=== function say_no_to_nothing ===
    ~ return say_yes_to_everything()

函数并没有必须返回任何东西

函数不需要有返回值,可以简单地做一些值得打包的事情:

=== function harm(x) ===
    { stamina < x:
        ~ stamina = 0
    - else:
        ~ stamina = stamina - x
    }

...但要记住,一个函数不能跳转,所以虽然上面的脚本防止负耐力值,但它不会杀死一个耐力掉到零的玩家。

函数可以内联调用

函数可以在~内容行上调用,但也可以在一段内容期间调用。在这种情况下,如果有返回值,则打印返回值(以及函数想要打印的任何其他值)。如果没有返回值,则不打印任何内容。

默认情况下,内容是“沾着”(glued in)的,所以如下所示:

小朱看起来 {describe_health(health)}.

=== function describe_health(x) ===
{
- x == 100:
    ~ return "很有精神"
- x > 75:
    ~ return "挺有活力"
- x > 45:
    ~ return "有点疲累"
- else:
    ~ return "很难过"
}

会输出:

小朱看上去很有精神

示例

例如,你可以包括:

=== function max(a,b) ===
    { a < b:
        ~ return b
    - else:
        ~ return a
    }

=== function exp(x, e) ===
    // 返回x的e次方,其中e为整数
    { e <= 0:
        ~ return 1
    - else:
        ~ return x * exp(x, e - 1)
    }

Then:

2^5和3^3 的最大值是{max(exp(2,5), exp(3,3))}.

produces:

 2^5 和 3^3的最大值是32.

示例:把数字变成文字

下面的例子很长,但几乎出现在迄今为止的所有inkle游戏中。(回想一下,多行花括号内的连字符行表示“要检验的条件”,如果花括号以变量开头,则表示“要比较的值”。)

原文是阿拉伯数字转换为英文数字,所以比较长,汉字的按相同逻辑要实现相同效果会简单些,但下面的脚本在表示10001这种中间加零的数字时会有问题。

=== function print_num(x) ===
{
 
    - x >=10000:
        {print_num(x/10000)}万{x mod 10000>0:{print_num(x mod 10000)}}
    - x >= 1000:
        {print_num(x/1000)}千{ x mod 1000 > 0:{print_num(x mod 1000)}}
    - x >= 100:
        {print_num(x/100)}百{ x mod 100 > 0:{print_num(x mod 100)}}
    - x >=10:
        {print_num(x/10)}十{x mod 10:{print_num(x mod 10)}}
    - x == 0:
        零
    - else:
        { x < 10:
            { x mod 10:
                - 1: 一
                - 2: 二
                - 3: 三
                - 4: 四
                - 5: 五
                - 6: 六
                - 7: 七
                - 8: 八
                - 9: 九
            }

        }
}

就可以在下面这样调用:

~ price = 15

我从口袋里掏出 {print_num(price)} 硬币,慢慢地数着。
“哈哈,我拿一半就行” 商人说着,拿走了{print_num(price / 2)},把剩下的还给了我。

参数可以通过引用传递

函数参数也可以“通过引用”传递,这意味着函数实际上可以改变传入的变量,而不是用该值创建一个临时变量。

例如,大多数inkle故事包括以下内容:

=== function alter(ref x, k) ===
    ~ x = x + k

像这样的脚本行:

~ gold = gold + 7
~ health = health - 4

就可以成为:

~ alter(gold, 7)
~ alter(health, -4)

它们更容易阅读,并且(更有用)可以内联完成,以获得最大的紧凑性。

*    我喝了一口奶啤[],感觉好些了。 {alter(health, 2)}
*    我给小朱递过奶啤[] ,她一口饮尽。 {alter(foggs_health, 5)}
-    <> T我们继续旅程。

如果需要,在函数中包装简单的操作还可以提供一个放置调试信息的简单位置。

6) 常量

全局常量

交互式故事通常依赖于状态机,跟踪某个更高级别的进程已达到的阶段。有很多方法可以做到这一点,但最方便的是使用常量。

有时候,将常量定义为字符串是很方便的,所以你可以将它们打印出来,用于游戏玩法或调试目的。

CONST xiaozhu = "小朱"
CONST xiaobin = "小斌"
CONST xiaoheizi = "小黑子"

VAR current_chief_suspect = xiaozhu

=== review_evidence ===
    { found_japps_bloodied_glove:
        ~ current_chief_suspect = xiaobin
    }
    现在的嫌疑人: {current_chief_suspect}

有时候给常量一些值是有用的:

CONST PI = 3.14
CONST VALUE_OF_TEN_POUND_NOTE = 10

有时这些数字在其他方面也很有用:

CONST LOBBY = 1
CONST STAIRCASE = 2
CONST HALLWAY = 3

CONST HELD_BY_AGENT = -1

VAR secret_agent_location = LOBBY
VAR suitcase_location = HALLWAY

=== report_progress ===
{  secret_agent_location == suitcase_location:
    秘密特工抓住了箱子!
    ~ suitcase_location = HELD_BY_AGENT

-  secret_agent_location < suitcase_location:
    秘密特工前进着。
    ~ secret_agent_location++
}

常量只是一种让你给故事状态起一个容易理解的名字的方法。

7) 进阶:游戏侧的逻辑

在Ink引擎中提供游戏钩子有两种核心方法。Ink中的外部函数声明允许你在游戏中直接调用c#函数,变量观察者是当Ink变量被修改时在游戏中触发的回调。这两种方法在《运行你的ink》中都有描述。

第四部分:进阶的故事流控制

1) 隧道(Tunnels)

Ink故事的默认结构是一个“扁平”的选择树,分支和连接在一起,也许是循环的,但故事总是“在一个特定的地方”。

但这种扁平结构会让某些事情变得困难:例如,想象一款游戏中可能发生以下互动:

=== crossing_the_date_line ===
*    “小朱!”[] 我惊讶地大喊, “我才发现,我们刚刚跨过了国际日期变更线!”
-    小朱只是稍微抬了抬眼皮“我已经调整好时间了。”
*    我擦掉眉毛上的汗珠[],幸好!
*    我点点头,冷静下来[],她当然早有准备!
*  我低声咒骂着[],又一次,我又一次被瞧不起了!

...但它可以发生在故事的几个不同的地方。我们不希望必须为每个不同的地方编写相同内容的副本,但是当内容完成时,它需要知道应该返回到哪里。我们可以使用参数来做到这一点:

=== crossing_the_date_line(-> return_to) ===
...
-    -> return_to

...

=== outside_honolulu ===
我们抵达了火奴鲁鲁的大岛。
- (postscript)
    -> crossing_the_date_line(-> done)
- (done)
    -> END

...

=== outside_pitcairn_island ===
船沿水向小岛驶去。
- (postscript)
    -> crossing_the_date_line(-> done)
- (done)
    -> END

这两个位置现在都调用并执行故事流的同一段,但一旦完成,它们就会返回到下一个需要去的地方。

但是,如果被调用的故事部分更复杂呢——如果它跨越了几个结点呢?使用上面的方法,我们必须在每个结之间传递’return-to‘参数,以确保我们总是知道返回的位置。

因此,ink用一种新的分流方式将其集成到语言中,其功能更像子程序,被称为“隧道”。

隧道运行支线故事

隧道语法看起来像一个跳转,在后面又接了另一个跳转:

-> crossing_the_date_line ->

这意味着“完成crossing_the_date_line故事,然后从这里继续”。

在隧道内部,语法从参数化的例子中简化了:我们所做的就是使用->->语句结束隧道,这实际上意味着“继续”。

=== crossing_the_date_line ===
// this is a tunnel!
...
-     ->->

注意,隧道结没有这样声明,所以编译器不会检查隧道是否真的以->->语句结束,除非在运行时。因此,您将需要仔细编写,以确保所有进入隧道的流确实再次流出。

隧道也可以连接在一起,或者在一个普通的跳转结束。

...
// 这条脚本首先经过该隧道,然后跳转到'done'
-> crossing_the_date_line -> done
...

...
//这条脚本首先经过该隧道,又是一条隧道,然后跳转到'done'this runs one tunnel, then another, then diverts to 'done'
-> crossing_the_date_line -> check_foggs_health -> done
...

隧道是可以嵌套的,所以下面的脚本是有效的:

=== plains ===
= night_time
    你脚下的黑草非常柔软。
    +    [Sleep]
        -> sleep_here -> wake_here -> day_time
= day_time
    是时候出发了。

=== wake_here ===
    你醒来时太阳刚刚升起。
    +    [Eat something]
        -> eat_something ->
    +    [Make a move]
    -    ->->

=== sleep_here ===
   你躺了下来,闭上眼睛。
    -> monster_attacks ->
    终于能好好睡觉了。
    -> dream ->
    ->->

…就像这样。

进阶:隧道可以返回到其他地方

有时候,在一个故事中,什么事情都会发生。所以有时隧道不能保证它总是想回到它来的地方。Ink提供了一种语法,允许你“从隧道返回,但实际上去了其他地方”,但应该谨慎使用,因为混淆的可能性确实非常高。

然而,在某些情况下,它是必不可少的:

=== fall_down_cliff 
-> hurt(5) -> 
你还活着!你拍打拍打尘土,继续前进。

=== hurt(x)
    ~ stamina -= x 
    { stamina <= 0:
        ->-> youre_dead
    }

=== youre_dead
突然,你周围出现了一道白光。手指从额头上拿起目镜。“你输了,伙计。从椅子上站起来。”

甚至在不那么极端的情况下,我们可能想要打破结构:

-> talk_to_jim ->

 === talk_to_jim
 - (opts)     
    *    [ 询问关于跃迁大门的事 ] 
        -> warp_lacells ->
    *    [ 询问关于护盾生成器的事] 
        -> shield_generators ->    
    *     [ S不说了]
        ->->
 - -> opts 

 = warp_lacells
    { shield_generators : ->-> argue }
    "别担心跃迁大门的事,它现在还很稳定。"
    ->->

 = shield_generators
    { warp_lacells : ->-> argue }
    "护盾发生器都很正常,不用担心。"
    ->->

 = argue 
     ”你问这么多问题有什么居心?“小朱问道。
     ...
     ->->

进阶:隧道使用调用栈

隧道位于调用堆栈上,因此可以安全地递归。

2) 线程

到目前为止,墨水中的一切都是完全线性的,尽管有各种分支和转移。但实际上,作者可以将故事“分叉”(fork)成不同的子部分,以涵盖更多可能的玩家行动。

我们称之为“线程”,尽管它并不是计算机科学家真正意义上的线程:它更像是拼接来自不同地方的新内容。

注意,这绝对是一个高级特性:一旦涉及到线程,故事就会变得更加复杂!

线程将多个部分连接在一起

线程允许您从多个来源组合部分的内容。例如:

== thread_example ==
我的头痛起来,多线程是很难让你的脑筋绕过去的。
<- conversation
<- walking


== conversation ==
我和小朱之间的气氛变得很紧张。
 * “你今天午饭吃了什么?”[] 我问。
    “食其家。”她说。
 * “今天天气真不错。”[] 我说。
    “之前的天气更好。”她答。
 - -> house

== walking ==
我们继续沿着土路前进。
 * [继续走路]
    -> house

== house ==
不久,我们就到了她家。
-> END

它允许故事的多个部分组合在一起成为一个单独的部分:

我的头痛起来,多线程是很难让你的脑筋绕过去的。
我和小朱之间的气氛变得很紧张。
我们继续沿着土路前进。
1: “你今天午饭吃了什么?”
2: “今天天气真不错。”
3: 继续走路

在遇到诸如<- conversation之类的线程语句时,编译器将故事流分叉。遇见的第一个分支将运行conversation中的内容,收集它找到的任何选项。一旦它搜尽这里的故事流,它就会运行另一个分叉。

所有内容都将被收集并呈现给玩家。但当玩家做出选择时,引擎便会转向故事的分叉,并抛弃其他内容。

注意,全局变量不被分叉,包括结点和针脚的读阅读计数。

线程的使用

在正常的故事中,可能永远都不需要线程。

但是对于拥有许多独立移动部件的游戏来说,线程便变得非常重要。想象一款角色在地图上独立移动的游戏:房间的主要故事中心可能是这样的:

CONST HALLWAY = 1
CONST OFFICE = 2

VAR player_location = HALLWAY
VAR generals_location = HALLWAY
VAR doctors_location = OFFICE

== run_player_location
    {
        //如果玩家位置在hallway,那么跳转到hallway
        - player_location == HALLWAY: -> hallway
    }

== hallway ==
    <- characters_present(HALLWAY)
    *    [抽屉]    -> examine_drawers
    *     [衣柜] -> examine_wardrobe
    *  [去办公室]     -> go_office
    -    -> run_player_location
= examine_drawers
    // 叽里咕噜

//这就是线程,它混进了与你共处一室的角色的对话。

== characters_present(room)
    { generals_location == room:
        <- general_conversation
    }
    { doctors_location == room:
        <- doctor_conversation
    }
    -> DONE

== general_conversation
    *    [询问船长染血小刀的事]
        “我只能告诉你,情况很糟。”
    -    -> run_player_location

== doctor_conversation
    *    [询问医生染血小刀的事]
        “我对这血一无所知。”
    -    -> run_player_location

特别需要注意的是,我们需要一种明确的方法将玩家从侧支线返回到主线。在大多数情况下,线程要么需要一个参数来告诉它们返回到哪里,要么需要结束当前的故事部分。

支线程什么时候结束?

边线程在耗尽能够处理的故事流时结束:注意,它们收集选项以稍后显示(不像隧道,会收集选项,显示它们并跟着它们前进,直到它们到达一个明显的返回,这一切可能发生在几步之后)。

有时候,一个线程并没有提供任何内容——也许根本就没有与角色的对话,或者我们只是还没有写出来。在这种情况下,我们必须显式地标记线程的结束。

如果我们不这样做,内容的结尾可能是一个故事bug或一个挂起的故事线程,我们希望编译器告诉我们这些。

使用 -> DONE

在我们想要标记线程结束的情况下,我们使用-> DONE:意思是“故事流故意在这里结束”。如果我们不这样做,我们可能会得到一个警告信息——我们仍然可以玩这个游戏,但它会提醒我们还有未完成的事情。

本节开头的示例将生成一个警告,可以按照下面这样修复:

== thread_example ==
我的头痛起来,多线程是很难让你的脑筋绕过去的。
<- conversation
<- walking
-> DONE

添加的DONE告诉Ink这里的流动已经结束,它应该依赖于故事的下一部分的线程。

注意,如果流以不符合条件检验的选项结束,我们不需要-> DONE。引擎将此视为有效的、有意的流结束状态。

你在一个选项已被选择后不需要-> DONE。一旦选择了一个选项,一个线程就不再是一个线程——它只是一个正常的故事流程。

在这种情况下使用-> END不会结束线程,而是整个故事流。(这就是为什么要用两种不同的方式来结束故事流。)

示例:在几个地方添加相同的选项

线程可以用于将相同的选项添加到许多不同的地方。当以这种方式使用它们时,通常会将一个跳转作为参数传递,以告诉故事在选择完成后去哪里。

=== outside_the_house
门前,这房子传出气味,是谋杀和薰衣草的味道。
- (top)
    <- review_case_notes(-> top)
    *    [走过前门]
        我走进房子。
        -> the_hallway
    *     [闻闻空气]
         我讨厌薰衣草,薰衣草让我想起肥皂,肥皂让我想起我的婚姻。
        -> top

=== the_hallway
走廊。正对着街道的门敞开着,可爱的小书桌。
- (top)
    <- review_case_notes(-> top)
    *    [走过前门]
       我走进凉爽的日光中。
        -> outside_the_house
    *     [打开小书桌]
        钥匙,钥匙,又是钥匙,他们为什么会需要这么多钥匙?
        -> top

=== review_case_notes(-> go_back_to)
+    {not done || TURNS_SINCE(-> done) > 10}
    [检查我的案件笔记]
    // 条件检验确保您不会得到重复检查的选项
     {我|又一次,我} 浏览了一下我到目前为止做的笔记,仍然没有明显的嫌疑人。
-     (done) -> go_back_to

注意,这与隧道不同,隧道运行相同的内容块,但不给玩家选择。这样的布局:

<- childhood_memories(-> next)
*    [看向窗外]
     我们一路前行,我做着白日梦……
 - (next)然后哨子响了……

可能会和下面的脚本 做同样的事情:

*    [回忆我的童年]
    -> think_back ->
*    [看向窗外]
    我们一路前行,我做着白日梦……
-     (next)然后哨子响了……

但是,只要线程中的选项包含多个选项,或者这些选项包括条件逻辑(当然,也可以是任何文本内容!),线程版本就会变得更加实用。

示例:广泛选择点的组织

使用Ink作为脚本而不是文字输出的游戏通常会产生大量的并行选择,旨在通过其他游戏内部互动(比如在一个环境中走动)由玩家过滤。在这些情况下,线程可以用来划分不同的选择。

=== the_kitchen
- (top)
    <- drawers(-> top)
    <- cupboards(-> top)
    <- room_exits
= drawers (-> goback)
    // 关于抽屉的选择
    ...
= cupboards(-> goback)
    // 关于茶柜的选择
    ...
= room_exits
    // 出口,在你离开时不需要一个返回点。你会去到别处。
    ...

第五部分:进阶的状态查询

带有大量互动的游戏可能会很快变得非常复杂,而在作者的工作中,保持内容的连续性与内容本身一样重要。

如果游戏文本是为了模拟任何东西——无论是纸牌游戏,玩家对游戏世界的了解,还是房子里各种电灯开关的状态,这就变得尤为重要。

ink并没有以经典解析器IF创作语言的方式提供一个完整的世界建模系统——没有“对象”,没有“包容”或“开放”或“锁定”的概念。但是,它确实提供了一个简单而强大的系统,以非常灵活的方式跟踪状态变化,使编写人员能够在必要时近似世界模型。

注意:新功能警报!

这个特性对于该语言来说非常新。这意味着我们还没有开始发现它可能被使用的所有方式,但我们非常确定它会很有用!所以如果你想到一个聪明的用法,欢迎告诉我们!

1) 基本列表(Basic Lists)

状态跟踪的基本单元是使用LIST关键字定义的状态列表。注意,列表与c#列表(它是一个数组)完全不同。

例如,我们可能有:

LIST kettleState = cold, boiling, recently_boiled

这一行定义了两件事:首先是三个新值——coldboilrecently_boiled——其次是一个名为kettleState的变量,用于保存这些状态。

我们可以告诉列表取什么值:

~ kettleState = cold

我们可以更改值:

*    [打开水壶]
   水壶开始冒泡沸腾。
    ~ kettleState = boiling

我们可以查询该值:

*    [碰碰水壶]
    { kettleState == cold:
        这水壶摸起来冰冰的。
    - else:
         水壶摸起来很温!
    }

为了方便起见,我们可以在使用括号()在定义列表时给它一个值:

LIST kettleState = cold, (boiling), recently_boiled
// at the start of the game, this kettle is switched on. Edgy, huh?

…如果这个符号看起来有点多余,这是有原因的,在后面的小节中会讲到。

2) 重用列表

上面的例子适用于水壶,但如果我们在炉子上也有一个锅呢?然后,我们可以定义一个状态列表,但将它们放入多个变量中——我们想要多少变量就有多少变量。

LIST daysOfTheWeek = Monday, Tuesday, Wednesday, Thursday, Friday
VAR today = Monday
VAR tomorrow = Tuesday

状态可以被重复使用

这允许我们在多个地方使用相同的状态机。

LIST heatedWaterStates = cold, boiling, recently_boiled
VAR kettleState = cold
VAR potState = cold

*    {kettleState == cold} [Turn on kettle]
    水壶开始沸腾冒泡。
    ~ kettleState = boiling
*    {potState == cold} [点燃火炉]
     锅里的水开始沸腾冒泡。
     ~ potState = boiling

但是如果我们再加一个微波炉呢?我们可能想要开始泛化我们的功能:

LIST heatedWaterStates = cold, boiling, recently_boiled
VAR kettleState = cold
VAR potState = cold
VAR microwaveState = cold

=== function boilSomething(ref thingToBoil, nameOfThing)
     {nameOfThing}开始加热了。
    ~ thingToBoil = boiling

=== do_cooking
*    {kettleState == cold} [打开水壶]
    {boilSomething(kettleState, "kettle")}
*    {potState == cold} [点亮火炉]
    {boilSomething(potState, "pot")}
*    {microwaveState == cold} [打开微波炉]
    {boilSomething(microwaveState, "microwave")}

或者更进一步:

LIST heatedWaterStates = cold, boiling, recently_boiled
VAR kettleState = cold
VAR potState = cold
VAR microwaveState = cold

=== cook_with(nameOfThing, ref thingToBoil)
+     {thingToBoil == cold} [打开 {nameOfThing}]
       {nameOfThing}开始加热了。
    ~ thingToBoil = boiling
    -> do_cooking.done

=== do_cooking
<- cook_with("kettle", kettleState)
<- cook_with("pot", potState)
<- cook_with("microwave", microwaveState)
- (done)

请注意,“heatedWaterStates”列表仍然可用,并且仍然可以测试,并取一个值。

列表值可以共享名称

重用列表会带来歧义。如果我们有:

LIST colours = red, green, blue, purple
LIST moods = mad, happy, blue

VAR status = blue

... 编译器怎么知道你指的是哪个蓝色呢?

我们使用一个.的语法来解决这些问题,类似于用于结点和针脚的语法。

VAR status = colours.blue

编译器直到你指定列表名前都将发出一个错误警告。

注意,状态的“家族名称”和包含状态的变量是完全独立的。所以

{ statesOfGrace == statesOfGrace.fallen:
    // 检验当前的状态是否是"fallen"
}

…是正确的

进阶:一个列表其实是一个变量

一个令人惊讶的特点是下面的声明:

LIST statesOfGrace = ambiguous, saintly, fallen

它实际上同时做了两件事:它创造了三个值,ambiguoussaintlyfallen,并在需要时赋予它们statesOfGrace的父名;它创建了一个名为statesOfGrace的变量。

这个变量可以像普通变量一样使用。所以下面的脚本是正确的,尽管它让人非常困惑,而且是个坏主意:

LIST statesOfGrace = ambiguous, saintly, fallen

~ statesOfGrace = 3.1415 // 把变量设为数字而非状态值

...但这并不妨碍以下情况的发生:

~ temp anotherStateOfGrace = statesOfGrace.saintly

3) 列表的值

定义列表时,值按顺序列出,该顺序被认为是重要的。事实上,我们可以把这些列表值当作数字来处理。(也就是说,它们是枚举。)

LIST volumeLevel = off, quiet, medium, loud, deafening
VAR lecturersVolume = quiet
VAR murmurersVolume = quiet

{ lecturersVolume < deafening:
    ~ lecturersVolume++

    { lecturersVolume > murmurersVolume:
        ~ murmurersVolume++
       低语声越来越大
    }
}

值本身可以使用通常的{…}语法输出,但这将打印它们的名称。

演讲者的声音变得 {lecturersVolume}。

将值转换为数字

如果需要,可以使用LIST_VALUE函数显式地获得数值。注意,列表中的第一个值是1,而不是0。

The lecturer has {LIST_VALUE(deafening) - LIST_VALUE(lecturersVolume)} notches still available to him.

将数字转换为值

你可以用另一种方法,使用列表的名称作为函数:

LIST Numbers = one, two, three
VAR score = one
~ score = Numbers(2) // score will be "two"

进阶:定义你自己的数值

默认情况下,列表中的值从1开始,每次增加1,但如果需要,您可以指定自己的值。

LIST primeNumbers = two = 2, three = 3, five = 5

如果你指定了一个值,没指定下一个值,则ink将假定增量为1。所以下面和上面的脚本效果是一样的:

LIST primeNumbers = two = 2, three, five = 5

4) 多值的列表

下面的例子都包含了一个故意的谎言,我们现在要将其删除。列表——以及包含列表值的变量——不必只包含一个值。

列表是布尔集

列表变量不是包含数字的变量。更确切地说,列表就像住宿街区的进出铭牌。它包含一个名称列表,每个名称都有一个与之相关的房间号,以及一个表示“进”或“出”的滑块。

也许没有人在:

LIST DoctorsInSurgery = Adams, Bernard, Cartwright, Denver, Eamonn

也许每个人都在:

LIST DoctorsInSurgery = (Adams), (Bernard), (Cartwright), (Denver), (Eamonn)

或者可能有些人在,有些人不在:

LIST DoctorsInSurgery = (Adams), Bernard, (Cartwright), Denver, Eamonn

括号中的名字包含在列表的初始状态中。

注意,如果你在定义自己的值,你可以把括号放在整个术语或只是名称周围:

LIST primeNumbers = (two = 2), (three) = 3, (five = 5)

赋多个值

我们可以一次为列表中的所有值赋值,如下所示:

~ DoctorsInSurgery = (Adams, Bernard)
~ DoctorsInSurgery = (Adams, Bernard, Eamonn)

我们可以给空列表赋值来清空一个列表:

~ DoctorsInSurgery = ()

添加和删除条目

列表条目可以单独或一起添加和删除。

~ DoctorsInSurgery = DoctorsInSurgery + Adams
 ~ DoctorsInSurgery += Adams  // 这句和上一句效果一样
~ DoctorsInSurgery -= Eamonn
~ DoctorsInSurgery += (Eamonn, Denver)
~ DoctorsInSurgery -= (Adams, Eamonn, Denver)

尝试添加列表中已经存在的条目不会起任何作用。试图删除不存在的条目也没有任何作用。两者都不会产生错误,并且列表永远不能包含重复的条目。

基础查询

我们有几种获取列表信息的基本方法:

LIST DoctorsInSurgery = (Adams), Bernard, (Cartwright), Denver, Eamonn

{LIST_COUNT(DoctorsInSurgery)}     //  "2"
{LIST_MIN(DoctorsInSurgery)}         //  "Adams"
{LIST_MAX(DoctorsInSurgery)}         //  "Cartwright"
{LIST_RANDOM(DoctorsInSurgery)}     //  "Adams" or "Cartwright"

空性测试

像大多数Ink中的值一样,列表可以“按原样”测试,并且将返回true,除非它为空

{ DoctorsInSurgery: 诊所今天开着门。 | 每个人都回家了。 }

检验是否完全相等

测试多值列表比测试单值列表稍微复杂一些。equal(==)现在意味着“集合相等”——也就是说,所有的条目都是相同的。

所以有人可能会说:

{ DoctorsInSurgery == (Adams, Bernard):
    Dr Adams和Dr Bernard正在某个角落大吵一架。
}

如果Eamonn博士也在,这两个人就不会争论了,因为被比较的名单是不相等的——DoctorsInSurgery将有一个Eamonn,而这个名单(Adams, Bernard)没有。

不相等时的工作原理如下:

{ DoctorsInSurgery != (Adams, Bernard):
    至少Adams and Bernard没在吵架。
}

控制测试

如果我们只想问亚当斯和伯纳德是否在场呢?为此,我们使用一个新的运算符,has,或者称为?。

{ DoctorsInSurgery ? (Adams, Bernard):
    Dr Adams和Dr Bernard正在某个角落大吵一架。
}

然后?也可以应用于单个值:

{ DoctorsInSurgery has Eamonn:
    Dr Eamonn正在擦他的眼镜。
}

我们也可以用hasnt!?(不是?)。注意,这开始变得有点复杂

DoctorsInSurgery !? (Adams, Bernard)

这并不是说Adams或Bernard都不在场,而是说他们两个同时都不在场(并在争论)。

警告:没有包含空列表的列表

注意下面的检验

SomeList ? ()

将总是返回false,不管SomeList本身是否为空。在实践中,这是最有用的默认值,因为你经常想做这样的检验:

SilverWeapons ? best_weapon_to_use 

如果玩家是空手,就会返回false。

示例:基础知识跟踪

多值列表最简单的用法是整齐地跟踪“游戏标记(game flags)”。

LIST Facts = (Fogg_is_fairly_odd),     first_name_phileas, (Fogg_is_English)

{Facts ? Fogg_is_fairly_odd:我耐心地微笑。|我皱起眉头,他有毛病吗?}
'{Facts ? first_name_phileas:Phileas|先生},准备好!' 我喊着。

特别是,它允许我们在单行中检验多个游戏标志。

{ Facts ? (Fogg_is_English, Fogg_is_fairly_odd):
    <> '我知道英国人很奇怪,但这也*太怪了*!'
}

示例:医生的手术

我们还没提供一个更完整的例子,所以下面就是一个。

LIST DoctorsInSurgery = (Adams), Bernard, Cartwright, (Denver), Eamonn

-> waiting_room

=== function whos_in_today()
  今天在诊所的人有 {DoctorsInSurgery}。

=== function doctorEnters(who)
    { DoctorsInSurgery !? who:
        ~ DoctorsInSurgery += who
        {who} 医生匆忙地走了进来。
    }

=== function doctorLeaves(who)
    { DoctorsInSurgery ? who:
        ~ DoctorsInSurgery -= who
        {who} 医生去吃午饭了。
    }

=== waiting_room
    {whos_in_today()}
    *    [时间流逝...]
        {doctorLeaves(Adams)} {doctorEnters(Cartwright)} {doctorEnters(Eamonn)}
        {whos_in_today()}

会输出如下的结果:

今天在诊所的人有Adams和Denver。

> Time passes...

Adams医生去吃午饭了。 Cartwright 匆忙地走进来。Eamonn医生匆忙地走进来。

今天在诊所的人有Cartwright, Denver和Eamonn。

进阶:更好的列表输出

在游戏中使用基本的列表输出并不是特别有吸引力。以下是更好的:

=== function listWithCommas(list, if_empty)
    {LIST_COUNT(list):
    - 2:
            {LIST_MIN(list)}和{listWithCommas(list - LIST_MIN(list), if_empty)}
    - 1:
            {list}
    - 0:
            {if_empty}
    - else:
              {LIST_MIN(list)}, {listWithCommas(list - LIST_MIN(list), if_empty)}
    }

LIST favouriteDinosaurs = (stegosaurs), brachiosaur, (anklyosaurus), (pleiosaur)

My favourite dinosaurs are {listWithCommas(favouriteDinosaurs, "all extinct")}.

手边有一个is/are函数可能也很有用(这里就是英文用法,不翻了):

=== function isAre(list)
    {LIST_COUNT(list) == 1:is|are}

My favourite dinosaurs {isAre(favouriteDinosaurs)} {listWithCommas(favouriteDinosaurs, "all extinct")}.

要是搞得学究一点:

My favourite dinosaur{LIST_COUNT(favouriteDinosaurs) != 1:s} {isAre(favouriteDinosaurs)} {listWithCommas(favouriteDinosaurs, "all extinct")}.

列表不需要有多个条目

列表不是必须包含多个值。如果你想使用一个列表作为状态机,上面的例子就能正常工作。使用=++--设置值,使用==<<=>>=检验它们。这些都将按预期工作。

“完整的”列表

注意,LIST_COUNTLIST_MINLIST_MAX指的是谁在列表内/外,而不是可能的医生的全部集合。我们可以使用下面的方法:

LIST_ALL(element of list)

或者

LIST_ALL(list containing elements of a list)
{LIST_ALL(DoctorsInSurgery)} // Adams, Bernard, Cartwright, Denver和Eamonn
{LIST_COUNT(LIST_ALL(DoctorsInSurgery))} // "5"
{LIST_MIN(LIST_ALL(Eamonn))}                 // "Adams"

注意,使用{...}生成列表的基本表示,值作为单词,用逗号分隔。

Note that printing a list using {...} produces a bare-bones representation of the list; the values as words, delimited by commas.

进阶:“刷新”列表类型

如果你真的需要,你可以创建一个空列表,通过赋值让它知道它是什么类型的列表。

LIST ValueList = first_value, second_value, third_value
VAR myList = ()

~ myList = ValueList()

然后你就可以:

{ LIST_ALL(myList) }

进阶:“完整”列表的一部分

可以使用LIST_RANGE函数检索完整列表的“切片”。有两个用法,都是有

LIST_RANGE(list_name, min_integer_value, max_integer_value)

还有

LIST_RANGE(list_name, min_value, max_value)

这里的最小值和最大值是包含在内的。如果游戏无法找到这些值,它就会尽可能接近,但绝不会超出这个范围。例如:

{LIST_RANGE(LIST_ALL(primeNumbers), 10, 20)} 

会产生:

11, 13, 17, 19

示例:河内塔(Tower of Hanoi)

为了演示其中的一些想法,这里有一个可以运作的河内塔的例子,这样就不需要其他人来写了。(这里列表内的值不能直接用中文,只能之后写个函数来实现下替换了)

LIST Discs = one, two, three, four, five, six, seven
VAR post1 = ()
VAR post2 = ()
VAR post3 = ()

~ post1 = LIST_ALL(Discs)

-> gameloop

=== function can_move(from_list, to_list) ===
    {
    -   LIST_COUNT(from_list) == 0:
        // 没有碟片可以移动
        ~ return false
    -   LIST_COUNT(to_list) > 0 && LIST_MIN(from_list) > LIST_MIN(to_list):
        //移动的圆盘比新塔上最小的圆盘要大
        ~ return false
    -   else:
         // 没有问题!
        ~ return true

    }

=== function move_ring( ref from, ref to ) ===
    ~ temp whichRingToMove = LIST_MIN(from)
    ~ from -= whichRingToMove
    ~ to += whichRingToMove

== function getListForTower(towerNum)
    { towerNum:
        - 1:    ~ return post1
        - 2:    ~ return post2
        - 3:    ~ return post3
    }

=== function name(postNum)
     {postToPlace(postNum)}寺庙

=== function Name(postNum)
     {postToPlace(postNum)}寺庙

=== function postToPlace(postNum)
    { postNum:
        - 1: 第一个
        - 2: 第二个
        - 3: 第三个
    }

=== function describe_pillar(listNum) ==
    ~ temp list = getListForTower(listNum)
    {
    - LIST_COUNT(list) == 0:
        {Name(listNum)} 是空的。
    - LIST_COUNT(list) == 1:
         {list}号石环在 {name(listNum)}.
    - else:
        在 {name(listNum)}, 是{list}号石环。
    }


=== gameloop
    从天堂往下看,你看到你的追随者正在完成最后一座伟大寺庙的建设,准备开始工作。
- (top)
    +  [ Regard the temples]
        你依次看向每座寺庙。每一个上面都堆着石环。 {describe_pillar(1)} {describe_pillar(2)} {describe_pillar(3)}
    <- move_post(1, 2, post1, post2)
    <- move_post(2, 1, post2, post1)
    <- move_post(1, 3, post1, post3)
    <- move_post(3, 1, post3, post1)
    <- move_post(3, 2, post3, post2)
    <- move_post(2, 3, post2, post3)
    -> DONE

= move_post(from_post_num, to_post_num, ref from_post_list, ref to_post_list)
    +   { can_move(from_post_list, to_post_list) }
        [ 把一个石环从{name(from_post_num)}放到{name(to_post_num)} ]
        { move_ring(from_post_list, to_post_list) }
        { stopping:
        -   远在地下的祭司们建造了一个巨大的挽具,经过多年的工作,这个巨大的石环被举起到空中,然后飘到旁边的寺庙。绳子被划破,转眼间它又掉了一次。
        -   你下一个命令必以大筵席和许多祭物来迎接。葬礼硝烟散尽后,转移巨石圈的工作正式开始。一代一代的成长和衰落,戒指落到了它指定的位置。
        -   {cycle:
            - 几年过去了,石环慢慢移动。
            - 下面的牧师为了穿什么颜色的长袍而战斗,但当他们倒下和死亡时,工作仍然完成了。
            }
        }
    -> top

5) 进阶列表操作

上面的部分涵盖了基本的比较。还有一些更强大的特性,但是——任何熟悉数学集的人都会知道——事情开始变得有点棘手。所以这一节附带了一个“进阶”警告。

本文中的许多功能对于大多数游戏来说都不是必需的。

比较列表

我们可以使用><>=<=来比较小于精确值的列表。警告!我们使用的定义并不完全是标准结果。它们是基于比较所测试列表中元素的数值。

“明显大于”

LIST_A > LIST_B表示“A中最小的值大于B中最大的值”:换句话说,如果放在数轴上,A的全部在B的全部的右边。反过来<也一样。

“绝对不小于”

LIST_A >= LIST_B表示——“a中的最小值至少是B中的最小值,a中的最大值至少是B中的最大值”。也就是说,如果画在数轴上,a的整体要么在B上面,要么与B重叠,但B不高于a。

请注意,LIST_A > LIST_B暗示LIST_A != LIST_B,而LIST_A >= LIST_B允许LIST_A == LIST_B,但排除LIST_A < LIST_B,正如您可能希望的那样。

Health warning!

LIST_A >= LIST_BLIST_A > LIST_BLIST_A == LIST_B不相同

总之,除非你想得很明白要干什么,否则不要使用这些方法。

反转列表

一个列表可以是“反转的”,这就相当于通过调节入/出名称板,并将每个开关翻转到之前的相反位置。

LIST GuardsOnDuty = (Smith), (Jones), Carter, Braithwaite

=== function changingOfTheGuard
    ~ GuardsOnDuty = LIST_INVERT(GuardsOnDuty)

注意,空列表上的LIST_INVERT将返回空值,如果游戏没有足够的上下文来知道反转是什么。如果你需要处理这种情况,最安全的方法是手工操作:

=== function changingOfTheGuard
    {!GuardsOnDuty: // "GuardsOnDuty现在是空的吗?"
        ~ GuardsOnDuty = LIST_ALL(Smith)
    - else:
        ~ GuardsOnDuty = LIST_INVERT(GuardsOnDuty)
    }

脚注

反转的语法最初是~ list,但我们改变了它,因为不改的话行

~ list = ~ list

不仅能够工作,而且实际上导致了列表本身的反转,这看起来非常反常。

交叉列表

has或者?运算符更正式一些,是“你是我的子集吗”的运算符⊇,它包括相等的集合,但如果较大的集合不完全包含较小的集合,则不包括。

为了测试列表之间的“一些重叠”,我们使用重叠操作符^来获得交集。

LIST CoreValues = strength, courage, compassion, greed, nepotism, self_belief, delusions_of_godhood
VAR desiredValues = (strength, courage, compassion, self_belief )
VAR actualValues =  ( greed, nepotism, self_belief, delusions_of_godhood )

{desiredValues ^ actualValues} // 输出"self_belief"

结果是一个新的列表,所以你可以检验它:

{desiredValues ^ actualValues: 这位新总统至少有一个可取的品质。}

{LIST_COUNT(desiredValues ^ actualValues) == 1: 更正一下,这位新总统只有一个可取的品质。{desiredValues ^ actualValues == self_belief: 是个有点可怕的品质。}}

6) 多列表的列表

到目前为止,我们所有的例子都包含了一个很大的简化,同样——列表变量中的值必须都来自同一个列表族。但事实并非如此。

这允许我们使用列表——到目前为止,列表扮演着状态机和标记跟踪器的角色——也可以充当通用属性,这对世界建模很有用。

这是我们开始的时刻。结果是强大的,但也比之前的任何东西更像“真正的代码”。

跟踪对象的列表

例如,我们可以定义:

LIST Characters = Alfred, Batman, Robin
LIST Props = champagne_glass, newspaper

VAR BallroomContents = (Alfred, Batman, newspaper)
VAR HallwayContents = (Robin, champagne_glass)

然后,我们可以通过测试任何房间的状态来描述它的内容:

=== function describe_room(roomState)
    { roomState ? Alfred: Alfred在这里,安静地站在角落中。 } { roomState ? Batman: Batman的存在压倒了一切。 } { roomState ? Robin: Robin几乎被遗忘了。}
    <> { roomState ? champagne_glass: 一个香槟杯被丢弃在地板上。} { roomState ? newspaper: 一张桌子上,报纸醒目的标题写着:“谁是蝙蝠侠?谁是他几乎不被记得的助手?“ }

之后下面的脚本:

{ describe_room(BallroomContents) }

就会产生:

Alfred在这里,安静地站在角落中。Batman的存在压倒了一切。

一张桌子上,报纸醒目的标题写着:“谁是蝙蝠侠?谁是他几乎不被记得的助手?“

而下面的脚本:

{ describe_room(HallwayContents) }

产生:

Robin几乎被遗忘了。

一个香槟杯被丢弃在地板上。

我们可以根据这些东西的组合来提供选项:

*    { currentRoomState ? (Batman, Alfred) } [对Alfred和Batman说话]
”所以,你们俩互相认识吗?“

用于跟踪多个状态的列表

我们可以模拟具有多种状态的设备。再回到水壶上…

LIST OnOff = on, off
LIST HotCold = cold, warm, hot

VAR kettleState = off, cold

=== function turnOnKettle() ===
{ kettleState ? hot:
    你打开水壶,但它马上又自动关掉了。
- else:
    水壶里的水开始热起来。
    ~ kettleState -= off
    ~ kettleState += on
    // 注意,我们避免使用“=”,因为它将删除所有现有状态
}

=== function can_make_tea() ===
    ~ return kettleState ? (hot, off)

正如上面的off/on所演示的那样,这些混合状态可能会使状态的更改变得有点棘手,因此下面的助手函数可能很有用。

 === function changeStateTo(ref stateVariable, stateToReach)
     // 删除该类型的所有状态
     ~ stateVariable -= LIST_ALL(stateToReach)
     // 恢复我们想要的状态
     ~ stateVariable += stateToReach

这样就可以执行如下代码:

 ~ changeState(kettleState, on)
 ~ changeState(kettleState, warm)

这对查询有什么影响?

上面给出的查询大多可以很好地推广到多值列表:

LIST Letters = a,b,c
LIST Numbers = one, two, three

VAR mixedList = (a, three, c)

{LIST_ALL(mixedList)}   // a, one, b, two, c, three
{LIST_COUNT(mixedList)} // 3
{LIST_MIN(mixedList)}   // a
{LIST_MAX(mixedList)}   // three or c, albeit unpredictably

{mixedList ? (a,b) }        // false
{mixedList ^ LIST_ALL(a)}   // a, c

{ mixedList >= (one, a) }   // true
{ mixedList < (three) }     // false

{ LIST_INVERT(mixedList) }            // one, b, two

7) 长例子:犯罪现场

最后,这里有一个很长的示例,演示了本节中的许多想法。你可能想在通读之前试着玩一下,以便更好地理解各种活动的部分。

-> murder_scene

// 辅助函数:从列表中弹出元素
=== function pop(ref list)
   ~ temp x = LIST_MIN(list) 
   ~ list -= x 
   ~ return x

//
//  系统:物品不同的状态
//  有些是一般性的,有些是针对特定项目的
//


LIST OffOn = off, on
LIST SeenUnseen = unseen, seen

LIST GlassState = (none), steamed, steam_gone
LIST BedState = (made_up), covers_shifted, covers_off, bloodstain_visible

//
// 系统:物品栏
//

LIST Inventory = (none), cane, knife

=== function get(x)
    ~ Inventory += x

//
// 系统:定位物品
// 物品可以放进或放上某地
//

LIST Supporters = on_desk, on_floor, on_bed, under_bed, held, with_joe

=== function move_to_supporter(ref item_state, new_supporter) ===
    ~ item_state -= LIST_ALL(Supporters)
    ~ item_state += new_supporter


// 系统:增量知识。
// 每个列表都是一连串的事实。每个事实都取代前面的事实
//

VAR knowledgeState = ()

=== function reached (x) 
   ~ return knowledgeState ? x 

=== function between(x, y) 
   ~ return knowledgeState? x && not (knowledgeState ^ y)

=== function reach(statesToSet) 
   ~ temp x = pop(statesToSet)
   {
   - not x: 
      ~ return false 

   - not reached(x):
      ~ temp chain = LIST_ALL(x)
      ~ temp statesGained = LIST_RANGE(chain, LIST_MIN(chain), x)
      ~ knowledgeState += statesGained
      ~ reach (statesToSet)     // set any other states left to set
      ~ return true             // and we set this state, so true

    - else:
      ~ return false || reach(statesToSet) 
    }    

//
// 设置游戏
//

VAR bedroomLightState = (off, on_desk)

VAR knifeState = (under_bed)


//
// Knowledge chains
//


LIST BedKnowledge = neatly_made, crumpled_duvet, hastily_remade, body_on_bed, murdered_in_bed, murdered_while_asleep

LIST KnifeKnowledge = prints_on_knife, joe_seen_prints_on_knife,joe_wants_better_prints, joe_got_better_prints

LIST WindowKnowledge = steam_on_glass, fingerprints_on_glass, fingerprints_on_glass_match_knife


//
// Content
//

=== murder_scene ===
    The bedroom. This is where it happened. Now to look for clues.
- (top)
    { bedroomLightState ? seen:     <- seen_light  }
    <- compare_prints(-> top)

*   (dobed) [The bed...]
    The bed was low to the ground, but not so low something might not roll underneath. It was still neatly made.
    ~ reach (neatly_made)
    - - (bedhub)
    * *     [Lift the bedcover]
            I lifted back the bedcover. The duvet underneath was crumpled.
            ~ reach (crumpled_duvet)
            ~ BedState = covers_shifted
    * *     (uncover) {reached(crumpled_duvet)}
            [Remove the cover]
            Careful not to disturb anything beneath, I removed the cover entirely. The duvet below was rumpled.
            Not the work of the maid, who was conscientious to a point. Clearly this had been thrown on in a hurry.
            ~ reach (hastily_remade)
            ~ BedState = covers_off
    * *     (duvet) {BedState == covers_off} [ Pull back the duvet ]
            I pulled back the duvet. Beneath it was a sheet, sticky with blood.
            ~ BedState = bloodstain_visible
            ~ reach (body_on_bed)
            Either the body had been moved here before being dragged to the floor - or this is was where the murder had taken place.
    * *     {BedState !? made_up} [ Remake the bed ]
            Carefully, I pulled the bedsheets back into place, trying to make it seem undisturbed.
            ~ BedState = made_up
    * *     [Test the bed]
            I pushed the bed with spread fingers. It creaked a little, but not so much as to be obnoxious.
    * *     (darkunder) [Look under the bed]
            Lying down, I peered under the bed, but could make nothing out.

    * *     {TURNS_SINCE(-> dobed) > 1} [Something else?]
            I took a step back from the bed and looked around.
            -> top
    - -     -> bedhub

*   {darkunder && bedroomLightState ? on_floor && bedroomLightState ? on}
    [ Look under the bed ]
    I peered under the bed. Something glinted back at me.
    - - (reaching)
    * *     [ Reach for it ]
            I fished with one arm under the bed, but whatever it was, it had been kicked far enough back that I couldn't get my fingers on it.
            -> reaching
    * *     {Inventory ? cane} [Knock it with the cane]
            -> knock_with_cane

    * *     {reaching > 1 } [ Stand up ]
            I stood up once more, and brushed my coat down.
            -> top

*   (knock_with_cane) {reaching && TURNS_SINCE(-> reaching) >= 4 &&  Inventory ? cane } [Use the cane to reach under the bed ]
    Positioning the cane above the carpet, I gave the glinting thing a sharp tap. It slid out from the under the foot of the bed.
    ~ move_to_supporter( knifeState, on_floor )
    * *     (standup) [Stand up]
            Satisfied, I stood up, and saw I had knocked free a bloodied knife.
            -> top

    * *     [Look under the bed once more]
            Moving the cane aside, I looked under the bed once more, but there was nothing more there.
            -> standup

*   {knifeState ? on_floor} [Pick up the knife]
    Careful not to touch the handle, I lifted the blade from the carpet.
    ~ get(knife)

*   {Inventory ? knife} [Look at the knife]
    The blood was dry enough. Dry enough to show up partial prints on the hilt!
    ~ reach (prints_on_knife)

*   [   The desk... ]
    I turned my attention to the desk. A lamp sat in one corner, a neat, empty in-tray in the other. There was nothing else out.
    Leaning against the desk was a wooden cane.
    ~ bedroomLightState += seen

    - - (deskstate)
    * *     (pickup_cane) {Inventory !? cane}  [Pick up the cane ]
            ~ get(cane)
          I picked up the wooden cane. It was heavy, and unmarked.

    * *    { bedroomLightState !? on } [Turn on the lamp]
            -> operate_lamp ->

    * *     [Look at the in-tray ]
            I regarded the in-tray, but there was nothing to be seen. Either the victim's papers were taken, or his line of work had seriously dried up. Or the in-tray was all for show.

    + +     (open)  {open < 3} [Open a drawer]
            I tried {a drawer at random|another drawer|a third drawer}. {Locked|Also locked|Unsurprisingly, locked as well}.

    * *     {deskstate >= 2} [Something else?]
            I took a step away from the desk once more.
            -> top

    - -     -> deskstate

*     {(Inventory ? cane) && TURNS_SINCE(-> deskstate) <= 2} [Swoosh the cane]
    I was still holding the cane: I gave it an experimental swoosh. It was heavy indeed, though not heavy enough to be used as a bludgeon.
    But it might have been useful in self-defence. Why hadn't the victim reached for it? Knocked it over?

*   [The window...]
    I went over to the window and peered out. A dismal view of the little brook that ran down beside the house.

    - - (window_opts)
    <- compare_prints(-> window_opts)
    * *     (downy) [Look down at the brook]
            { GlassState ? steamed:
                Through the steamed glass I couldn't see the brook. -> see_prints_on_glass -> window_opts
            }
            I watched the little stream rush past for a while. The house probably had damp but otherwise, it told me nothing.
    * *     (greasy) [Look at the glass]
            { GlassState ? steamed: -> downy }
            The glass in the window was greasy. No one had cleaned it in a while, inside or out.
    * *     { GlassState ? steamed && not see_prints_on_glass && downy && greasy }
            [ Look at the steam ]
            A cold day outside. Natural my breath should steam. -> see_prints_on_glass ->
    + +     {GlassState ? steam_gone} [ Breathe on the glass ]
            I breathed gently on the glass once more. { reached (fingerprints_on_glass): The fingerprints reappeared. }
            ~ GlassState = steamed

    + +     [Something else?]
            { window_opts < 2 || reached (fingerprints_on_glass) || GlassState ? steamed:
                I looked away from the dreary glass.
                {GlassState ? steamed:
                    ~ GlassState = steam_gone
                    <> The steam from my breath faded.
                }
                -> top
            }
            I leant back from the glass. My breath had steamed up the pane a little.
           ~ GlassState = steamed

    - -     -> window_opts

*   {top >= 5} [Leave the room]
    I'd seen enough. I {bedroomLightState ? on:switched off the lamp, then} turned and left the room.
    -> joe_in_hall

-   -> top


= operate_lamp
    I flicked the light switch.
    { bedroomLightState ? on:
        <> The bulb fell dark.
        ~ bedroomLightState += off
        ~ bedroomLightState -= on
    - else:
        { bedroomLightState ? on_floor: <> A little light spilled under the bed.} { bedroomLightState ? on_desk : <> The light gleamed on the polished tabletop. }
        ~ bedroomLightState -= off
        ~ bedroomLightState += on
    }
    ->->


= compare_prints (-> backto)
    *   { between ((fingerprints_on_glass, prints_on_knife),     fingerprints_on_glass_match_knife) } 
[Compare the prints on the knife and the window ]
        Holding the bloodied knife near the window, I breathed to bring out the prints once more, and compared them as best I could.
        Hardly scientific, but they seemed very similar - very similiar indeed.
        ~ reach (fingerprints_on_glass_match_knife)
        -> backto

= see_prints_on_glass
    ~ reach (fingerprints_on_glass)
    {But I could see a few fingerprints, as though someone hadpressed their palm against it.|The fingerprints were quite clear and well-formed.} They faded as I watched.
    ~ GlassState = steam_gone
    ->->

= seen_light
    *   {bedroomLightState !? on} [ Turn on lamp ]
        -> operate_lamp ->

    *   { bedroomLightState !? on_bed  && BedState ? bloodstain_visible }
        [ Move the light to the bed ]
        ~ move_to_supporter(bedroomLightState, on_bed)

        I moved the light over to the bloodstain and peered closely at it. It had soaked deeply into the fibres of the cotton sheet.
        There was no doubt about it. This was where the blow had been struck.
        ~ reach (murdered_in_bed)

    *   { bedroomLightState !? on_desk } {TURNS_SINCE(-> floorit) >= 2 }
        [ Move the light back to the desk ]
        ~ move_to_supporter(bedroomLightState, on_desk)
        I moved the light back to the desk, setting it down where it had originally been.
    *   (floorit) { bedroomLightState !? on_floor && darkunder }
        [Move the light to the floor ]
        ~ move_to_supporter(bedroomLightState, on_floor)
        I picked the light up and set it down on the floor.
    -   -> top

=== joe_in_hall
    My police contact, Joe, was waiting in the hall. 'So?' he demanded. 'Did you find anything interesting?'
- (found)
    *   {found == 1} 'Nothing.'
        He shrugged. 'Shame.'
        -> done
    *   { Inventory ? knife } 'I found the murder weapon.'
        'Good going!' Joe replied with a grin. 'We thought the murderer had gotten rid of it. I'll bag that for you now.'
        ~ move_to_supporter(knifeState, with_joe)

    *   {reached(prints_on_knife)} { knifeState ? with_joe }
        'There are prints on the blade[.'],' I told him.
        He regarded them carefully.
        'Hrm. Not very complete. It'll be hard to get a match from these.'
        ~ reach (joe_seen_prints_on_knife)
    *   { reached((fingerprints_on_glass_match_knife, joe_seen_prints_on_knife)) }
        'They match a set of prints on the window, too.'
        'Anyone could have touched the window,' Joe replied thoughtfully. 'But if they're more complete, they should help us get a decent match!'
        ~ reach (joe_wants_better_prints)
    *   { between(body_on_bed, murdered_in_bed)}
        'The body was moved to the bed at some point[.'],' I told him. 'And then moved back to the floor.'
        'Why?'
        * *     'I don't know.'
                Joe nods. 'All right.'
        * *     'Perhaps to get something from the floor?'
                'You wouldn't move a whole body for that.'
        * *     'Perhaps he was killed in bed.'
                'It's just speculation at this point,' Joe remarks.
    *   { reached(murdered_in_bed) }
        'The victim was murdered in bed, and then the body was moved to the floor.'
        'Why?'
        * *     'I don't know.'
                Joe nods. 'All right, then.'
        * *     'Perhaps the murderer wanted to mislead us.'
                'How so?'
            * * *   'They wanted us to think the victim was awake[.'], I replied thoughtfully. 'That they were meeting their attacker, rather than being stabbed in their sleep.'
            * * *   'They wanted us to think there was some kind of struggle[.'],' I replied. 'That the victim wasn't simply stabbed in their sleep.'
            - - -   'But if they were killed in bed, that's most likely what happened. Stabbed, while sleeping.'
                    ~ reach (murdered_while_asleep)
        * *     'Perhaps the murderer hoped to clean up the scene.'
                'But they were disturbed? It's possible.'

    *   { found > 1} 'That's it.'
        'All right. It's a start,' Joe replied.
        -> done
    -   -> found
-   (done)
    {
    - between(joe_wants_better_prints, joe_got_better_prints):
        ~ reach (joe_got_better_prints)
        <> 'I'll get those prints from the window now.'
    - reached(joe_seen_prints_on_knife):
        <> 'I'll run those prints as best I can.'
    - else:
        <> 'Not much to go on.'
    }
    -> END

8) 总结

总结一下这个较难的部分,ink的列表结构提供:

标记(Flags)

  • 每个列表条目都是一个事件
  • 使用+=标记已发生的事件
  • 使用?! ?进行检验

示例:

LIST GameEvents = foundSword, openedCasket, metGorgon
{ GameEvents ? openedCasket }
{ GameEvents ? (foundSword, metGorgon) }
~ GameEvents += metGorgon

状态机(State machines)

  • 每个列表条目都是一个状态
  • 使用=来设置状态;++--向前或后退
  • 使用=等进行检验

示例:

LIST PancakeState = ingredients_gathered, batter_mix, pan_hot, pancakes_tossed, ready_to_eat
{ PancakeState == batter_mix }
{ PancakeState < ready_to_eat }
~ PancakeState++

属性

  • 每个列表都是一个不同的属性,属性的状态值(打开或关闭,点亮或未点亮,等等)
  • 通过删除旧状态,然后添加新状态来改变状态
  • 使用?!?进行检验

示例:

LIST OnOffState = on, off
LIST ChargeState = uncharged, charging, charged

VAR PhoneState = (off, uncharged)

*    {PhoneState !? uncharged } [给手机充电]
    ~ PhoneState -= LIST_ALL(ChargeState)
    ~ PhoneState += charging
    你给手机插上了充电线。
*    { PhoneState ? (on, charged) } [给妈妈打电话]

第6部分:标识符中的国际字符支持

默认情况下,墨水对在故事内容中使用非ascii字符没有限制。然而,目前在部分字符中存在限制,即可以用于常量、变量、缝合、转移和其他命名流元素的名称(也就是标识符,identifiers)的字符。

有时,对于使用非ASCII语言编写故事的作者来说,这是不方便的,因为他们必须不断地切换到ASCII的命名标识符,然后切换回他们在故事中使用的任何语言。此外,用作者自己的语言命名标识符可以提高原始故事格式的整体可读性。

为了帮助实现上述场景,ink自动支持一个预定义的非ascii字符范围列表,可以将其用作标识符。一般来说,这些范围已被选择为包括官方unicode字符范围的字母-数字子集,这足以用于命名标识符。下一节将提供有关墨水自动支持的非ascii字符的更详细信息。

支持的标识符

对Ink中附加字符范围的支持目前仅限于预定义的一组字符范围。

下面列出了当前支持的标识符范围。

  • ArabicEnables characters for languages of the Arabic family and is a subset of the official Arabicunicode range \u0600\u06FF.
  • ArmenianEnables characters for the Armenian language and is a subset of the official Armenian unicode range \u0530\u058F.
  • CyrillicEnables characters for languages using the Cyrillic alphabet and is a subset of the official Cyrillicunicode range \u0400\u04FF.
  • GreekEnables characters for languages using the Greek alphabet and is a subset of the official Greek and Coptic unicode range \u0370\u03FF.
  • HebrewEnables characters in Hebrew using the Hebrew alphabet and is a subset of the official Hebrewunicode range \u0590\u05FF.
  • Latin Extended AEnables an extended character range subset of the Latin alphabet – completely represented by the official Latin Extended-A unicode range \u0100\u017F.
  • Latin Extended BEnables an extended character range subset of the Latin alphabet – completely represented by the official Latin Extended-B unicode range \u0180\u024F.
  • Latin 1 SupplementEnables an extended character range subset of the Latin alphabet – completely represented by the official Latin 1 Supplement unicode range \u0080 – \u00FF.

注意!Ink文件应以UTF-8格式保存,以确保上述字符范围得到支持。

如果您想在标识符中使用的特定字符范围不受支持,请去ink的github上提交issue。

目录