Inkle Inky ink 上手指南
术语表#
Inkle
制作和发行这个系列项目的公司ink
他们推出的一种用于撰写互动小说(interactive stories)的编程语言Inky
专门用来编写ink
的编辑器工具软件 / 集成开发环境(IDE)inklewriter
特指inklewriter.com
这个在线编辑和预览 ink 项目的第三方工具网站
Tips#
- 「缩进」在代码层面没有任何意义,只是为了改善开发时的可读性。
- 「空行」当然也是(仅作代码的排版)。
- 但「换行」不是,换行会体现到实际游玩中的排版上。
- 针脚(stitches,子结点),如果结点直接接子结点默认运行第一个,否则不运行。
- 跳转目标可以作为参数传递。
ink 语法#
首先请打开示例项目,结合实际效果更好理解。
注释#
// 单行注释
/**
* 多行注释
* 想写几行写几行
*/
TODO: 高亮注释 提醒自己需要完成的待办事项
结点(Knots)===
#
就是
结点 ,不是节点 。
类似一种标签(label),使用唯一的标识符(结点名称)来标记一段代码。
注意本来只要是 2 个及以上 的
=
等号就行。
但为了和子结点(单个等号,=
)明显地区分开来,我们惯例使用 3 个等号===
。
=== a_knot ===
随便写些什么。
遇到另一个结点的标记时会自动视为结束。
=== another_knot ===
再随便写些什么。
需要注意的是和 Markdown 的标题 #
类似,解析时只要左边存在就作数。
因此右边是可以省略的,比如:
=== a_knot_without_closed
随便写些什么。
这种格式也是合法的,和上面的效果一样。
针脚(Stitches,即 子结点
)=
隶属于父级结点的子节点。
=== tavern ===
你来到了酒馆,要跟谁说话呢?
+ [向酒保搭话]你向酒保搭话。
-> bartender
+ [向侍者搭话]你向侍者搭话。
-> waiter
+ [向吟游诗人搭话]你向吟游诗人搭话。
-> bard
+ [向沉默的酒客搭话]你向沉默的酒客搭话。
-> silent_drinker
= bartender
“没有这种酒。换一个。”
-> tavern
= waiter
“很抱歉,敝店没有提供这样的咨询服务呢。您需要点别的什么吗?”
-> tavern
= bard
吟游诗人只是对你微笑,依旧弹唱着晦涩难懂的小调。
-> tavern
= silent_drinker
他自顾自地喝着酒,好像完全没注意到你的招呼。
-> tavern
父级结点可以非常轻松地访问子结点(就像访问同级结点一样)。
由于每个结点的标识符(名称)都具有唯一性(在同级下),所以每个结点也会拥有唯一的标识符(名称),即支持在任何地方调用它。
=== fire_magic ===
= fire_ball
你施放了火球术。
=== fast_cast ===
你想施放什么法术?
+ [火球术]
// 使用子结点的唯一标识符
-> fire_magic.fire_ball
+ [还是火球术]
// 直接调用父结点
// 由于父结点除了子结点以外没有其他内容 因此默认为第一个子结点
-> fire_magic // 默认为 .fire_ball
参数
结点和针脚可以拥有参数,也就是说某种程度上你可以把它们视为「函数」。
* [控诉黑斯廷]
-> accuse("黑斯廷斯")
* [控诉布莱克女士]
-> accuse("克劳迪娅")
* [控诉我自己]
-> accuse("我自己")
=== accuse(who) ===
“我控诉{who}!”波洛宣布。
“真的吗?”杰普回道,“{who == "myself":你要这么做?|{who}?}”
“为什么不可以?”波洛回击道。
跳转(Diverts)->
#
用短横线(或者说 减号)-
与右尖括号(或者说 大于符号)>
组成的箭头符号 ->
。
可以跳转到指定的结点,有且仅有这个作用。
有一个需要注意的小细节是和换行的配合。
左边部分和 -> next
=== next ===
右边部分会连接起来显示在同一行。(中间有一个固定的空格)
第一行——
-> next
=== next ===
和第二行之间就会有换行了。
跳转到特殊结点 END
DONE
有两个特殊的预设结点:END
和 DONE
,二者都同样代表「游戏结束」。
选择你的结局:
+ [Happy End] -> happd_end
+ [Bad End] -> bad_end
+ [Good End]就这样吧。 -> END
+ [True End]真相大白。 -> DONE
=== happd_end ===
可喜可贺可喜可贺。-> END
=== bad_end ===
人生长恨水长东。-> END
选项(Choices)/ 按钮 +
*
#
- 使用
+
开头的为常驻选项
每次来到这个节点一定会显示 - 使用
*
开头的为一次性选项
只要你点击过一次,下次再回到这个节点就不会显示了
=== choices ===
下面演示两种不同的选项用法。
+ [\+ 这个选项怎么点都不会消失]
# CLEAR
-> choices
* [\* 这个选项点一次就会消失]
# CLEAR
注意:使用 \* 的选项消失了。
-> choices
可变选项(Varying Choices)[
]
官方起的这个名字有点抽象,我更愿意简单明了地称之为「阅后即焚」。
=== burn_after_reading ===
# CLEAR
下面演示 \[阅后即焚文本\] 的用法。
// 选项全部删除
+ [“这个畜生。”你心里想。但现在还不是时候。]“没问题,先生。很高兴为您服务。”
你展露出无可挑剔的微笑。
-> burn_after_reading
// 只删除选项最后的标点以延续句子
+ “我就是饿死、死外边、从这里跳下去,不会吃你们一点东西[!”]……烤全羊啊嗯。”
“真香。”你擦了擦嘴,“想问什么?赶紧的吧。”
-> burn_after_reading
// 不删除任何东西 保留整个选项的原文
+ “爷今天必须给你整个活。”[]你深吸一口气,“草,走,忽略!”
“漂亮!”路过的行人见状驻足鼓起掌来,有些人甚至吹起了口哨。
-> burn_after_reading
后备选项(Fallback choices)+ ->
/ * ->
我们非常自然的能够联想到这样一种场景:
「如果某个结点内所有选项都是一次性的,重复多次之后用光了全部选项会怎么样?」
会卡住;如果是在编辑器内预览,会报错 ran out of content
。
为了避免这种情况发生,我们需要一个可以在「用光选项」之后应急的处理方式。
即「后备选项」:
“你竟然逃出来了?!你到底是怎么做到的?”
你不能暴露这个秘密。要如何回应?
* [假装懊悔]“我用掉了最后一张闪光卷轴。”你捂着脑袋沉闷开口。
-> next
* [假装哽咽]“哈兰……哈兰他……”你说不出话了。
-> next
+ ->
“我也不太清楚。”[]你挠了挠头,试图蒙混过关,“就这样那样。”
-> next
在上面这个例子中,你每次死里逃生之后,为了守护秘密,都必须用掉一个理由。
根据不同的理由,对方的信任度也会发生变化。
但当你用光所有理由后,游戏仍要继续下去。
因此必须准备「后备选项」,哪怕你已经没有理由可用了。
需要注意一点:后备选项仍然遵从 +
(可重复使用)和 *
(一次性)的规则。
而我要强烈建议的是,如无必要,永远不要使用 * ->
作为后备选项,一律用 + ->
。
试问如果连你的后备选项也是一次性的,那么用掉了后备选项之后,你再用什么呢?
除非你的后备选项能够保证你再也不会触发这个场景。
比如指向 END
/DONE
、# RESTART
、或者特意编排故事线。
既然你能够保证再也不会回到这里,那么用 *
还是 +
对你而言又有什么区别呢?
恕我愚钝,我是实在想不到 * ->
的存在意义,如果有想出用途的读者欢迎点醒我。
胶水(Glue)<>
#
顾名思义,就是把「后面一行」强行粘到「涂了胶水的地方」。
“不好,我们回去!”<> // 这里的胶水无效 因为选项必然换行
+ “去萨维尔街[”],<>
-> as_fast_as_we_could
+ “去沈阳大街[”],<>
-> as_fast_as_we_could
=== as_fast_as_we_could ===
要尽可能地快。”
逻辑判断(Conditional blocks){
}
#
if/else 条件判断
类似别的语言里的 if-else
。
{ true_or_false: // 判断条件
这一行只有 true_or_false 为 true 的时候才会显示。
}
不止文本,选项也可以放在判断结果里执行(满足条件才出现选项)。
{ not injured and (got_the_key or super_powered): // 支持逻辑运算符 and or not
// 这一行只有「无伤抵达」并且「拿到钥匙 或者 力量超强」的时候才会显示。
你打开了这个密室。
}
一个小坑:
and
可以写成&&
,但or
不能写成||
(因为会和「序列」混淆)。
当然也可以加上 else:
{ true_or_false:
结果为 true。
- else:
结果为 false。
}
如果有 else if
,if-else 也可以写成平铺的形式:
{
- bless:
你受到了神圣的庇佑,免疫了这个诅咒。
- own_senior_curse:
这个诅咒被一股比它更为邪恶的力量驱散了。
- else:
你无法抵御这个诅咒。你死了。
}
甚至可以扩展为类似 switch 的形式:
{ x:
- 0: 零
- 1: 一
- 2: 二
- else: 很多
}
用于正文
一个小 Tip:「途经的结点」可以作为判断条件。
当条件判断用于选项时,可以简写作为可选触发的特殊语句:
你要如何前往下一个城镇?
+ 你选择骑马前往[]。
-> ride_a_horse
+ 你选择步行前往[]。
-> just_walk
+ 你脑海里突然响起一个声音:“布响丸辣”[]。
-> have_a_rest
=== ride_a_horse ===
你在骑马疾行的过程中被绊马索暗算,摔下了马。
-> next_town
=== just_walk ===
走到半路,你被路边突然冒出来的一伙蒙面恶徒团团包围。
-> next_town
=== have_a_rest ===
虽然搞不太懂是什么意思,总之还是在这个村子休息一阵子再出发吧。
你养精蓄锐后重新启程,意外发现路上的人烟出奇地少。
-> next_town
=== next_town ===
{ride_a_horse: 你的四肢和脖子折成了不可思议的角度。}
{just_walk: 你竭力反抗。可是双拳难敌四手。}
{ride_a_horse or just_walk: 你死了。}
{have_a_rest: 沉默地不断前进着,你心里莫名生出的异样感越来越强烈。}
{not ride_a_horse and not just_walk: 你怀着不安的心情抵达了邻近的城镇。}
{not (ride_a_horse or just_walk): 你惊讶地发现这个地方已经被烧成了一片白地。}
用于选项
当条件判断用于选项时,可以简写在开头:
“好了,还有别的问题吗?”
+ {have_the_evidence} “等等,你得看看这个。”
+ {not have_the_evidence} “等等,我想起一件很重要的事。”
+ “没有,我们走吧。”
可变文本(variable text){
}
#
当一对花括号 {}
用在文本区域时,视为可变文本。
序列
如果什么标记都没有加,默认视为序列文本,每次触发时依次显示序列中的文本。
当显示次数超过文本总数时,显示最后一条文本。
{我花掉四个银币买了一瓶药剂。|我又买了第二瓶。|我身上已经没有钱了。}
循环 &
类似序列,但显示次数超过总数时,会重新从第一条文本开始,无限循环下去。
今天是{&周一|周二|周三|周四|周五|周六|周日}。
一次性 !
类似序列,但显示次数超过总数时,不再继续显示。
他给我讲了个笑话。
{!我有礼貌的笑了。|我微笑了。|我做了下鬼脸。|我不想再做出反应了。}
随机 ~
每次触发都从可选的文本中随机选取一个显示。
硬币高高抛起,快速旋转着落到我手上。是{~正面|反面}。
进阶技巧
元素可以为空
你前进了一步{!|||,然后火把熄灭了。 -> into_dark}。
可以嵌套
这个怪兽{&{!看起来很不情愿,|犹豫了一下,}{没有选择|试图}攻击|抓}{&你|伤了你{~的腿|的胳膊|的胸}}。
看起来很复杂吗?让我们拆解一下。
首先是外层的循环(&
)序列,显然可得前后两个循环是一一对应的:
{!看起来很不情愿,|犹豫了一下,}{没有选择|试图}攻击
→你
抓
→伤了你{~的腿|的胳膊|的胸}
后者没什么好说的,一个随机(~
)序列,随机攻击腿、胳膊、胸三个部位其中之一。
重点分析前者,前面的部分又可以拆开:
{!看起来很不情愿,|犹豫了一下,}
这是一个一次性序列(!
)- (用掉
看起来很不情愿,
)最开始是不想动手的 - (用掉
犹豫了一下,
)有点犹豫,但还是出手了 - (已经用光文本了)不再犹豫,果断出手
- (用掉
{没有选择|试图}
这是一个普通序列,用掉没有选择
之后就会停留在试图
所以最后的效果:
这个怪兽看起来很不情愿,没有选择攻击你。
这个怪兽抓伤了你的腿。
这个怪兽犹豫了一下,试图攻击你。
这个怪兽抓伤了你的胸。
这个怪兽试图攻击你。
这个怪兽抓伤了你的胳膊。
可以插入跳转
我{耐心等待着|等了一会|又等了一会|开始打盹了|醒来又等了会|放弃并离开了。 -> leave_office}。
可以用在选项中
不过不能用在开头,因为会和「条件判断」混淆,产生无法预知的错误。
在循环的选项内使用,无需特别的处理就可以让游戏看起来很智能。
-> whack_a_mole
=== whack_a_mole ===
{我举起了锤子。|{~没打中!|没有!|不好,它在哪?| 偏了!| 见鬼…| 啊,手滑了!| 嗷呜!|啊哈,抓住了!-> game_over }}
这个{&地鼠|{&不友好的|可恶的|低级的}{&生物|啮齿动物}}{在某处|藏在某处|仍然逍遥法外|嘲笑我|依旧没打中|注定会失败}。 <>
{!我会抓住它!|但这次它逃不掉了。}
* [{~打|砸|尝试}左上角] -> whack_a_mole
* [{~就是|暴击|猛击}右上角] -> whack_a_mole
* [{~砍|锤}中间] -> whack_a_mole
* [{~痛击|肯定是}左下角] -> whack_a_mole
* [{~掀开|重击}右下角] -> whack_a_mole
* ->
你累坏了!地鼠击败了你!
-> game_over
= game_over
游戏结束 # CLASS: end
+ 再玩一次?
# RESTART
-> END // 防止编辑器报语法错误用的 实际在上一行就重启了 永远不会执行到这一行
查询(Queries)#
计算当前存在的选项数量 CHOICE_COUNT()
返回到目前为止,当前区域内存在的选项数量。
* {false} Option A // 条件判断未通过 不显示
* {true} Option B // 条件判断通过 显示
* {CHOICE_COUNT() == 1} Option C // 条件判断通过 显示
当执行 CHOICE_COUNT()
的时候 Option A
没有显示,只有 Option B
显示,因此 CHOICE_COUNT()
为 1。最后结果为显示 Option B
和 Option C
。
计算已经经过的回合数 TURNS()
返回自游戏开始以来的回合数(跳转次数),重复的结点也算。
计算从某个结点开始经过的回合数 TURNS_SINCE(-> knot)
返回自上次访问特定结点(包括
若返回值为 0 表示「正在经历这个结点」。
=== gamble ===
你的赌金输掉了。
+ [继续下注]“至少把本金捞回来吧?”你想。
-> gamble
+ {TURNS_SINCE(-> gamble) == 0} [自认倒霉]算了,以后再也不碰了。
-> END
如上例所示,只有第一次赌博可以抽身离开,只要开始尝试「翻本」就再也回不了头了。
十赌十输,赌狗最后 100% 会赔掉所有人性。答应我,远离赌博,好吗?
随机数生成器种子 SEED_RANDOM()
如果你的游戏里存在随机数生成,测试的时候显然很不方便。
此时可以使用固定随机种子,保证每次随机的结果是一致的。
这里的一致指序列一致,结果本身还是随机的。
某些特殊的机制也可能会用到。
编织(Weave)-
#
到目前为止,我们一直在以最基本的方式构建故事,从「选项」到「页面」。
但这要求我们:
- 必须命名故事中的每个分支的目的地(结点)
- 且保持唯一性(结点的标识符)
对像我这样的取名废来说简直是噩梦。
这大大拖慢了我们编写的速度,阻挠了我们创作更多分支(的意愿)。
因此 ink 推出了一种新的语法,旨在简化「始终向前的故事线」。
最后大部分时候写起来更像流式的「正常小说」,而不是「计算机程序」。
它就是「编织」:
这也是「结点」「针脚」等术语的命名原因。
“怎么了?”我的主人问。
* “我有些累。”[]我重复道。
“确实。”他答道,“这太糟了。”
* “没事,先生!”[]我回应道。
* “我说,这次旅行真可怕[。”],我不想再继续了。”
“啊。”他有些动容,"你看起来很沮丧。到了明天,情况会好些的。"
- Fogg 先生离开了房间。 // 所有分支都会在这里收束
无论你选什么选项,都会走向同一个结果:「Fogg 先生离开房间」,这就是「收束」。
可以嵌套
- “讲个故事,船长!”
* “好吧,你们这些海狗。这个故事是这样的……”
* * “在一个黑灯瞎火狂风暴雨的夜晚……”
* * * “……船员们都很不安……”
* * * * “……他们对船长说……”
* * * * * “……给我们讲个故事!”
* “不,已经过了你们睡觉的时间。”
- 无一例外,船员们开始打起哈欠。
总结一下,分支/收束这种方式的好处是你不必再耗费心力思考哪个选项该去哪里。
只需要专注于撰写故事,在拥有大量分支可供选择的情况下,不用额外处理,依旧可以自然而然地顺利从头走到尾。
可以定位 (
)
理论上整个 ink
文件都是一个大的编织,因此你可以在任何地方使用标签 (label_name)
来标注「发生过一件事」。
=== meet_the_guard ===
卫兵在盯着你。
+ (greet) [打招呼]“早。”
+ (get_out) “让开。”[]你对卫兵说。
- “嗯。”卫兵回应道。
+ {greet} “天气不错哈?”
+ “咦?”[]你回应道?
+ {get_out} [把他推到一旁]你把他推到一旁。作为礼尚往来,卫兵直接拔出了剑。
-> END
- “嗯。” 卫兵给你一个小纸袋,“太妃糖?”
如果换成 galgame 的逻辑来说,就是「立起了 flag」。
“好安静啊。说点什么吧。”
* (dead_flag_1) “打完这仗我就回老家结婚。”
* (dead_flag_2) “干完这一票我就金盆洗手了。”
* (dead_flag_3) “もう何も恐くない。”
* [沉默]你默默擦拭剑身,什么都没说。
- {dead_flag_1 or dead_flag_2 or dead_flag_3: 你阵亡了。 -> END}
- 战争结束了。 -> END
标签也可以在任何地方被检查,同「结点 / .
链式引用。
利用标签可以构建标准的 NPC 询问场景:
- (opts)
* “我从哪能弄到制服吗?”[]你询问快乐的卫兵。
“当然,在储物柜那。”他笑着说,“不过可能不太合身。”
* “告诉我安防系统。”
“它很老。”卫兵信誓旦旦地保证,“就像古董一样老。”
* “这里的狗?”
“很多很多。”卫兵咧嘴一笑,“也都是饿鬼。”
* {loop} [问够了] // 只有经过了 loop 才会显示,即:要求玩家至少问一个问题
-> done
- (loop) // 在警卫厌烦前循环
{ -> opts | -> opts | } // 用可变文本序列控制卫兵是否厌烦
他挠挠头。
“好吧,不能整天只站着说话。”他说道。
- (done)
你谢过警卫,便离开了。 -> END
变量和逻辑运算(Variables and Logic)#
全局变量(Global Variables)
定义全局变量:
VAR knowledge_of_the_cure = false
VAR players_name = "Emilia"
VAR number_of_infected_people = 521
VAR current_epilogue = -> they_all_die_of_the_plague
使用全局变量:
=== the_train ===
火车颠簸了一下。{ mood > 0: 我觉得还好,没介意奇怪的颠簸|我受不了了}。
* { not knows_about_wager } “但是,先生,我们为什么要旅行?”[]我问道。
* { knows_about_wager} 我考虑了我们有些离奇的冒险[]。这可能吗?
打印全局变量:
VAR friendly_name_of_player = "Jackie"
VAR age = 23
我的名字叫 Jean Passepartout,但我的朋友都叫我 {friendly_name_of_player}。我 {age} 岁了。
进阶用法:
跳转命令本身是一种值,因此可以存在变量里。
VAR current_epilogue = -> everybody_dies
=== continue_or_quit ===
现在放弃,还是努力拯救你的国度?
* [绝不言弃] -> more_hopeless_introspection
* [我太累了] -> current_epilogue
再进一步,由于 ink 本身(解释器)和「文本」高度结合的特性,计算式也是一种「值」。
也就是说,你可以轻松使用类似别的语言里的 eval()
功能,把字符串当作代码执行。
VAR a_color = ""
~ a_color = "{~红色|蓝色|绿色|黄色}"
{a_color} // 等于直接 {~红色|蓝色|绿色|黄色}
需要注意的是,当你输出过一次之后,这个式子输出的结果就确定了。
就好比薛定谔的猫,你不观测它,它就是生与死之间的叠加态;你一观测它,它就会坍缩为某个确定的本征态。
呆子撞到了你,你眼冒金星,{a_color}和{a_color}交替闪烁。
// 试图这样实现多次随机是不行的 两次输出的颜色一定会是同一种颜色
临时变量(Temporary Variables)
只在定义的结点内部生效,离开结点后里面的临时变量将被丢弃。
=== warn ===
~ temp number_of_warm_things = 0
常量(Constants)
互动小说往往需要状态机来追踪当前故事进程已经发展到哪个阶段。
有很多方式可以实现这一点,但最方便的还是使用常量。
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++
}
逻辑与数值运算(Logic and Numerical)
=== set_some_variables ===
~ knows_about_wager = true
~ x = (x * x) - (y * y) + c
~ y = 2 * x * y
=== estimate_variables ===
{ x == 1.2 }
{ x / 2 > 4 }
{ y - 1 <= x * x }
基本的数学运算都支持,加 +
、减 -
、乘 *
、除 /
,以及取余 %
(或者 mod
)。
甚至支持「(允许指数为小数的)幂运算 POW()
」:
{POW(3, 2)} 的结果为 9。
{POW(16, 0.5)} 的结果为 4。
当然也可以生成随机数:
~ 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)
注意 ink 是带有隐式类型转换的,尤其是除法——
整数除以整数自动取整、浮点数除以浮点数仍为浮点数。
当你不想要自动类型转换,或者需要对数值进行舍入时,可以强制指定类型转换:
{INT(3.2)} 的结果为 3。
{FLOOR(4.8)} 的结果为 4。
{INT(-4.8)} 的结果为 -4。
{FLOOR(-4.8)} 的结果为 -5。
{FLOAT(4)} 的结果为,呃,仍然为 4。
官方自承很奇怪的一点是,ink 作为一个文本游戏引擎,并没有那么多花里胡哨的字符串处理功能。只有最简单的三种判断:
{ "Yes, please." == "Yes, please." } // 等价
{ "No, thank you." != "Yes, please." } // 不等价
{ "Yes, please" ? "ease" } // 包含
// 以上三个结果都为 true
多行块(Multiline blocks)#
其实就是展开的序列:
// 普通序列:依次显示序列中的文本,显示完后停留在最后一条
{ stopping:
- 我进入了那个赌场。
- 我又进入了那个赌场。
- 又一次,我进去了。
}
// 循环序列:依次显示序列中的文本,显示完最后一条后回到开头
{ cycle:
- 我屏住呼吸。
- 我不耐烦地等着。
- 我犹豫着。
}
// 一次性序列:依次显示序列中的文本,显示完最后一条后不再显示
{ once:
- 我的运气如何?
- 这一手我能赢吗?
}
// 随机序列:随机抽取其中一个显示
坐上赌桌后,我抽了一张牌。 <>
{ shuffle:
- 红桃 A。
- 黑桃 K。
- 方片 2。
“这局你输了!”庄家叫嚷道。
}
默认的「随机序列」确切地说其实是「随机循环序列」。
你可以视为每次洗牌后抽一张,然后在放回去等待下一次洗牌。
还有两种进阶的随机方式:
// 随机一次性序列:每次随机抽取一个,不放回去,抽完就没了
{ shuffle once:
- 太阳很晒。
- 天气很热。
}
// 随机停留序列(对应普通序列):每次随机抽走一个,抽到只剩最后一个
{ shuffle stopping:
- 一辆银色宝马呼啸而过。
- 一辆明黄色的野马正在转弯。
- 这里有许多车。
}
函数(Functions)#
我们之前说过结点和针脚(子结点)近似函数,但毕竟不是真正的函数。
最主要的原因就是因为「不能返回值」;真正的函数是可以
所以说起来,「结点/针脚」是类似「命令」的东西,「函数」才是可以直接返回值的真正的「行内函数」。用过 Emuera、写过
erb
(era basic)的有没有既视感?
=== function lerp(a, b, k) ===
~ return ((b - a) * k) + a
=== function say_yes_to_everything ===
~ return true
~ x = lerp(2, 8, 0.3)
* {say_yes_to_everything()} 'Yes.'
至于返回另一个函数(的结果)或者递归调用自己都是允许的:
=== function say_no_to_nothing ===
~ return say_yes_to_everything()
以引用方式传递参数(ref argv
)
默认情况下,我们传递的参数是「深拷贝后的
但我们有时会需要对某个变量进行某种处理,此时引用就派上用场了。
直接传递变量本身,在函数内直接进行操作。
又一个我们熟知的 erb 里也有的特性,也许这就是解释型脚本语言的殊途同归吧。
=== function alter(ref x, k) ===
~ x = x + k
隧道(Tunnels)#
虽然结点允许传递「跳转目标」作为参数,但这种用法一旦多了还是显得繁琐和啰嗦:
=== crossing_the_date_line(-> return_to) ===
- -> return_to
=== outside_honolulu ===
我们抵达了 Honolulu 最大的岛。
- (postscript)
-> crossing_the_date_line(-> done)
- (done)
-> END
=== outside_pitcairn_island ===
水上的船驶向小岛。
- (postscript)
-> crossing_the_date_line(-> done)
- (done)
-> END
而且还有个问题就是:如果跳转跨越了多个结点怎么办?使用上面的写法,我们必须把 return_to
参数不断传递下去,以确保我们始终知道返回的位置。
为了解决这个问题,ink 为「执行完跳转回原处」的这类结点加入了一种语法。
也就是「隧道(Tunnel)」:
=== crossing_the_date_line ===
// 这里是隧道!
- ->->
用起来也很简单:
-> crossing_the_date_line ->
你甚至可以连续串起多个隧道:
-> crossing_the_date_line -> check_foggs_health -> done
多个隧道本身也是可以嵌套的。
所以类似「战斗结算」这种场景(分别结算经验值、道具、货币)写起来会很方便。
线程(Threads)#
- TODO
不想写了,累了,咕了。
如果你需要这部分内容,请联系我催更。(文末有联系方式)
列表(Lists)#
- TODO
杂项#
插入图片#
# IMAGE: image_name.png
如果你使用的图片达到一定数量,推荐使用单独的文件夹来存放。
此时可以通过相对路径访问:
# IMAGE: img/image_001.png
放在行尾的图片也可以正常插入,比如:
她拿出一张地图:“你看这里。” # IMAGE: img/level_3/map_01.png
图片总是会显示在文本上方(而不是重叠成一团 某 Emu 学废了吗?),因为所有标记都是和特定的文本行相关联的。当标记被单独放置在某一行时,它将自动与下方的文本行关联。
清空屏幕#
# CLEAR
重启游戏#
这个命令同时还会 清空玩家所有「经历过的结点」和「修改过的变量」。
# RESTART
URL 链接跳转#
+ 跳转到网址
// 注意必须用 \/\/ 转义 否则后面的 URL 会视为注释文本
# LINK: https:/\/lackb.fun
-> END
+ [在新窗口打开网址]已经在新窗口打开网址。
# LINKOPEN: https:\/\/lackb.fun
-> END
HTML 标签#
警告:这是一个我不清楚它到底是
但我不能保证它以后会一直可用,而且确实也不怎么 neat,完全跟优雅二字沾不上边。
你可以直接在正文里写 HTML 标签。
<span style="color: blue;">这是一条带有<b>加粗</b>、<i>斜体</i>、<u>下划线</u>、<s>删除线</s>的蓝色文本。</span>
它在 Inky 编辑器里会显示异常,但当你把它导出到 Web 再打开时,它是正常工作的。
合并多个脚本文件#
INCLUDE utils/player_status.ink
INCLUDE level_1/dungeon.ink
项目元(meta)信息#
暗黑(Deep Dark)模式:
# theme: dark
作者署名:
# author: 你の名字
自定义 CSS 的类(class)#
你推开房门。
立刻看到地上有一滩血。 # CLASS: danger
编辑导出项目的 style.css
文件,新增:
|
|
你推开房门。
立刻看到地上有一滩血。
另外还有可以直接使用的内置默认样式:
# CLASS: end