inky语法备忘
INKY
ink 是一门围绕标记纯文本展开的脚本语言,以生成互动脚本。
最简单的,它能让你写出一个 Choose Your Own 风格的故事,或有分支的对话树。但它真正的优势在于大量选项的复杂对话,以及大量故事线的编组。
ink 提供了一些特性能让非技术人员也能随心分支,写好每一个分支的后续,同时搞定主线与支线,而不必手忙脚乱。故事线则尽可能地以声明式描述。(注,声明式是告诉机器“做什么 what to do”,与其相对的命令式则是告诉机器“怎么做 how to do”。)
ink 的目标是让一切清新有逻辑地排序,“用眼”就能检查每条对话分支。
它在设计时也考虑了想法在头脑中思考的过程,因此编写一个故事线是非常快的
(某天在网上冲浪的时候看到了一个很有趣的互动文字冒险,在关于界面找到了这款软件,发现意外的容易上手,于是找到相关教程并记录备忘。)
(光有工具也没用啊,贫瘠的艺术细胞限制了我,哭死。)
使用文档
举个例子
现在,让我们看门见山,来看一个简单的故事示例。
若你以前从未接触过编程(很多符号!),下方截屏中的蓝色标记最初可能让人望而生畏。但我们会逐步介绍它们,以便你了解它们的组合方式。
请将下方内容复制到 Inky 的左栏中,然后在右栏中预览。右上角的单箭头是后退,双箭头是重置预览的故事。
1 | 伦敦,1872年 |
预览时,调整左栏的内容,右栏预览也会实时更新,即使你正处于故事中。你甚至可以点击右栏中的某个单词,它会选中左栏中的源文本(当故事变得非常复杂时,这非常有用!)。你也可以先看这个故事,用默认模板发布到到网页的效果,点我运行体验。
学会注释*
好记性不如烂笔头。你可以在任何地方,用 //
,或 /* …… */
帮你插入注释,记住你想记住的东西,避免一觉醒来就忘记“这是什么东西”。注释内容不会在故事运行时显示给玩家,也不会影响周围的其他内容。
1 | “你怎么看?” 她问。 |
还有一种更强的注释 TODO:
,帮你提醒要做什么。
1 | TODO: 认真写这一节! |
在这个简单的故事中,你只需要简单的了解三个部分:结点、跳转和选项。
1. 结点 knots
故事是由多个相互链接的部分构成的,在 INK 中我们称其为结点(knots)。结点名称的左侧**至少**需要两个等号(==
)进行标记,右侧的则是可选的(例如 === london ===
)。Inky 中会用蓝色突出显示已识别的标记,提示书写正确,并与其他内容区分。结点标记下面的内容都属于这个结点。
1 | === london === //这是节点 |
结点的名称(不能有空格、中文)不会在运行故事时显示。
而当我们想要把不同部分链接的在一起时,会用到它们。
2. 跳转 divert
现在谈谈链接,我们称之为跳转(divert)。-> london
使用跳转箭头(输入”减”与”右尖括号”),告诉故事去另一个结点。运行故事时,跳转是自动的且不会显示给玩家。
因此,在故事的开头,显示了介绍性段落之后,就有 -> london
的跳转,可将玩家直接跳入该名称的结点中。
1 | 伦敦,1872年 |
特殊:END
这个故事的结尾,还有一种特殊的跳转。如果你跳转到“ END
”,会告诉 INK 我们完成了,这里是结局。
1 | === ending //这也是节点 开始 |
试着删除例子中该有的 -> END
,Inky 将在边缘显示错误。若将鼠标悬停在警告上,会提示你“loose end”。
这是 Inky 的一个重要的帮助功能,当你故事越来越复杂时,这些警告可提醒你仍要编写哪些部分。
胶水* glue
如必须让内容在同一行显示,还可以使用胶水(glue) <>
将其强制黏在同一行。
1 | 我们回家<> |
你会在预览看到 我们回家去萨维尔街尽可能的快
,而非分三行显示。
3. 选项 choices
谈到分歧,为了使互动小说更有趣,我们需要的最后一个关键功能当然是选项(choices):
1 | + “环游世界,先生?” |
选项看起来是 INK 中最重要的元素。通常它们会与跳转直接结合在一起,以便能创造一个直接跳转到故事其他地方的选项。
1 | + [点头。] -> nod |
我们编写这些内容时,需要注意几件事:
通常,你输入的选项内容既会成为可点击的选项,也会成为你点击后出现的文本的一部分。但是,如果你想要一个“无声的”(如动作)选项,则可以将选项放在[
方括号中]
,如上方第二个示例所示。
这可以用于某些形式,像是不同的叙述风格。例如:
1 | + [草率的点头] -> nod |
第一个选项只会显示,“这有些不可思议。”;而第二个会显示“我草率的点了点头,这有些不可思议。”
你还可以在选项下添加额外的文本行。这将在选择后显示。例如:
1 | + “环游世界,先生?” |
等同于——
1 | + “环游世界,先生?” -> astonished |
INK 是非常灵活的,它可以让你有很多不同的方式来构建你的故事。
事实上*,]
之前的部分会被显示为选项,[]
内的内容会在选择后隐藏。你也可以把 []
当作选项与选择之后内容的分隔符,便于快速输入,这些内容会显示在同一行。
1 | + 我草率的点了点头,[]这有些不可思议。 |
小技巧,你也可以试试 “就是死我也不会吃的[!”]……真香!”
,体验下选择前后的语气变化,这种细节在某些叙事风格种很有用。
一次性选项*
如想要选项只能被被玩家点一次,最简单的方式就是把选项的 +
改成 *
。
1 | * “环游世界,先生?” |
举个例子,你会发现下图中的一次性选项“2 Hello back!”不见了。
1 | ->hello |
结构说明
关于 INK 整体结构最后的说明:INK 始终从文件的顶部开始,然后一直向下运行。但如果你将所有内容拆分为结点,则务必确保在文件顶部至少有一个初始跳转(如-> top_knot
),以告诉它以哪个结点开始(修改初始跳转,可更方便的进行测试)。
1 | -> top_knot //初始跳转 |
现在,为什么不尝试续写个故事呢?尝试插入更多的结点,更多的跳转,更多的选项,更多的结局,看看会发生什么!作为练习的开始,也许先试着把介绍性段落变成你的结点?
4. 条件内容
到目前为止,我们编写的所有内容,都能写成可发布的互动小说。但如果我们想根据玩家之前的经历不同,显示不同的文本和选项呢?
INK 会记录运行中玩家经历的每个内容,以便你今后查询。例如,要确定玩家是否经过了称为(地下墓穴)的特定结点 catacombs
:
1 | { catacombs: //条件:经过地下墓穴 |
在这个{
大括号中}
,你可以包含多行内容,甚至是跳转和选项(只有玩家经过了“catacombs”这个结点,大括号里的内容才会被显示)。
但是,一个更简洁的编写条件选项的方法如下,仅当玩家经过某个结点后才显示特定选项的方法:
1 | + {catacombs} [告诉她你的发现] -> tell_her |
如果你想反转这个条件,换句话说,如果你想确认玩家没有经过地下墓穴,你只需在前面简单地加上:not
1 | + {not catacombs} [前往地下墓穴] -> catacombs |
最后,你可用 and
和 or
形成更复杂的判定条件,并且用小括号来阐明逻辑的细节。几个例子:
1 | { catacombs and not pick_up_ring: //条件:经过地下墓穴且没经过捡起戒指 |
1 | { (catacombs or cross_river or sing_in_rain) and not buy_new_shoes: |
再看个例子*,你能发现选“白色的”虽然和灰色的后面走的是一样的结点,但显示的内容变多了。
1 | 你想买什么颜色的狼? |
高阶 INK
我们只介绍了 INK 中强大功能的一小部分。如你想阅读更多或只是深入了解已经看到的东西,看看官方文件:
- 与任何编程语言一样,你可以创建自定义变量并执行数学计算。
- 我们一直在使用
+
作为选项符号,但通常我们建议使用*
。区别在于,*
选项只能选择一次,非常适合需要重复浏览,但你不希望重复选择的选项。 - 一个有点难学但易于编写的系统,称为“编织(weave)”,用于编写复杂分支,不需要你自己命名每个部分。我们的大多数游戏像80天,都是用这种系统编写的。
- 在结点中,你还可以包含称为“针脚(stitches)”的子结点。
- 写不同的内容和分支的中间内容是可能的,也是直截了当的(?)。
- 你可将 INK 文件拆分为多个连在一起的较小文件。
- ……
到这里为止,你已经掌握了 Inky 书写故事最最基础的语法。现在试着考验下自己:
写一段游戏中常见的“询问NPC”剧情。基础要求至少有三个可以问NPC的选项,且可以问完所有问题。
进一步要求,玩家至少问一个问题后才能继续剧情,玩家问完所有问题后会直接继续剧情……
是不是觉得有些难(或麻烦)呢?这也是为什么会有高阶 INK 的原因。强烈建议你读一下“编织”,这个问题的答案就在编织那块的底部。你会发现,INK 应对这种内容其实游刃有余,或者说 INK 就是为写这种故事而生的,简单而流畅。事实也是如此,大部分 INK 的故事都是用编织写主要部分的,而非最基础的语法。
为此,我翻译了官方文档的部分内容,便于你选择性了解,点我阅读。
导出作品
下一步是将我们的故事保存到一组文件中,这些文件可以作为网页上传到网上。要在 Inky 中执行此操作,请选择“File”,然后选择“Export for web…”,然后选下故事的名称。你选择的名称将同时用于网页上的标题(因此,请适当的用空格和大写字母),以及保存到的文件夹的名称。
当你对故事进行更改后,尤其是你对外观更改后(参见下文),你应该选择“Export story.js only…”,将其保存到相同的位置并覆盖“ 故事名字.js”。这是更新 INK 内容后唯一需要更新的文件,其余的文件都只是 Web 模板的一部分。因此,若你更改了模板的外观,这样就不会意外覆盖。
至此,你已经完成了!如果愿意,你可以直接跳到“发布到网页”的步骤。
但是,如果你还想添加图像,修改外观,请继续阅读!
页面美化
INK 被设计成尽可能的专注于一项任务:编写交互式文本。为了在功能性、灵活性和简洁性保持良好的平衡,我们抵制了可能使语言超载的额外功能。
因此,INK 语言中没有用于插入图像之类的任何内置工具,这完全取决于你创造的故事类型。如你正在为3D游戏编写对话,那么指定要显示的图像则不一定有意义。
但是,Inky 确实提供了一些额外的功能,这些功能可在导出 Web 后可用(自0.10.0版本开始)。这些额外功能都用的是 INK 的标签系统,它允许你为每个行提供特殊的文本注释,在行的开头或上方。标签是玩家不可见的,但可以由系统或 Web 模板读取。
1. 插入图片
插入图片的方法如下(图片会在导出web后显示):
1 | # IMAGE: imageName.jpg |
图像文件应与其他文件位于同一文件夹中,或者如果需要,可以使用相对路径:
1 | # IMAGE: myImages/imageName.jpg |
你也可以在行尾添加图片标签。该图像将始终显示在游戏文本上方。
1 | The above picture is a dog. # IMAGE: wolf.jpg |
(因为所有标记都与特定的文本行相关联,且当单独在某行放个标记时,它将与其下方的文本行关联。)
2. 清空屏幕
Inky 的 Web 模板另一个特有功能是 clear 标签:# CLEAR
这将清空当前显示的所有文本,从页面顶部重新开始。建议仅在选项后直接插入!如果插入到文本中间,则部分内容将在看到之前就被清除。
3. 深色主题与作者
要用深色主题,只需将其放在 INK 文件的最顶部:# theme: dark
Web模板能够读取它作为一个全局标签,并内置了样式来改变主题(它使用”dark”作为整个页面的类。当你在 CSS 文件中看到 .dark
选择器的使用时,这些是深色主题的重写。)
要设置作者姓名,请将其放在 INK 文件的顶部:# author: 你的名字
这将用到 Inky 的 Web 模板种,并位于主标题的正下方。
4. 自定义CSS类
你还可以使用CSS类完全自定义各行文本的外观。在 INK 中,如下添加标签到文本行里:
1 | 你进入了森林。 |
然后,可以通过向你的 style.css
文件中添加以下内容,来告诉浏览器“danger”CSS类的外观:
1 | .danger { |
圆点后跟一个单词,是我们在CSS中定义样式类的方式。我们之前的大多数示例都没有点,因为这些元素内置,如”p”表示”段落”。这些也可以组合使用,因此 p.danger
意味着“危险的段落”,并且在我们的示例中同样适用。
示例:END
如要以居中粗体显示结尾的“全剧终”,可用 Inky 模板包含的 end
的内置样式:
1 | # CLASS: end |
回顾下:
- 中间一行是文本本身。
- 第一行将 CSS 类”end”应用于文本。
- 最后一行告诉 INK ,我们完成了,这不是一个“loose end”。
要调整 end CSS 类的样式,请在 CSS 文件底部查找 .end
。
5. 重启故事
Inky 的 Web 模板还具有一个内置功能,如果插入 # RESTART
标签,它将立即重置故事的进度(包括玩家选择的所有经历和变量),并从头开始。由于它会立刻生效,建议你放在一个选项后,例如:
1 | 你死啦! |
-> END
跳转实际上从未到达,但它是个好办法:防止 INK 抱怨你“loose end”。
6. 定制外观
本指南面向初学者 ,假定他对 Web 技术一无所知。
文件结构
上面的导出步骤应已保存出包含以下文件的文件夹(分享故事请分享整个文件夹):
- index.html - HTML 文件将其他文件绑定在一起,如你想运行你的故事,双击该文件。它告诉浏览器页面的结构,并告诉它加载其他文件。
- main.js - 这是主要的 JavaScript 文件,你可在其中自定义故事的呈现方式。
- style.css - 这是主要的 CSS 文件,它定义诸如颜色、字体以及大小调整和间距信息(如段落的宽度和边距)之类的内容。我们还可以定义过渡,例如段落淡入的方式。
- 故事名字.js - 这是你 INK 的实际内容,由 Inky 导出。这是你故事的唯一文件,而非 Inky 保存的通用模板的一部分。请勿直接编辑此文件。
- ink.js - 这是 INK 引擎本身的 JavaScript 端口。
要修改任何文件,应在纯文本编辑器中打开,如 VS Code 而非 Word!
更改字体和颜色
字体和颜色均由CSS文件 style.css 控制。如果打开文件,你将看到一系列类似下面的块:
1 | body { |
每个名称都以“ body”或“ h1”之类的名称开头。在CSS中,这称为选择器,因为它选择网页的特定部分进行样式设置。在上述情况下,它选择正文,实际上是整个页面。
字体列表 font-family
按优先顺序排列,这取决于用户计算机上的可用内容。在上面的示例中,Open Sans 是一种网络字体,是在文件顶部导入的,只需访问 fonts.google.com 进行选择即可选择,并说明如何使用它们(它为你提供了几个@import
,你可以自己粘贴)。如由于某些原因无法下载该字体,它将退回到 sans-serif 选项。
你还可以使用用户系统中自带的字体,但是问题是,一定都有的字体 非常有限。因此通常字体最后都会写上sans-serif
。
强烈建议*前往 W3C School,花些时间简单的自学下CSS教程,再看下方阴影部分更容易上手。
文件中的一些CSS选择器和规则包括:
h1
-代表“一级标题”-即最大的标题,用于页面顶部故事的主要标题。如果要更改标题的大小,可在font-size
更改属性。同样,这h1, h2
是一次可同时在两个选择器上设置规则的快捷方式,即一级二级标题。如果# author: 你的名字
在 INK 顶部有标签,则后者用于副标题。.header
-以点开头的名称是类名称。这个特定的示例选择一个元素,该元素只是title和 byline 的容器。通常,在 HTML/CSS 中,元素被赋予容器来帮助构建或对间距和布局进行精细控制。p
-这代表“段落”。它是页面上主体文本的选择器。因此,这是你要更改字体大小,文本颜色等的地方。a
-这代表锚标记,锚标记是可点击链接的HTML术语-这些标记仅在我们的页面上用于可点击的选择,但也可以是任何东西。p.choice
-“具有’choice’类的段落”。如果只是.choice
,那就意味着“任何选项类”。我们页面上的选择实际上是按特殊样式设计的段落,其中包含可单击的链接(锚标记)。a:hover
-这里:hover
称为伪类,它用于当用户的鼠标指针悬停该元素上时选择一个元素。
CSS根据优先级应用样式,优先级基于选择器的具体程度(更具体的是更高的优先级)和文件中的位置(更低的是较高优先级)。这使你可以首先使用粗笔画定义样式,然后逐步覆盖它们。例如,我们h2
先定义,然后下移.dark h2
覆盖h2样式的一部分:当深色主题处于活动状态(.dark
类分配给整个页面-)时,我们更改颜色。
有关可以单独使用CSS更改样式的所有其他方式的更多信息,请尝试使用搜索“ CSS教程”,那里有大量的资源!
例子:花哨标题
在 Google字体 上选择字体,然后插入 CSS 文件顶部的行,例如:@import
1 | @import url('https://fonts.googleapis.com/css?family=Lobster'); |
然后,将规则更改为类似的:
1 | h1 { |
发布到网页*
原文此部分介绍的是发布到 itch.io
,但好消息是,你几乎可以把 Inky 导出的 Web 托管到任何可以放静态网页的地方,如 netlify,github page,coding…… 和你的服务器上。
Netlify*
- 注册 netlify ;
- 将你的故事文件夹拖动到“Want to deploy a new site without connecting to Git?Drag and drop your site folder here”的框里。显示绿色的发布网址后,即发布成功,点击网址访问你的故事。
- 点击 Domain Setting ,可修改你故事的网址。
- 如需更新故事,选择 Depolys ,将更新后的文件夹拖入框里即可。(事实上,你一开始也可以通过连接Github,完成全自动的更新发布,这里不再赘述了。)
看一下我们刚发布的 example 。
App*
将网页打包成安装包是件很容易的事。
你可尝试用 HBuilderX 创建“5+App”项目进行云打包,这里不再赘述了。
另一种思路,也可以选择创建“Wap2App”项目,将托管的网页设成“wap站首页地址”,更新与打包更容易些。
评论系统*
为网页插入评论系统里是容易的,同理为 Inky 生成的网页插入评论系统也是容易的。
这里有一个将 Gitlak 评论系统插入的 例子,点我运行体验。
当然如果你愿意,甚至还能插入 DaoVoice 这样的客服系统。
统计代码*
想知道有多少人玩,最简单的方式就是插入统计代码。方法和插入到网页一样,这里不再赘述了。
itch.io
你已经完成了你的杰作!是时候与世界分享它了。
你可以选择将文件上传到允许你托管网页的任何地方,但我们建议 itch.io,因为它是一个拥有独立游戏开发者的伟大社区,它是少数几个允许你上传纯 Web 内容的地方之一,最重要的是,它是免费的!如果你愿意,你甚至可以选择出售你的内容。
itch.io 还允许你上传游戏内容为私有草稿,以便在发布之前测试该过程。
最后,Inkjam 是从 itch.io 托管的,如果你已经将游戏上传到平台上,就可以轻松加入。
高阶 Inky 残卷
INK 最初被设计为游戏引擎的脚本插件,因此具有丰富而强大的功能。我将官方文档中,部分我有兴趣,且对你使用 Inky 可能有帮助的内容翻译至此。如你发现“好像少了些什么”,或想进一步了解,请阅读官方文档。
1. 基础
1.1 Inky 基础教程
1.2 针脚 stitches
结点可有子节点
随着故事变长,在没有其他结构的情况下,它们会越来越乱,难以井井有条。
结点可以包括称为“针脚”(stitches)的子结点,这些使用单个等号(=
)标记。
1 | === the_orient_express === //东方快车 结点 |
例如,可以使用一个结点来写拍摄场景,用结点里的多个针脚来写场景中的事件。
针脚有唯一的名称
针脚可被用于跳转的地址。
1 | * [前往三等厢] |
第一个是默认针脚
如果结点包含针脚,默认跳转到第一个针脚。
1 | * [前往头等厢] |
等同于(…除非我们改变结点内针脚的顺序!):
1 | * [前往头等厢] |
你还可以在针脚外的顶部加入任何内容,但注意此时不会自动运行第一个针脚。
1 | === the_orient_express === |
本地跳转
在结点内部,你无需完整的针脚地址。
1 | -> the_orient_express |
这意味着针脚和结点的名称不能相同,但是不同的结点里可以有相同名称的针脚。(你不能把某个车厢也叫东方快车,但东方快车和蒙古专列里都可以有头等舱。)如果使用了不明确的名称,编译器发出警告。
引用其他 INK 文件
您还可以根据需要将 ink
文件拆分为多个文件,然后使用 INCLUDE
语句引用它们。
1 | INCLUDE newspaper.ink |
INCLUDE
语句应始终位于文件的顶部,而不是在内部其他地方。
对于将结点转移到哪个文件,或者说 INK
文件的拆分没有限制规则(换句话说,拆分文件不会影响游戏的命名空间,和在同一个文件中编写没有区别)。
1.3 选项 choices
后备选项 fallback choices
如果某个地方都是一次性选项,那么多次经历后,可能会出现“内容用光(ran out of content)”的错误。
我们可通过“后备选择”解决问题。后备选项不会直接显示给玩家,但如果没有其他选项可选是,则游戏会自动选择。
后备选项只是“没有选项时的选择”:* -> out_of_options
并且,在轻微滥用语法下,我们可以使其中的内容做出默认选择:
1 | * -> |
举个例子:
1 | === find_help === |
1.4 可变文本 variable text
序列
序列(默认)每次都会显示下一个元素。当“没有下一个”时,它将继续显示最后一个元素。
1 | 电台嘶嘶作响。 {“三!”|“二!”|“一!”|传来了爆炸的响声。|这只有些噪声} |
周期,一次性,随机
循环(标记 &
)就像序列,但它循环内容。
1 | 今天是 {&周一|周二|周三|周四|周五|周六|周日}。 |
一次性(标记 !
)也像序列,但当“没有下一个时”,它将不显示。
1 | 他给我讲了个笑话。 |
随机(标记 ~
)随机显示内容。
1 | 我掷了枚硬币。 {~字面|花面}。 |
更多技巧
可以包含空元素。
1 | 我向前迈了一步 {!||||然后灯火熄灭了。 -> eek} |
可以进行嵌套。
1 | 这个怪兽{&{没浪费时间|}打|爪}{&你|进你{&的腿|的胳膊|的胸}}。 |
可插入跳转。
1 | 我{在等。|等了一会|打盹了。|醒来又等了会。|放弃并离开了 -> leave_post_office} |
也可在选项中使用。(注意,{
不能出现在选项开头,会与条件选项混淆;/
出现在里面可能出现错误。)
1 | + “您好, {&主人|Fogg先生|你|棕眼}!”[] 我喊道。 |
可变文字可以在循环内使用,让游戏看起来很智能,而无需特别的工作。
这里是一个单结点版的”打地鼠”,体验故事运行。我们使用一次性和后备选项,阻止“地鼠逃走”,游戏始终会结束。
1 | ->whack_a_mole |
这里有一些生活建议。请注意选项,电视的吸引力永远不会消失:
1 | === turn_on_television === |
1.5 查询 queries
INK 提供了一些有用的关于游戏状态“game level”查询,用于条件逻辑。它们不是语言的一部分,但它们总是有用的,并且不能由作者编辑。从某种意义上说,它们是语言的“标准库函数”。惯例是用大写字母来命名。
CHOICE_COUNT()
CHOICE_COUNT
返回到目前为止,当前区域内的选项数,例如:
1 | * {false} Option A |
false 表示不生成此选项,因此计数是1,显示的B和C(?)。
TURNS()
TURNS
返回自游戏开始以来的回合数。
1 | 您总共经历了{TURNS()}个结点。 |
TURNS_SINCE(-> knot)
TURNS_SINCE
返回自上次访问特定结点或针脚的跳转次数(玩家输入的)。
值为 0 表示”在经历这个区域”。
1 | ==laugh |
值为 -1 表示”从未经历过”,其他任何正值都意味着“经历过多次了”。
1 | * {TURNS_SINCE(-> sleeping.intro) > 10} 你觉得很累…… -> sleeping |
注意,这里实际只是向TURNS_SINCE
传递一个参数“转移的目标”
,而非是跳转至结点地址,此处不进行节点跳转。
SEED_RANDOM()
SEED_RANDOM
出于测试目的,固定随机数生成器通常很有用,这会让每次播运行 INK 都固定产生相同的结果。你可以通过“种子”随机系统来实现。
1 | ~ SEED_RANDOM(235) |
传递给种子函数的数字可以是任意的,不同的种子将导致不同结果的序列。
2. 编织 weave
到目前为止,我们一直以最基础的方式构建故事的分支,从“选项”到“页面”。
但这要求我们命名故事中的每个目的地且保持唯一性,可能会减慢编写速度并阻挠较小的分支。
INK 拥有更为强大的语法,旨在简化始终向前的故事流(写起来像大多数的故事,而非计算机程序)。
这种系统称为“编织”(weave),它由基础语法和选项语法,以及两个新功能构成:“收束标记 -
”,“选项与收束的嵌套”。
2.1 收束 gathers
收束(gathers) -
将小分支收回到一起。让我们看这个例子:
1 | “怎么了?”我的主人问。 |
在真正的游戏中,这三个选项都可能导致相同的结果,Fogg 先生离开了房间。我们可用收束来执行此操作,而无需创建任何新结点或新跳转。
1 | “怎么了?”我的主人问。 |
我们可将这些“收束和分支”串在一起,让故事始终向前运行。
1 | === escape === |
这是最基本的编织。本节后面还详细介绍了编织的嵌套、包含选项和跳转、内部跳转、以及根据前面的经历来影响后续的内容。
编织的哲学
编织不仅仅是方便分支收束,也是一种强大的内容创作的方式。上面的示例就已有4种路线,甚至可能会更复杂。使用普通的跳转,必须点到点的检查分支,且很容易出错。
通过编织,只需从顶部开始,就能运行到故事底部。在基本编织结构中,不会有跳转错误,编写的文本也很容易浏览。这意味着无需测试游戏中的所有分支,就能确保它们都能按预期工作。
使用编织还可以轻松修改选项,拆开故事并插入其他选项,而无需重新设计任何流程。
2.2 嵌套
上面展示的编织是非常简单的“扁平”结构。不管玩家选什么,他们都要经历相同的回合数才能从上到下。但是,有时某些选项需要深或复。因此,编织可以嵌套。
注意,编织的嵌套非常强大且紧凑,但你可能需要一些时间来适应!
选项收束均可
选项嵌套看下方的场景:
1 | - “好吧,Poirot?谋杀还是自杀?” |
提出的问题首选是“谋杀!” 或“自杀!”。如果 Poirot 宣布自杀,就没有其他事情可做了。但是在谋杀案中,还需要一个后续问题,“他怀疑是谁?”
我们可以通过一组嵌套的子选项来添加新选项。通过使用两个星号(**
)而非是一个星号,告诉 INK 这些新选项是另一个选择的一部分。
1 | - “好吧,Poirot?谋杀还是自杀?” |
(用缩进显示嵌套关系是个不错的习惯,即使编译器不在乎这些。)
如果我们想向另一条路线添加新的子选项,可以按类似的方式做。
1 | - “好吧,Poirot?谋杀还是自杀?” |
现在,无论前面选择什么,最后都能收到 Christie 女士的出场。
有时不只是更多的选项,可能还有更多的剧情。我们可以用收束嵌套解决这个问题。
1 | - “好吧,Poirot?谋杀还是自杀?” |
如果玩家选择谋杀,在这一分支后面会有两组选项,完全是“扁平”编织的。
收束是直观的,但它们的行为很难说出来:通常在选择之后,故事会找到下一个非更低级的收束,并移动下来。基本思想是:选项将故事的支线分开,并通过收束将它们重新合在一起。(因此,这被称为“编织”!)
嵌套可以很多层
上面我们使用了两层嵌套。事实上,你深入嵌套多少层都没有限制。
1 | - “讲个故事,船长!” |
这样下去,整个嵌套都会变得难以阅读或操作。因此,如果子选项变得笨拙,跳转到新针脚上是一种好习惯。
但至少在理论上,你可以把你整个故事都写成一个编织。
再看一个更长的例子:
1 | - 我看着 Fogg 先生 |
这里验证了之前的哲学:编织提供了一种紧凑的方式,在有大量分支大量选择的同时,故事依然能顺利的从头走到尾!
2.3 编织定位
有时候只是编织结构就足够了,但有时我们还需要更多的控制权。
在默认情况下,编织中的内容没有地址或标签,这意味着它们不能被跳转到,也无法对其进行测试。在最基本的编织结构中,选项会改变玩家经历;可一旦走过编织,这些经历就会被遗忘。
但如果我们想记录玩家的经历,我们可贴上“标签语法”(label_name)。
贴标签 (label_name)
任何层嵌套的收束都可以使用括号贴标签。- (top)
贴上标签后,可以跳转至此或进行测试,就像结点和针脚一样。这意味着在编织中你可使用之前的语法来管理内容,同时保持清晰稳定的优点。
选项也可以用括号贴标签,就像收束一样。标签括号应位于条件之前。
这些地址也可用于条件测试中,这对测试“由其他选项解锁的选项”很有用。
1 | === meet_guard === |
范围
在同一块编织内,你可只简单的写标签名称;到外面则需要写路径,像同个结点下的不同针脚:
1 | === knot === |
或到另一个结点:
1 | === knot_one === |
更多技巧
事实上,即使没有看到收束,INK 内的所有内容都是在编织中的。这意味着游戏中的所有选项都可贴标签,然后使用它的地址。特别的,这意味着你可以测试玩家为达到特定结果,选了哪个选项。
1 | === fight_guard === |
我们能用贴标签创建内部循环,这是询问NPC的标准模式,体验故事运行。
1 | - (opts) |
选项也可以被跳转到,就好像选择了该选项一样。因此,显示的内容会忽略 []
中的文本,并如果该选项是一次性选项,它将被用掉。
1 | - (opts) |
选项中可直接收束。注意下方第一个选项下二级收束并没其收束作用,而是方便第二个选项的跳转。
1 | * “还好吧,先生”[]我问。 |
后面的内容较为复杂,本文只选取了部分简单的内容进行翻译。如有兴趣请浏览官方文档。
3. 变量与逻辑
到目前为止,我们已经根据玩家看到的东西,做出了条件文本与条件选项。
INK 还支持临时变量与全局变量,存储数字和内容数据,甚至故事流命令;逻辑功能也很齐全,并包含一些额外的结构,以帮助组织更复杂的故事结构。
3.1 全局变量 global variables
最强大的变量,储存游戏某些独特状态属性的变量,对每个故事来说可能都最有用的——从主人公口袋里的钱,到主人公精神状态的SAN值。
这种变量称为全局变量(global variables
),因为可以从故事中的任何位置访问(包括设置和读取)它。(传统情况下,编程会试图避免这种情况,因为它可能会相互冲突。但故事有所不同,有一些事物贯穿始终,全局需要并一直存在。 )
全局变量可以通过语句 VAR 定义在任何地方。给它们指定一个初始值,定义它们是哪种类型的变量:整数、浮点(十进制)、内容或故事的地址。
1 | VAR knowledge_of_the_cure = false |
我们可以控制全局变量以控制选项,并控制条件文本。方式与之前看到的类似。
1 | === the_train === |
变量的值可用类似于序列和条件文本的语法显示出来,这在调试时也很有用:
1 | VAR friendly_name_of_player = "Jackie" |
你可能注意到,上面我们将变量称为能够包含“内容”而非“字符串”。这是有意的,因为在 INK 中定义的字符串能包含 INK ,尽管它始终会计算为字符串(很赞)。
1 | VAR a_colour = "" |
输出“红色,蓝色,绿色或黄色”中的一种颜色。注意,一旦输出了这样的内容,其值就是“确定的”。(量子状态崩溃。)因此,下面内容不会产生有趣的效果。(如果你希望这样做,请使用可变文本!)
1 | 呆子撞到了你,你眼冒金星, {a_colour}和{a_colour}闪烁。 |
这也是为什么 VAR a_colour = "{~red|blue|green|yellow}"
是不推荐的,它会影响到你的整个游戏。
3.2 逻辑运算 logic
显然,我们的全局变量并非为常量,因此我们需要一种语法来改变它们。
默认情况下,INK 中的任何文本都直接显示到屏幕上。因此我们用 ~
来标记表示某行是在进行数值工作。
以下均是为变量分配值:
1 | === set_some_variables === |
以下是测试用的条件:
1 | { x == 1.2 } |
INK 支持数学四则运算(+
,-
,*
和/
),以及 % (或 mod
)进行求余。还支持 POW
(幂)运算:
1 | {POW(3, 2)} is 9. |
还可以用 RANDOM
函数来生成随机整数。RANDOM 就像掷骰子,因此最小值和最大值都包括在内。( temp
为临时变量)
1 | ~ temp dice_roll = RANDOM(1, 6) |
可用上文的 SEED_RANDOM() 进行测试。
INK 作为文字游戏引擎,奇怪的是并没有那么多字符串查询(string queries)方式。这有三种基本查询,相等,不相等和子字符串。以下全部返回 true:
1 | { "Yes, please." == "Yes, please." } |
3.3 条件块 if/else
我们已经看到了用于控制选项和故事内容的条件内容,而 ink
还提供了与常规 if/else - if/else
结构等效的功能。
单纯使用 if
—— 简单而有效的判断:
if
语法的判断条件,是从开始到现在为止产生的条件线索,而 {
… }
语法表示正在测试某些内容。
1 | { x > 0: |
或者加上其他条件 else
:
1 | { x > 0: |
扩展 if/else - if/else
条件块:
上面的语法也可更改形式为另一种结构方式,类似于编程语言中的 switch
语句格式。
1 | { |
在此格式基础之上进一步拓展,我们还可以实现 else-if
形式的条件判断:
1 | { |
(请注意,与其他所有内容一样,空格仅出于可读性考虑,没有语法含义。)
Switch Block (开关座:选择性判断语句):
实际上 ink 的确有和其他编程语言中 switch
类似的语句。
1 | { x: |
示例:关联上下文内容
请注意,这里的判断条件除了可以使用变量之外,还可以使用玩家是否经历过结点作为条件,并且这种条件使用频繁,例如要做一些与当前游戏状态相关的内容:
1 | === dream === |
此语法具有易于拓展的优势,优先选择使用此语法。
条件块中不仅限于逻辑运算
条件块可以用于控制故事内容以及发展走向:
1 | 我望着 Fogg 先生。 |
你甚至可以将选项放在条件块中:
1 | { door_open: |
但是请注意,上面的示例中没有出现编织语法和嵌套语法不是偶然:为了避免嵌套带来的内容混淆,不允许在条件块中加入 gathers
收束点。
实现多种特殊功能的多行块 Multiline blocks
它们是另一种特殊的代码块,可以分别实现不同的功能:
1 | // 顺序执行:根据每次的跳转依次执行,全部执行后持续执行显示最后一行 |
高级:修改的随机洗牌
上面的随机洗牌,实际上是 shuffle cycle
循环随机洗牌;它每次随机选择内容,显示内容,然后重新随机显示内容,显示内容。
随机洗牌还有两种方式:
shuffle once
随机显示执行内容,只显示执行一次,之后不会再执行
1 | { shuffle once: |
shuffle stopping
随机显示执行内容(最后一条除外),当其他项全部随机执行完毕后,将一直保持在最后一条
1 | { shuffle stopping: |
3.4 临时变量 Temporary Variables
临时变量用于临时计算
有时,全局变量无法满足我们的临时需求,这时候 ink 为我们提供了临时变量 Temporary Variables
。
1 | === near_north_pole === |
临时变量只在其定义的当前结点生效,离开结点后临时变量将被丢弃。
结点和针脚可以拥有参数
临时变量有一种非常实用的形式,就是作为传递给任意结点或针脚的一个参数。
1 | * [Accuse Hasting] |
如果要将临时变量从一个针脚传递给另一个针脚,就要用参数。
示例:递归节点中的定义
临时变量可以放心的在递归中使用(与全局变量不同,全局变量改变后递归过程中都会变为改变后的值),因此可以执行下面的内容:
1 | -> add_one_to_one_hundred(0, 1) |
实际上,这种递归节点中的定义非常实用,也因此 ink
中提供了一种特殊的结点,一个 function
。它有一定的限制,并且可以返回一个值,请参阅以下部分。
高级:将 **divert targets**
(跳转目标)作为参数传递
结点、针脚的地址也是一种类型的值,用 ->
符号表示,可以传递与存储。因此以下的内容是正常的,并且非常有用:
1 | === sleeping_in_hut === |
要注意的是,在 generic_sleep
的定义中,是有 ->
符号存在的。因为这里是作为一个参数值存在,如果不加 ->
符号,就会变成下面这样:
1 | === sleeping_in_hut === |
这种情况下, ink
会将 waking_in_the_hut
的结点数字传递给 sleeping
结点,并且尝试跳转到此结点。
3.5 函数 Functions
3.6 常数 Constants
4.1 隧道(?)tunnels
狼也不知道这里 tunnels
翻译成什么更恰当。无端的想起马里奥中可以往返隐藏区域的“管道”。但最后决定还是直译成了“隧道”。
INK 故事的默认结构是“扁平”的树状结构,分支又重新收束回一起,也许还有循环,但故事始终位于“某个位置”。
但是这种扁平结构会使某些事情变得困难:想象一个游戏,可能发生以下的相互作用:
1 | === crossing_the_date_line === |
…但它可能发生在故事中的几个不同地方。我不想为不同的地方写相同内容,但是当内容完成后,它还需要知道返回到哪里。我们可以使用参数来做到这一点:
1 | === crossing_the_date_line(-> return_to) === |
这两个位置都调用并执行相同的故事片段,一旦完成,它们就会回到下一步要去的地方。
但如果这个“故事的部分”更复杂怎么办?如果跨越多个结点怎么办?使用上面的方法,我们必须不断地将“ return-to”参数从一个结点传递到另一个结点,以确保我们始终知道该返回的位置。
因此,INK 把跳转和新功能结合到起来加语言中,其功能类似于子程序,称为”隧道”(tunnels)。
隧道的语法看起来很像跳转,末尾还有另一个跳转:
1 | -> crossing_the_date_line -> |
这意味着运行一个“crossing_the_date_line 的故事”,然后从这继续。
在隧道内部,语法展示进行了简化:用 ->->
语句结束隧道,实质上意味着“继续”。
1 | === crossing_the_date_line === |
注意,隧道结点和普通的结点不同,编译器将不会检查隧道是否真的以“->->”语句结尾,除非在运行时。因此,您需要仔检查进出隧道的跳转。
隧道可串在一起,也可在普通跳转上:
1 | ... |
隧道可以嵌套,因此以下内容也行:
1 | === plains === |