如何使用Ink?

ink是一款叙事性的脚本语言 ,可以用来编写文字游戏

下面介绍其基本的使用方法

一、基础部分

1). 内容

1.文本

普通文本,会直接输出

复制代码
Hello, world!
2.注释

使用 // 作为注释

复制代码
"What do you make of this?" she asked.
// Something unprintable...

使用TODO: 编辑器会标红提示

复制代码
TODO: Write this section properly!
3.标签

使用# 打标签,不会显示

可以用某种手段操作,提供额外信息

复制代码
A line of normal game-text. # colour it blue

2). 选项

1.选项

使用* 创建选项,选择之后,进入分支,会输出一遍选项文本

复制代码
Hello world!
*	Hello back!
	Nice to hear from you!

使用 包裹选项,可以不输出选项文本

复制代码
Hello world!
*	[Hello back!]
	Nice to hear from you!

使用 包裹选项一部分时,

[ 之前的既会出现在选项中,又会输出在文本中

]之后的不会出现在选项中, 但会输出到文本中

包裹的不会输出到文本,但会出现在选项中

复制代码
Hello world!
*	Hello [back!] right back to you!
	Nice to hear from you!
2.多个选项

一个选项后面 是对应的分支,多个选项可以这样写

复制代码
"What's that?" my master asked.
*	"I am somewhat tired[."]," I repeated.
	"Really," he responded. "How deleterious."
*	"Nothing, Monsieur!"[] I replied.
	"Very good, then."
*  "I said, this journey is appalling[."] and I want no more of it."
	"Ah," he replied, not unkindly. "I see you are feeling frustrated. Tomorrow, things will improve."

3). 节点

节点是一段内容 ,对段落进行标记,方便管理

1.创建节点

有两个或以上的 = 号标记一个节点,后面的 = 是可选的

名称必须是单个单词,不能有空格

复制代码
=== top_knot ===
==== top_knot ====
== top_knot

它像一个标题,随后跟随的的内容都属于该节点

复制代码
=== back_in_london ===
We arrived into London at 9.45pm exactly.
2.跳转到节点

节点内的内容不会自动输出,使用 -> 符号跳转进行输出

复制代码
-> top_knot

=== top_knot ===
Hello world!
3.严格的结尾

Inky需要有严格的结尾 使用 ->END 或 ->DONE 明确标记结尾,否则会报错

或者在流程结束后用选择支 或者 -> 跳转到其他敌方

复制代码
=== top_knot ===
Hello world!
-> END  //注意大写

4) 跳转

1.从一个节点跳到另一个节点

跳转时自动的 ,不需要用户点击

=== back_in_london ===

复制代码
We arrived into London at 9.45pm exactly.
-> hurry_home

=== hurry_home ===
We hurried home to Savile Row as fast as we could.
2.跳转平滑的无缝的

跳转可以在段落中 ,将两段内容拼接成一段

复制代码
=== hurry_home ===
We hurried home to Savile Row -> as_fast_as_we_could

=== as_fast_as_we_could ===
as fast as we could.
3.胶水

将两行连接到一起,

原理分析: 删除前后内容之间所有的换行符

复制代码
=== hurry_home ===
We hurried home <>
-> to_savile_row

=== to_savile_row ===
to Savile Row
-> as_fast_as_we_could

=== as_fast_as_we_could ===
<> as fast as we could.

对节点有效,但是对选项好像无效

复制代码
=== hurry_home ===
We hurried home <>
* to_savile_row
to Savile Row

5) 分支流

1.基础分支

使用节点、选项、跳转,你可以这样组织你的结构

复制代码
=== paragraph_1 ===
You stand by the wall of Analand, sword in hand.
* [Open the gate] -> paragraph_2
* [Smash down the gate] -> paragraph_3
* [Turn back and go home] -> paragraph_4

=== paragraph_2 ===
You open the gate, and step out onto the path.

...
2.世界线收束

合理运用跳转,让分支之后重新收束

复制代码
VAR grilfriend = ""
=== 攻略美少女 ===
要选择谁攻略呢?
*	[绫地宁宁] 好难选呀!
-> 宁宁线
*	[四季夏目] 决定了!
-> 夏目线
*	[空门苍] 就是她了!
-> 苍线
=== 宁宁线 ===
~grilfriend = "宁宁"
经过激烈的思想斗争,
<>我最终选择了宁宁
之后我们每天在学校里收集心之回忆 
-> 结尾

=== 夏目线 ===
~grilfriend = "夏目"
<>还是要选夏目
之后我们每天在咖啡馆愉快工作
-> 结尾

=== 苍线 ===
~grilfriend = "苍"
<>我一定要选苍
之后我们每天一起在夜间上山指引蝴蝶
-> 结尾

=== 结尾 ===
在某个夕阳西斜的傍晚,
我和{grilfriend}走在最熟悉的小路上
我静静地牵着她的手
两个人不约而同地看向对方
她的微笑是如此的可爱动人
我开口到:"明天再一起哟!"
"嗯!"
->END
3.循环

可以用 -> 实现无限循环

inky有一些功能结合循环使用,比如让内容自行变化,比如控制选项被选择的频率

复制代码
=== round ===
循环体
+ [开始循环]->round

//点击一次后开始自动无限循环
//不能实现循环的控制,因此不推荐使用
=== round ===
循环体
* [开始循环]->round
+ ->round

6) Includes和Stitches

1.Knots还可以被划分成Stitches

用一个 = 号,可以将Knots划分成多个Stitches,方便管理

复制代码
=== the_orient_express ===
= in_first_class
	...
= in_third_class
	...
= in_the_guards_van
	...
= missed_the_train
	...

可以将knot比作一个场景,stitches就是其中的事件

2.选择Stitches

可以用 . 号 选择Knots的子Stitches

the_orient_express.in_third_class 称为Stitches的地址

复制代码
*	[Travel in third class]
	-> the_orient_express.in_third_class

*	[Travel in the guard's van]
	-> the_orient_express.in_the_guards_van
3.默认进入第一个stitch

进入Knot 默认会进入第一个Stitch ,

但是当前方有 -> 跳转时,就不会进入Stitch了

复制代码
=== the_orient_express ===

We boarded the train, but where?
*	[First class] -> in_first_class
*	[Second class] -> in_second_class

= in_first_class
	...
= in_second_class
	...
4.本地跳转

同一个Knot内部Stitch的跳转 可以不使用完全的地址

直接使用内部的名称跳转

这意味着 stitch 和 knot 不能同名,但不同 knot 可以有同名的 stitch。

如果产生了歧义,编译器会给出警告

复制代码
=== the_orient_express ===
= in_first_class
	I settled my master.
	*	[Move to third class]
		-> in_third_class

= in_third_class
	I put myself in third.
5.引用其他文件

你可以将文件分成多个,./是同一级目录,.../是上一级目录

然后用INCLUDE 引用文件

然后用INCLUDE应该写在文件头,并且不能放到Knots中

复制代码
INCLUDE newspaper.ink
INCLUDE cities/vienna.ink
INCLUDE journeys/orient_express.ink
INCLUDE ./攻略美少女.ink    
INCLUDE ../攻略美少女.ink

7) 有变化的选项

1.普通选项只能选一次

用 * 设置的选项,只能选一次

如果有循环,下次到这个选择支时,这个选项就消失了,最终你会没有选项可选

复制代码
=== find_help ===

You search desperately for a friendly face in the crowd.
*	The woman in the hat[?] pushes you roughly aside. -> find_help
*	The man with the briefcase[?] looks disgusted. -> find_help
2.默认选项 (Fallback Chioce)

选项内容为空白时,玩家在选择时不会显示,但是当选项耗尽时会自动选择

复制代码
=== find_help ===
You search desperately for a friendly face in the crowd.
*	The woman in the hat[?] pushes you roughly aside. -> find_help
*	The man with the briefcase[?] looks disgusted . ->find_help
*	->out_of_options
*   ->
    默认选项,不会显示,在选项耗尽之后自动选择 ->END

=== out_of_options ===
自动选择的分支,不会显示,在在选项耗尽之后自动选择 ->find_help

用 * 时 默认选项也只会出现一次

3.可重现的选项(Sticky choices)

用 + 标记的选项,可以重复出现,不会被耗尽

复制代码
=== homers_couch ===
+	[Eat another donut]
	You eat another donut. -> homers_couch
*	[Get off the couch]
	You struggle up off the couch to go and compose epic poetry.
	-> END

默认选项(Fallback Chioce)也可以重复出现

复制代码
=== homers_couch ===
*	[Eat another donut]
	You eat another donut. -> homers_couch
*	[Get off the couch]
	You struggle up off the couch to go and compose epic poetry.
	-> homers_couch
+	-> sit_in_silence_again

=== sit_in_silence_again ===
安静地坐下 ->END
3.条件选项(Conditional choices)

可以将 "玩家是否看过某段内容作为条件" 显示和隐藏选项

方法是将knot / stitch的地址 包裹在{ }中

某个 knot 内的任意一个 stitch 被看过,那么对该 knot 名称的测试就会返回 true。

用 * 时 条件选项也只会出现一次,用 + 时 条件选项可以出现多次

当然 这只是inky的其中一种逻辑判断方式

复制代码
今天放学后,你在校门口看到了熟悉的身影。
她站在夕阳下,抱着书,看起来有点犹豫。
"啊,是你。"  
->after_school
=== after_school ===

"要一起回家吗?"
* [什么也不说] 
	->no_say
* {no_say}[找借口拒绝]
    -> make_refuse
* {after_school.make_refuse} [一起回家]
	-> walk_home

= no_say
	她歪着头,有些疑惑:<>
	->after_school
	
= make_refuse
    "不要。"
	她低下头,语气明显冷了几分:<>
	->after_school

= walk_home
	你点了点头。
	"太好了。"  
	她露出了松了一口气的笑容。
	->END
多个条件

一个选项可以有多个条件

复制代码
* {make_refuse } {no_say} [一起回家]
4.逻辑运算符

支持 与或非逻辑运算

运算符 and (还可写作&&)、or(还可写作 || ) 、not(还可写作 ! )

不支持xor

使用 !做非运算时可能产生歧义 ,与一次性列表混淆 {!text},因此建议用not 做非运算

复制代码
*	{ not (make_refuse or no_say) && (have_time || good_mood) } [ 一起回家 ] 

逻辑运算操作都要在大括号中执行,可以用小括号改变运算优先级

5. knot/stitch标签的运算

knot/stitch的标签在运算时,其实表示的是这段内容被看过的次数

0表示false 非0表示true

复制代码
* {seen_clue > 3} [Flat-out arrest Mr Jefferson]

inky还支持更多的逻辑,还可以创建变量,在后面讨论

8) 变化的文本

文本可以在输出时产生差异

1. Alternatives

Alternatives是用 大括号{ }包裹,并用竖线 | 隔开的文本

它可以让内容 多次输出时产生不同,

它可以用最少的代价营造出一种智能感

2. 几种Alternatives
Sequences

序列(sequence,也称为"停止块 / stopping block")

它会记录自己被访问了多少次,并在每一次访问时显示下一个元素。当新的内容用尽后,它会继续显示最后一个元素。

复制代码
"要一起回家吗{主人|亲爱的|欧尼酱|狗修金}?"
Cycles (用& 标记)

Cycles和Sequences类似,但是会循环输出

复制代码
"要一起回家吗{&主人|亲爱的|欧尼酱|狗修金}?"
Once-only (用! 标记)

Cycles和Sequences类似,但是当内容用尽之后,就什么都不输出了

可以看作是最后一个元素是空白的Sequences

复制代码
"要一起回家吗{!主人|亲爱的|欧尼酱|狗修金}?
Shuffles (用~ 标记)

Shuffles 随机输出其中的一个

复制代码
"要一起回家吗{~主人|亲爱的|欧尼酱|狗修金}?
3. Alternatives的特点

Alternatives 可以包含空的元素

复制代码
"要一起回家吗{||主人|亲爱的|欧尼酱|狗修金}?

Alternatives 可以嵌套

复制代码
"要一起回家吗?{&{!亲爱的|变态的}欧尼酱|{~可爱的|愚蠢的}狗修金}.

Alternatives 可以包含跳转

复制代码
=== after_school ===
"要一起回家吗?{欧尼酱|狗修金|->tired}".
+ [嗯嗯]
->after_school

=== tired ===
"你好像有点不舒服?"
"没问题吗?",她投来关切的眼神
->END

Alternatives 可以用在选项中,但是其中不能包含跳转

如果选项输出到文本中了,也会消耗一次

复制代码
+ [不要叫我{欧尼酱|狗修金|主人}了]

用在选项开头会与 条件判断 混淆,需要在前面加上转义字符 \ 来识别文本

复制代码
+  \ {&欧尼酱|狗修金|主人}是我哟   //转义字符后面有一个空格
+  [{&欧尼酱|狗修金|主人}是我哟]

还可以创建多行alternatives,后面 multiline blocks 会详细解释

4. 根据条件输出文本

就像编程语言中的if语句一样 根据条件输出文本

if语句
复制代码
{clean_classroom :我有点累了}

//用编程语言解释
if(看过clean_classroom)
{
	输出:我有点累了
}
if...else...语句
复制代码
{trouble_meet_girl.help_her: 我喜欢你|我讨厌你}

//用编程语言解释
if(看过trouble_meet_girl.help_her)
{
	输出:我喜欢你
}
else
{
	输出:我讨厌你	
}
嵌套的if...else...语句
复制代码
{trouble_meet_girl.help_her: 
	我{girl_heart_beat:爱|喜欢}你
|
	我讨厌你
}

//用编程语言解释
if(看过trouble_meet_girl.help_her)
{
	输出:我 if(看过girl_heart_beat){输出:爱}else{输出:喜欢} 你
}
else
{
	输出:我讨厌你	
}

9) 一些查询函数

一些查询游戏状态的函数,它的值作者也不能改变,可以说是inky的库函数

CHOICE_COUNT()

当前选项在选择支的索引,索引从0开始

在文本中使用,则返回0

复制代码
+ Option A  Current_Choice_Index : {CHOICE_COUNT() }   //结果为0
+ Option B  Current_Choice_Index : {CHOICE_COUNT() }   //结果为1
		Body:{CHOICE_COUNT() }      //结果为0
+ Option C  Current_Choice_Index : {CHOICE_COUNT() }   //结果为2

如果选项输出到文本中,结果也为0

TURNS()

当前文字游戏的回合数

记录从游戏开始到现在,玩家做了多少次选择

因为初始值从0开始, 其数值为 "玩家的选择数-1"

对于默认选项(Fallback Choice),因为是系统自动选的,所以数值不会增加

复制代码
	{TURNS() < 6: ->test2 }       //用作条件判断                  
	{TURNS()}                     //用作结果显示     
TURNS_SINCE(-> knot)

距离这个knot/stitch 上一次进入以来,玩家做了多少次选择

返回值的含义:

  • 返回值为 0,表示上一次进入该knot之后,玩家还没有做过选择

  • 返回值为 -1,表示"从未被看到过"

  • 任何其他的正数,表示它是在 那么多回合之前 被看到的
    参数:
    传入的是带箭头的跳转目标(divert target)
    因为 knot 名字本身代表的是一个数字(阅读次数),而不是故事中的一个位置

    knot_name ≈ 这个内容被看过多少次(int)
    -> knot_name ≈ 这个内容所在的位置(story address)

    === test1 ===
    test1主体 距离上次test2,玩家选择了{TURNS_SINCE(->test2)}次
    ->test2

    === test2 ===
    test2主体 距离上次test2,玩家选择了{TURNS_SINCE(->test2)}次

    • test2 Option A
      test2分支 距离上次test2,玩家选择了{TURNS_SINCE(->test2)}次
      ->test1

其实就是TURNS() 每次进入knot时 计数清零

TURNS_SINCE封装到函数

TURNS_SINCE(->x) == 0 可以用来判断玩家是不是刚刚看过 x这个内容

因为很常用 可以把他封装到函数中

函数应该放在文件的最后,

参数有两种类型,一种是值类型,一种是divert target

复制代码
=== function came_from(-> x)
	~ return TURNS_SINCE(x) == 0

判断玩家是不是刚刚看过某内容

复制代码
* {came_from(->  nice_welcome)} 'I'm happy to be here!'
* {came_from(->  nasty_welcome)} 'Let's keep this quick.'
SEED_RANDOM()

设置随机数种子, 可以控制随机数生成的序列

种子一样,生成的序列一样,方便测试

复制代码
~ SEED_RANDOM(235) //设置随机数种子

影响 Shuffle和随机数生成

复制代码
He told me a joke. {~I laughed.|I smiled.|I grimaced.}
~ roll = RANDOM(1, 6)

二、交织(Weave)

在前面我们学会了一种编写故事的方式:

复制代码
Knot/Stitch+divert 跳转的方式编写故事.

但是这要求每一个节点都要有唯一名称,实现小分支也比较麻烦.

为了方便顺序地编写故事,inky提供被称为weave的故事编写方式:

复制代码
选项(`*`) 与 gather(`-`) 的结合来顺序编写故事

1) 聚集(Gathers)

聚集可以让选择分支结束之后,让故事回到主线

只需要在选择支后面 加上 - 标记

复制代码
"What's that?" my master asked.
	*	"I am somewhat tired[."]," I repeated.
		"Really," he responded. "How deleterious."
	*	"Nothing, Monsieur!"[] I replied.
		"Very good, then."
	*  "I said, this journey is appalling[."] and I want no more of it."
	"Ah," he replied, not unkindly. "I see you are feeling frustrated.    Tomorrow, things will improve."

-	With that Monsieur Fogg left the room.

这样选择项结束之后 就会继续主线,不需要新建knot,也不需要使用跳转

2) 嵌套流程

weave提供了一种紧凑的方式,让你可以实现大量分支和选择,同时保证故事从开头顺利推进到结尾!weave可以减少跳转跳转次数,使故事不容易出错,可以更方便浏览和修改。

嵌套选项与聚集

一个选项下面用 ** 可以创建子选项,子选项结束之后也可以重新汇聚到主线

复制代码
- 	"Well, Poirot? Murder or suicide?"
	*	"Murder!"
	 	"And who did it?"
		** 	"Detective-Inspector Japp!"
		** 	"Captain Hastings!"
		** 	"Myself!"
	* 	"Suicide!"
-	Mrs. Christie lowered her manuscript a moment. The rest of the writing group sat, open-mouthed.

++ 也可以创建子选项,要注意适当缩进让结构更直观

嵌套聚集

选项剧情可以更丰富, 子选项的情节结束之后, 也项回到该选项的情节中

使用 -- 让子选项分支结束之后重新汇聚到选项

复制代码
- 	"Well, Poirot? Murder or suicide?"
    *	"Murder!"
     	"And who did it?"
    	* * 	"Detective-Inspector Japp!"
    	* * 	"Captain Hastings!"
    	* * 	"Myself!"
    	- - 	"You must be joking!"  //汇聚到此处
    	* * 	"Mon ami, I am deadly serious."
    	* *		"If only..."
    * 	"Suicide!"
    	"Really, Poirot? Are you quite sure?"
    	* * 	"Quite sure."
    	* *		"It is perfectly obvious."
-	Mrs. Christie lowered her manuscript a moment. The rest of the writing group sat, open-mouthed.
聚集是怎么工作的?

在选项结束之后,故事回找到下一个同级或更高层级的 gather 并跳转过去.

基本思想如下:选项分开故事路径,gather 将它们重新汇聚在一起 .

(因此得名 "weave",即"交织"!)

嵌套没有深度限制

上面,我们仅使用了两层嵌套,但是实际上嵌套没有层数限制

复制代码
-	"Tell us a tale, Captain!"
	*	"Very well, you sea-dogs. Here's a tale..."
		* * 	"It was a dark and stormy night..."
				* * * 	"...and the crew were restless..."
						* * * *  "... and they said to their Captain..."
								* * * * *	"...Tell us a tale Captain!"
	*	"No, it's past your bed-time."

但是这样会让分支变得复杂且难以阅读

当分支过于复杂时 还是使用 knot/stitch + divert更合适

Gather与Chioce的层级影响

与choice同一级的gather 会将选项分开

下例:子分支会产生两次选择,每次一个选项,先出现Opt1,选择之后出现Opt2

复制代码
 "要一起回家吗?"
+ [什么也不说] 
    ++ [Opt1] 
    -- 她歪着头,有些疑惑:
    ++ [Opt2] 

如果不想分开,要让gather提升一个层级

下例:子分支只会产生一次选择,有两个选项,出现Opt1和Opt2供玩家选择

复制代码
 "要一起回家吗?"
+ [什么也不说] 
    ++ [Opt1] 
    	--- 她歪着头,有些疑惑:
    ++ [Opt2] 

3) 追踪Weave

采用weave的方式来编写故事,由于gather和choice没有地址,

因此无法对它们进行像是knot/stitch那样的 跳转和条件测试

无法了解到玩家之前是不是看过某个gather/选过某个选项,也没法跳转到那个位置

可以通过 (label_name) 语法在需要的位置添加标签。

标记Gather

gather 开头用(label_name)标记

就可以进行跳转和条件测试了,就像knot/stitch那样

普通文本也可以在前面加 - 变成gather,从而被标记

复制代码
今天放学后,你在校门口看到了熟悉的身影。
她站在夕阳下,抱着书,看起来有点犹豫。
"啊,是你。"  
- (invite_go_home) "要一起回家吗?"
+ [什么也不说] 
	她歪着头,有些疑惑:->invite_go_home
+ [找借口拒绝]
    "不要。"
	-- (girl_sad)她低下头,语气明显冷了几分:->invite_go_home
+ {girl_sad} [一起回家]
	你点了点头。
	"太好了。"  
	她露出了松了一口气的笑容。
- ->END
标记Choice

选项也可以被标记, 选项开头用(label_name)标记

就可以进行跳转和条件测试了,就像knot/stitch那样

选项作为目标时,可以直接到达选项被选择后的输出,

就像玩家选择之后一样(选项文本也可以输出)

并且只出现一次的选项(*标记),也可以直接输出,即使已经选过了

可以用条件测试,来判断玩家之前有没有选过某选项

复制代码
今天放学后,你在校门口看到了熟悉的身影。
她站在夕阳下,抱着书,看起来有点犹豫。
"啊,是你。"  
- (invite_go_home) "要一起回家吗?"
* [什么也不说] 
	她歪着头,有些疑惑:->invite_go_home
* (make_refuse) 找借口[拒绝]
    "不要。"
	-- 她低下头,语气明显冷了几分:->invite_go_home
* {make_refuse} [一起回家]
	你点了点头。
	"太好了。"  
	她露出了松了一口气的笑容。
- ->make_refuse
作用域

同一作用域下,可以直接使用标签的名字

复制代码
=== knot ===
= stitch_one
	- (gatherpoint) Some content.
= stitch_two
	*	{stitch_one.gatherpoint} Option

不同作用域下,需要指明明确的地址,作法与knot/stitch一样

复制代码
=== knot_one ===
-	(gather_one)
	* {knot_two.stitch_two.gather_two} Option

=== knot_two ===
= stitch_two
	- (gather_two)
		*	{knot_one.gather_one} Option
创建循环结构

通过标签来实现循环结构

复制代码
今天放学后,你在校门口看到了熟悉的身影。
她站在夕阳下,抱着书,看起来有点犹豫。
"啊,是你。"  
- (invite_go_home) "要一起回家吗?"
+ [什么也不说] 
	她歪着头,有些疑惑:->invite_go_home
+  [找借口拒绝]
    "不要。"
	-- 她低下头,语气明显冷了几分:->invite_go_home
+ [一起回家]
	你点了点头。
	"太好了。"  
	她露出了松了一口气的笑容。
- {->invite_go_home|->invite_go_home|}  //sequences 最后再循环两次
将文本提升成gather

可以将文本提升为gather,这样可以方便跳转与条件测试

不过要注意使用合适层级,防止扰乱选项

复制代码
今天放学后,你在校门口看到了熟悉的身影。
她站在夕阳下,抱着书,看起来有点犹豫。
"啊,是你。"  
- (invite_go_home) "要一起回家吗?"
+ [什么也不说] 
	她歪着头,有些疑惑:->girl_sad
+  [找借口拒绝]
    "不要。"
	-- (girl_sad)她低下头,语气明显冷了几分:->invite_go_home
+ [一起回家]
	你点了点头。
	"太好了。"  
	她露出了松了一口气的笑容。

三、变量和逻辑

到目前为止,我们一直是在基于玩家迄今为止已经看过的内容 来进行判断,从而实现条件文本条件选项

Ink 还支持变量 ,包括临时变量全局变量 ,这些变量可以存储数值数据内容数据 ,甚至故事流程指令

在逻辑能力方面,Ink 是功能完备的,并且还提供了一些额外的结构,用来帮助更好地组织分支故事中往往会变得相当复杂的逻辑。

1) 全局变量

全局变量是最常用的一类变量,用来存储游戏状态

它可以表示任何东西:从主角口袋里有多少钱,到一个用来反映主角心理状态的数值。

它可以在故事的任何地方被访问------既可以被设置,也可以被读取。

定义全局变量

全局变量可以在任何地方通过 VAR 语句来定义。

它们应当被赋予一个初始值,这个初始值同时也决定了变量的类型---布尔值、整数、浮点数(小数)、内容,或者一个故事地址。

复制代码
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 ===
		The train jolted and rattled. { mood > 0:I was feeling positive enough, however, and did not mind the odd bump|It was more than I could bear}.
		*	{ not knows_about_wager } 'But, Monsieur, why are we travelling?'[] I asked.
		* 	{ knows_about_wager} I contemplated our strange adventure[]. Would it be possible?
存储Diverts的变量

地址也是一种类型,可以存储,更改,并且用于跳转

复制代码
VAR Var_Walk_Home = ->after_school.walk_home
->Var_Walk_Home
=== after_school ===
"要一起回家吗?"
= walk_home
你点了点头。
"太好了。"  
她露出了松了一口气的笑容。
->END
全局变量在外部是可见的

全局变量不仅可以在脚本内部使用,还可以在外部使用,在运行时改变

可以将变量集中到一个文件中,作为游戏的状态

也方便游戏状态的保存和加载

输出变量

在文本中用 { } 标记,可以输出变量内容

与条件判断的语法一样,因此应该避开作为条件判断的场景

复制代码
VAR Var_Walk_Home = ->after_school.walk_home
VAR Age = 18
VAR IsDog = false
VAR players_name = "Windcaty"

今天放学后,你在校门口看到了熟悉的身影。
她站在夕阳下,抱着书,看起来有点犹豫。
{Var_Walk_Home}
{Age}
{IsDog}
{players_name}
"啊,是你。"  

调试时很有帮助

字符串的赋值

变量本身只能存储字符串类型,也就是固定的字符

但是可以用inky的内容进行赋值

复制代码
//包含inky语法的内容不能用于初始化
VAR a_colour = "{~red|blue|green|yellow}"

//但是可以用于赋值
VAR a_colour = ""
~ a_colour = "{~red|blue|green|yellow}"
{a_colour}

不过变量的值,在被赋值的时候就计算并固定下来了,并不能动态改变

复制代码
VAR a_colour = ""
~ a_colour = "{~red|blue|green|yellow}"
输出:{a_colour}
输出:{a_colour}
//输出结果相同,不会产生变化
//故事重新启动时,会重新计算

2) 逻辑

显然,全局变量不是常量,inky提供语法改变它们

为了防止ink把 变量名当作文本输出,

~在前面标记,表示要进行运算

复制代码
=== set_some_variables ===
~ knows_about_wager = true
~ x = (x * x) - (y * y) + c
~ y = 2 * x * y

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

变量的类型是动态的,意味着可以赋予不同类型的值

数学运算

支持(+, -, * ,/,% (mod), POW(a,b)运算

加减乘除,取模,求幂运算

复制代码
{POW(3, 2)} is 9.
{POW(16, 0.5)} is 4.

更复杂的运算,可以编写函数实现

如果有必要 还可以调用外部函数,甚至是UE5编写的函数

生成随机数 RANDOM(min, max)

:
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         //结果是0
~ y = 7 / 3         //结果是2
~ z = 1.2 / 0.5     //结果是2.4
高级 INT(), FLOOR() and FLOAT()

在某些情况下,如果你不想使用隐式类型

或者想要对变量进行四舍五入,可以直接进行类型转换。

复制代码
{INT(3.2)} is 3.
{FLOOR(4.8)} is 4.
{INT(-4.8)} is -4.
{FLOOR(-4.8)} is -5.
{FLOAT(4)} is, um, still 4.

字符串比较

Inky虽然作为文本引擎,但是其对于字符串的处理十分有限

它假定所有的字符串转换需求都由游戏代码或外部函数来处理

ink 对字符串提供了三种基本查询------相等、非相等和子字符串

复制代码
{ "Yes, please." == "Yes, please." }
{ "No, thank you." != "Yes, please." }
{ "Yes, please" ? "ease" }

3) 条件语句(if/else)

我们之前看到了条件语句用于控制选项和故事内容

inky也提供了类似于常规 if/else-if/else 结构的功能。

条件不必基于变量,也可以是阅读计数

这种语法的优点是易于扩展,并且可以方便地设置优先级。

简单的if

if 语句格式

复制代码
{ x > 0:
	~ y = x - 1
}

if/else 语句格式

复制代码
{ x > 0:
	~ y = x - 1
- else:
	~ y = x + 1
}

if/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
}
条件语句不只能用于逻辑运算

还可以控制输出的内容,甚至可以包含选项

但是为了防止混淆, 条件语句中不允许出现gather点

选项中必须要使用divert跳转离开

复制代码
* 快速回家
    你快速地回到了家
* (meet_girl) 慢慢回家
- 
{meet_girl:
     "啊,是你。" 
     "要一起回家吗?"
    + [什么也不说] 
        -> after_school.no_say
    + [找借口拒绝]
         "不要。"
        -> after_school.make_refuse
    + [一起回家]
    	-> after_school.walk_home
 -else:
     你在路上什么人都没遇到
}
Alternative的多行型式

用多行的型式 可以实现Alternative一样的效果

复制代码
// 序列(Sequence):按顺序显示每个选项,最后一个保持显示
{ stopping:
- 我进入了赌场。
- 我再次进入赌场。
- 我又一次走了进去。
}

// 随机(Shuffle):随机显示一个
在桌上,我抽了一张牌。<>
{ shuffle:
	- 红心 A。
	- 黑桃 K。
	- 方块 2。
		"你这次输了!"荷官喊道。
}

// 循环(Cycle):依次显示每个选项,然后循环
{ cycle:
	- 我屏住呼吸。
	- 我焦躁地等待。
	- 我停顿了一下。
}

// 一次(Once):每个选项显示一次,直到全部显示完
{ once:
	- 我的运气会持续吗?
	- 我能赢这一手吗?
}
修改的Shuffle
复制代码
// shuffle once 随机输出一个,显示过的将不再出现,最后什么都不输出
//无放回的随机摸小球
{ shuffle once:
- 太阳很烈。
- 天气很热。
}

//shuffle stopping 
//会随机输出一个(除了最后一条),,显示过的将不再出现,最后一直输出最后一条
{ shuffle stopping:
- 一辆银色 BMW 呼啸而过。
- 一辆亮黄色 Mustang 转过了弯。
- 这里好像有好多车。
}

4) 临时变量

临时变量用于临时计算

全局变量有时使用起来不方便。ink提供了临时变量,用于快速计算一些值。

复制代码
=== 摇色子 ===
开始摇色子 
+ [摇]
->开始
= 开始
~ temp number = RANDOM(1,6)
摇到了 {number} 点
->摇色子

临时变量在不再定义它的 stitch 中 就访问不到

Knots 和 stitches 可以接受参数

一种特别有用的临时变量形式是参数。任何 knot 或 stitch 都可以被赋予参数值。

复制代码
*	[指控 Hastings]
	-> accuse("Hastings")
*	[指控 Mrs Black]
	-> accuse("Mrs Black")
*	[指控自己]
	-> accuse("自己")

=== accuse(who) ===
	"我指控 {who}!" 波洛宣布。
	"真的吗?" 贾普回应。"{who == "自己":是你做的吧?|{who}?}"
	"那为什么不是呢?" 波洛反问道。

如果你想把临时值从一个 stitch 传递到另一个 stitch,就必须使用参数。

示例:递归 knot

临时变量在递归中使用是很安全的

复制代码
-> 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) ===
	"The result is {total}!" you announce.
	Gauss stares at you in horror.
	-> END

这种定义方式很有用,因此 ink 提供了一种特殊类型的 knot,称为 function,它有一些限制,并且可以返回值。

高级用法:将divert目标作为参数传递

Knot/stitch 的地址是一种值,用 -> 表示,并且可以被存储或传递。

这里的-> 表示的是一个类型,表示故事地址

用地址作为参数很有用

复制代码
=== sleeping_in_hut ===
    你躺下闭上眼睛。
    -> generic_sleep (-> waking_in_the_hut)

=== generic_sleep (-> waking) ===
    你沉沉入睡,或许会做些梦等等。
    -> waking

=== waking_in_the_hut ===
    你重新站起身,准备继续你的旅程。

在inky中,地址作为参数是唯一需要明确指定参数类型的

因为节点名 不仅可以作为地址,而且可以表示节点被阅读的次数

不显式指定,很有可能导致错误

5) 函数(Functions)

在 Knot 上使用参数意味着它们几乎就像常规意义上的函数,

但它们缺少一个关键概念------调用栈以及返回值的使用。

复制代码
Ink 引入了函数(functions):它们本质上是 Knot,但具有以下限制与特性:
一个函数:
	不能包含 stitches
	不能使用 divert 或提供选择
	可以调用其他函数
	可以包含打印内容
	可以返回任意类型的值
	可以安全地递归
返回值通过 `~ 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.'

和任何其他语言一样,函数在执行完成后,会将流程返回到调用它的地方------

函数不允许通过 divert 来改变流程,它们可以调用其他函数。

复制代码
=== function say_no_to_nothing === 	
	~ return say_yes_to_everything()
函数不是一定要有返回值

函数并不一定需要有返回值,也可以只是执行一些值得封装起来的操作:

复制代码
=== function harm(x) === 	
{ stamina < x: 		
	~ stamina = 0 	
- else: 		
	- ~ stamina = stamina - x 	
}

......不过请记住,函数不能进行 divert,因此上面的代码虽然可以防止 Stamina 变成负数,但并不会在玩家的 Stamina 归零时"杀死"玩家。

函数可以在内容中间调用(内联调用)

函数既可以在以 ~ 开头的内容行中调用,也可以在一段普通文本内容的中间被调用。

函数会首先打印 函数内部输出的内容,然后打印返回值,如果没有返回值,则不打印

内容在默认情况下是"粘连(glued)"的,因此下面的写法:

复制代码
Monsieur Fogg was looking {describe_health(100)}.

=== function describe_health(x) ===
{ 
- x == 100:
	~ return "spritely" 
- x > 75: 	
	~ return "chipper"
- x > 45: 	
    ~ return "somewhat flagging" 
- else: 	
	~ return "despondent" 
}

会生成如下结果:

Monsieur Fogg was looking despondent.

示例:数值运算
复制代码
=== 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)
	}
The maximum of 2^5 and 3^3 is {max(exp(2,5), exp(3,3))}.
示例:数字转文字

传入数字 转成文字

复制代码
=== function print_num(x) ===
{
  - x >= 10000:
        {print_num(x/10000)}萬{x%10000>0:{x%10000<1000:零}{print_num(x%10000)}}
    - x >= 1000:
        {print_num(x/1000)}仟{x%1000>0:{x%1000<100:零}{print_num(x%1000)}}
    - x >= 100:
        {print_num(x/100)}佰{x%100>0:{x%100<10:零}{print_num(x%100)}}
    - x >= 10:
        {print_num(x/10)}拾{x%10>0:{print_num(x%10)}}
    - x >= 0:
        {print_single_number(x)}
}

=== function print_single_number(num) ===
{num:
    - 0:零
    - 1:壹
    - 2:贰
    - 3:叁
    - 4:肆
    - 5:肆
    - 6:陆
    - 7:柒
    - 8:捌
    - 9:玖
}
参数可以通过引用传递

函数参数也可以"通过引用"传递

当变量作为引用传入时,函数可以修改传入的变量

而不是拷贝的临时变量

写法如下:

复制代码
=== 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, 1)} 
- <> 然后我们继续上路。

将简单操作封装在函数中,用于调试信息十分便捷

6) 常量(Constants)

全局常量

常量顾名思义,赋值之后就不能改变

可以用来表示一些固定的值

复制代码
CONST PI = 3.14
CONST VALUE_OF_TEN_POUND_NOTE = 10
常量模拟状态

但是,ink的常量与其他编程语言的有一些不同之处

感觉这里的常量不是"值不能改变的变量", 而是"一个固定量的另一种写法"

常量可以给变量赋值,来模拟不同的状态

复制代码
CONST HASTINGS = "Hastings"
CONST POIROT = "Poirot"
CONST JAPP = "Japp"

VAR current_chief_suspect = HASTINGS

=== review_evidence ===
	{ found_japps_bloodied_glove:
		~ current_chief_suspect = POIROT
	}
	Current Suspect: {current_chief_suspect}
常量模拟枚举

常量这样用,很像枚举

复制代码
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:
	The secret agent grabs the suitcase!
	~ suitcase_location = HELD_BY_AGENT

-  secret_agent_location < suitcase_location:
	The secret agent moves forward.
	~ secret_agent_location++
}

这句话总结的很好:常量只是为故事状态提供了一种易于理解的名称方式

7) 游戏端逻辑

游戏引擎结合使用,主要两种方法提供游戏钩子

一是 外部函数声明 ,允许你在 ink 中直接调用游戏中的 C# 函数;

二是 变量观察器,当 ink 变量被修改时,会在游戏端触发回调。

四、高级流程控制

1) 隧道(Tunnels)

到目前为止,故事还是以以一种平坦的方式编写的,故事产生分支然后再汇合,

可能会发生循环,但是故事总是发生在特定的位置

这种结构让一些事变得复杂,比如一段故事可以发生在不同的位置,

用之前的语法,我们需要为不同的位置复制多份文本,但是如果我们不想复制多份呢?

使用Knot的参数
复制代码
=== 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

每次调用的时候传入,目标knot地址,可以实现故事执行之后跳转到目标

但是这种方式让故事编写更加复杂,如果一段故事要多个knot参与,就需要为每个knot传参

所以ink提供了一种特殊的divert,称为tunnel

使用tunnel运行子故事
复制代码
->outside_pitcairn_island->

=== crossing_the_date_line ===
*	"先生!"[]我突然惊恐地喊道,"我刚刚意识到------我们已经越过了国际日期变更线!"
-	福格先生几乎没有抬起眉毛。"我已经作过调整了。"
*	我擦了擦额头上的汗水[]. 总算松了一口气!
*  我点了点头,心境平静了下来[]. 他当然早就考虑到了!
*  我低声咒骂[]. 又一次,我被他轻视了!
-	->END

=== outside_honolulu ===
我们抵达了檀香山所在的大岛。
- (postscript)
	->->


=== outside_pitcairn_island ===
小船沿着水面航行,朝着那座微小的岛屿前进。
- (postscript)
	->->
tunnel调用方式

在knot后面再接一个箭头 ->

复制代码
->outside_pitcairn_island->
tunnel节点实现方式

在knot结尾用双箭头标识 ->->

复制代码
=== outside_honolulu ===
我们抵达了檀香山所在的大岛。
- (postscript)
	->->

ink在运行时检查tunnel是否以 ->-> 结尾,而不是编译时,所以你需要小心编写

tunnel可以连写

tunnel后可以接普通的跳转

复制代码
->outside_pitcairn_island->crossing_the_date_line
->outside_pitcairn_island->END

tunnel后可以接tunnel

复制代码
->outside_pitcairn_island->outside_honolulu->
tunnel可以嵌套

knot内部也可以调用tunnel

复制代码
=== plains ===
= night_time
	黑暗的草地在你脚下显得柔软。
	+	[睡觉]
		-> sleep_here -> wake_here -> day_time
= day_time
	是时候继续前进了。

=== wake_here ===
	当太阳升起时,你醒了过来。
	+	[吃点东西]
		-> eat_something ->
	+	[采取行动]
	-	->->

=== sleep_here ===
	你躺下来,试着闭上眼睛。
	-> monster_attacks ->
	然后,终于可以睡觉了。
	-> dream ->
	->->
Tunnel可以返回其他地方

Tunnel不保证一定会返回原地执行

它允许你从一个Tunnel返回,但实际上却前往另一个地方

一般用在Tunnel调用结尾,你需要小心使用,避免调用混乱

复制代码
VAR stamina = 15
->fall_down_cliff->fall_down_cliff->fall_down_cliff

=== fall_down_cliff 
-> hurt(5) -> 
你还活着!你扶起自己,继续前行。
->->

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

=== youre_dead
突然,你周围被一片白光笼罩,你输了。
->END

在某些情况下,我们也希望把句子拆分开来

复制代码
-> talk_to_jim ->
=== talk_to_jim
 - (opts) 	
	*	[询问曲速整流罩的情况] 
		-> warp_lacells ->
	*	[询问护盾发生器的情况] 
		-> shield_generators ->	
	* 	[停止交谈]
		->->
 - -> opts 

= warp_lacells
	{ shield_generators : ->-> argue }
	"别担心曲速整流罩,它们没问题。"
	->->

= shield_generators
	{ warp_lacells : ->-> argue }
	"别管护盾发生器,它们运行得很好。"
	->->

= argue 
 	"你怎么问这么多问题?"吉姆突然质问道。 
 	...
 	->->
Tunnels 使用调用栈

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

2) Threads

Threads 将多个Knot第一个选择支之前的内容拼接起来,玩家选择之后,进入各自Knot

它不是计算机科学中的线程的意思,而是故事拼接

这是一个高级功能,使用它会使故事的结构稍微复杂一些

Thread将多个区域结合在一起
复制代码
== thread_example
我头疼得厉害;线程(threading)真的很难理解。
<- conversation
<- walking

== conversation ==
那对 Monty 和我来说是一个紧张的时刻。
 * "你今天午饭吃了什么?"[] 我问道。
    "午餐肉和鸡蛋," 他回答。
 * "今天天气不错啊,"[] 我说。
    "我见过更好的," 他回答。
 - -> house

== walking ==
我们继续沿着尘土飞扬的路走下去。
 * [继续走]
    -> house

== house ==
不久,我们就到了他家。 
{conversation} {walking}
-> END

将多个Knot第一个选择支之前的内容拼接起来,玩家选择之后,进入各自Knot

每个Knot的阅读计数也会加1

复制代码
CONST HALLWAY = 1
CONST OFFICE = 2

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

->run_player_location

== run_player_location
	{
		- player_location == HALLWAY: -> hallway
	}

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

= examine_drawers
...

= examine_wardrobe
...

= go_office
...

// 下面是Thread
== characters_present(room)
	{ generals_location == room:
		<- general_conversation
	}
	{ doctors_location == room:
		<- doctor_conversation
	}
	-> DONE

== general_conversation
	*	[问将军关于血淋淋的刀] 
		"这是件糟糕的事情,我可以告诉你。"
	-	-> run_player_location

== doctor_conversation
	*	[问医生关于血淋淋的刀] 
		"血有什么奇怪的吗,不是吗?"
	-	-> run_player_location
显示标记Thread结尾

需要使用 ->DONE 来显示标记Thread拼接的结尾,否则编译器会认为你的故事没写完

Thread拼接的Knot内部与正常编写故事一样,可以用Divert进行跳转到别的位置

复制代码
->thread_example
== thread_example
我头疼得厉害;线程(threading)真的很难理解。
<- conversation
<- walking
->DONE  //显示标记结尾  用->END会结束整个故事 也不会显示选项了


== conversation ==
那对 Monty 和我来说是一个紧张的时刻。
 * "你今天午饭吃了什么?"[] 我问道。
    "午餐肉和鸡蛋," 他回答。
 * "今天天气不错啊,"[] 我说。
    "我见过更好的," 他回答。
- ->house


== walking ==
我们继续沿着尘土飞扬的路走下去。
 *[继续走]
- ->house

== house ==
不久,我们就到了他家。 
{conversation} {walking}
->END
示例:在多个地方添加相同的选项

Thread可以用来在很多不同的地方添加相同的选项。

使用这种方法时,通常会传入一个跳转(divert)作为参数,告诉故事在选项完成后应该去哪。

复制代码
=== 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

Tunnel会运行同一段内容,但是它不会改变选项

这两段内容效果可能一样:

复制代码
<- childhood_memories(-> next)
*	[望向窗外]
 	我在一路上做白日梦...
- (next) 然后哨声响了...

*	[回忆我的童年]
	-> think_back ->
*	[望向窗外]
	我在一路上做白日梦...
- 	(next) 然后哨声响了...

但是,一旦被Thread加入的选项包含多个选择 ,或者对选择有条件判断逻辑(当然还有文本内容),Thread版本就更实用了。

示例:将选项分开

对于一个大的Knot,可以用Thread将选项分开管理

复制代码
=== the_kitchen
- (top)
<- drawers(-> top)
<- cupboards(-> top)
<- room_exits
->DONE

= drawers (-> goback)
	// 关于抽屉的选择...
	...

= cupboards(-> goback)
	// 关于橱柜的选择...
	...

= room_exits
	// 出口;不需要"返回点",因为离开就是去别的地方
	...

五、高级状态追踪

一个游戏当然会有各种状态,比如电灯有开关的状态。

Ink使用List来追踪物体的状态,List可以看作一个存储状态的表,或者状态的集合。

Ink不像解析器式互动小说(IF)创作语言,它没有对象(objects)、包含关系(containment)的概念。它用List灵活地追踪状态的变化。

1)初识List

使用 LIST 关键字来定义一个表。

复制代码
LIST kettleState = cold, boiling, recently_boiled

这一行做了三件事:

第一,定义了三个表项 ------coldboilingrecently_boiled

第二,定义了一张表 ------ kettleState表有三个表项

第三,定义了一个变量 ------ kettleState可以存储多个表项(变量名与表名相同)
:
kettleState = cold

我们也可以改变它的值:

复制代码
* [打开水壶]
The kettle begins to bubble and boil.
~ kettleState = boiling

我们还可以查询它的值:

复制代码
*	[触摸水壶]
{ kettleState == cold:
	The kettle is cool to the touch.
- else:
 	The outside of the kettle is very warm!
}

为了方便起见,我们可以在定义表时就给变量一个初始值,使用括号表示:

复制代码
LIST kettleState = cold, (boiling), recently_boiled
// 在游戏开始时,这个水壶是开着的。

2) Ink如何实现List

为了更好地理解List的概念,我阅读了ink源码,现在解释几个概念

表项(InkListItem)
复制代码
LIST kettleState = cold, boiling, recently_boiled

这段代码创建了这三个表项:coldboilingrecently_boled

一个InkListItem的主体由两个字符串组成originNameitemName
originName 为其所在的表名,itemName为表项自身名,

表项还有一个全名, 用这种方式表示:originName.itemName

如表项 cold,它的originName为 "kettleState",它的itemName为"cold",它的全名为"kettleState.cold"

注: 一般用表项表示状态,所以后续可能使用状态的说法来代替表项

表(InkListDefinition)

一个表包含表名和所有的表项,每个表项可以有一个值与之对应

复制代码
LIST kettleState = cold, boiling, recently_boiled

这段代码还创建了一张表,

InkListItem int
cold 1
boiling 2
recently_boiled 3
表名为 : kettleState

注: 这种包含所有表项的结构以后统称为

变量(InkList)

变量可以存储若干表项,它为InkList类型,后续用VAR创建与表相关的变量都是这个类型,

复制代码
LIST kettleState = cold, boiling, recently_boiled

这段代码还创建了一个变量kettleState,变量不仅可以存储kettleState表的表项,还可以存储其他表的表项.

复制代码
LIST kettleState = cold, boiling, recently_boiled
LIST daysOfTheWeek = Monday, Tuesday, Wednesday, Thursday, Friday
//变量一开始是空的,需要进行赋值
~kettleState = (cold,boiling,Monday,Wednesday)

由于它可以存储多个表的表项,因此还需要存储表项来自哪些表

总之,InkList存储若干表项与对应的值,并且存储表项来自哪些表,与这些表的表名

kettleState现在的结构如下:

InkListItem int
cold 1
boiling 2
Monday 1
Wednesday 3
表项来自哪些表: kettleState表,daysOfTheWeek表
表项来自表的表名:"kettleState","daysOfTheWeek"

注: 这种存储表项的结构,之后会用 变量列表列表变量来表示

3) 不同变量可以存储相同状态

不同的变量可以使用相同的表项

如下示例,水壶,锅,微波炉,都可以使用cold, boiling, recently_boiled这三个状态

复制代码
LIST heatedWaterStates = cold, boiling, recently_boiled
VAR kettleState = cold
VAR potState = cold
VAR microwaveState = cold

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

=== do_cooking
<- cook_with("水壶", kettleState)
<- cook_with("锅", potState)
<- cook_with("微波炉", microwaveState)
- (done)
->DONE
不同表的表项名也可以相同
复制代码
LIST colours = red, green, blue, purple
LIST moods = mad, happy, blue

不同表的表项名也可以相同,但是直接使用可能引起歧义

复制代码
VAR status = blue   //这是颜色的blue还是心情的blue?

可以使用全名(full name),或者叫做家族名(family name)

复制代码
VAR status = colours.blue
LIST创建的变量
复制代码
LIST kettleState = cold, boiling, recently_boiled

这段话创建的kettleState变量可以改变类型,并不是专门的InkList类型变量

与用 VAR 创建的变量没有区别

复制代码
LIST kettleState = cold, boiling, recently_boiled
~ kettleState = 3.1415    

这时kettleState变量存储的是浮点数,不建议这样做,因为会让人产生混乱

但是它不妨碍这下面的代码依然正确

复制代码
~ temp anotherkettleState = kettleState.boiling
 //因为这时的kettleState指的是表名,而不是kettleState这个变量

4) 状态的值

每个状态对应一个数字,就是它的值

在定义表时,状态的值从1开始,往右依次增加1

复制代码
LIST kettleState = cold, boiling, recently_boiled
LIST kettleState = cold = 1, boiling = 2, recently_boiled = 3

状态可以当作数字来使用

复制代码
LIST volumeLevel = off, quiet, medium, loud, deafening
VAR lecturersVolume = medium
VAR murmurersVolume = quiet
{ volumeLevel.quiet < volumeLevel.deafening:
	The murmuring gets louder1
}
{ lecturersVolume > murmurersVolume:
	The murmuring gets louder2
}

可以使用{ ... }输出状态的名字

复制代码
LIST volumeLevel = off, quiet, medium, loud, deafening
VAR lecturersVolume = medium
{volumeLevel.quiet} //输出quiet
{lecturersVolume}  //输出medium

使用LIST_VALUE获取状态的值

复制代码
LIST heatedWaterStates = cold, boiling, recently_boiled
VAR kettleState = cold
VAR potState = recently_boiled
{LIST_VALUE(kettleState)}                          //1
{LIST_VALUE(heatedWaterStates.boiling)}            //2
{LIST_VALUE(potState) - LIST_VALUE(kettleState)}   //2

使用表名(状态值) 来获取状态

复制代码
LIST heatedWaterStates = cold, boiling, recently_boiled
VAR kettleState = cold
~kettleState = heatedWaterStates(2)    //boiling

可以自定义状态对应的数值

复制代码
LIST heatedWaterStates = cold = 2, boiling = 6, recently_boiled = 8

如果为某个值指定了数值,但没有为下一个值指定数值,ink 会根据前一个值递增 1

复制代码
LIST heatedWaterStates = cold = 2, boiling, recently_boiled = 8 
//boiling值为3

5) 多值列表

给变量赋予多个值(初始化时)

前面已经提到, 列表变量并不是只能存储一个项

可能一个也不包含:

复制代码
LIST DoctorsInSurgery = Adams, Bernard, Cartwright, Denver, Eamonn

可能所有都包含:

复制代码
LIST DoctorsInSurgery = (Adams), (Bernard), (Cartwright), (Denver), (Eamonn0

可能包含几个:

复制代码
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" 或 "Cartwright"
变量是否为空

可以直接对列表变量进行条件查询,如果不为空则返回true,为空则返回false

复制代码
{ DoctorsInSurgery: 今天诊所开放。 | 所有人都已经回家了。 }
测试是否相等

使用 == 运算符来判断是否相等

对于有多个值的列表变量,要求两个变量包含的条目一致,顺序可以不同

复制代码
{ DoctorsInSurgery == (Adams, Bernard):
	Adams 医生和 Bernard 医生正在角落里激烈争论。
}

使用 != 运算符来判断是否不相等

复制代码
{ DoctorsInSurgery != (Adams, Bernard):
	 Adams 和 Bernard 没有在争论。
}

另外还有其他运算方式 如++ and --; 另有<, <=, >>=;

测试是否包含

使用一个运算符 has 来测试是否包含,也可以写作 ?

复制代码
{ DoctorsInSurgery ? (Adams, Bernard):
	Adams 医生和 Bernard 医生正在角落里低声争论。
}

{ DoctorsInSurgery has Eamonn:             // ? 同样也可以用于单个值
	Eamonn 医生正在擦拭他的眼镜。
}

可以对其取反,使用 hasnt!?(即 not ?)。

需要注意的是,这里开始变得有些容易混淆,因为:

复制代码
DoctorsInSurgery !? (Adams, Bernard)

并不表示 Adams 和 Bernard 都不在场,而只是表示他们并非同时在场。

注意:变量不会包含空列表

下面这个示例:

复制代码
SomeList ? ()

无论 SomeList 本身是否为空,都会始终返回 false。

在实际使用中,这是一个最有用的默认行为,因为你经常会希望像下面这样的测试:

复制代码
SilverWeapons ? best_weapon_to_use

在玩家手中什么都没有的情况下能够失败。

获取完整的表

可以使用表项或者列表变量来获取对应表的所有项

复制代码
LIST_ALL(表项)             //获得所在表的所有项
LIST_ALL(列表变量)         //获得变量存储的表项 所在表的所有项

LIST ValueList = first_value, second_value, third_value
VAR myList = (first_value)
{ LIST_ALL(first_value) }
{ LIST_ALL(myList)}
刷新列表变量的类型

可以创建空的列表变量,并使用 表名()的方式来设置其应该存储哪个表的表项

复制代码
LIST ValueList = first_value, second_value, third_value
VAR myList = ()
~ myList = ValueList()

之后就可以这样使用:

复制代码
{ LIST_ALL(myList) }
获取列表变量的一部分

使用 LIST_RANGE 函数来获取列表变量的一部分,或者说切片

复制代码
LIST_RANGE(列表变量, 最小值, 最大值)   //结果包含最小值和最大值

LIST ValueList = (first_value), (second_value), (third_value)
{ LIST_RANGE(ValueList,2,3) }
示例:用于游戏标志(game flags)

多值列表最简单的用途,就是用来整洁地追踪"游戏标志(game flags)"。

复制代码
LIST Facts = (Fogg_is_fairly_odd), first_name_phileas, (Fogg_is_English)

{Facts ? Fogg_is_fairly_odd: 我礼貌地笑了笑。 | 我皱起眉头。他是个疯子吗?}
'{Facts ? first_name_phileas: Phileas | Monsieur},真的是你!' 我喊道。

特别的,它允许我们在一行中同时测试多个游戏标志。

复制代码
{ Facts ? (Fogg_is_English, Fogg_is_fairly_odd):
	<> '我知道英国人很古怪,但这也太不可思议了!'
}
示例:更美观的打印方式

更注重文本化的打印方式

复制代码
LIST favouriteDinosaurs = (stegosaurs), brachiosaur, (anklyosaurus), (pleiosaur)

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

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

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

结果对比

My favourite dinosaurs are stegosaurs, anklyosaurus and pleiosaur.

My favourite dinosaurs are stegosaurs, anklyosaurus, pleiosaur. //直接打印

6) 高级列表操作

本节中的功能对于大多数游戏来说并不必要。

主要涉及一些集合运算,如果你不是很清楚,就尽量避免使用

列表比较

我们可以使用 ><>=<= 对列表变量进行比较

明显大于
复制代码
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_INVERT 对列表变量进行取反

复制代码
LIST ValueList = (first_value), (second_value), (third_value)
VAR testlist = (first_value)	
{ LIST_INVERT(testlist) }

如果对空的列表变量使用 LIST_INVERT,而游戏没有足够的上下文来判断如何取反,它会返回一个空值。

最安全的处理办法是手动判断。

复制代码
	LIST ValueList = first_value, second_value, third_value
	VAR testlist = ()	
	{ LIST_INVERT(testlist) }   //什么都不输出

	LIST ValueList = first_value, second_value, third_value
	VAR testlist = ()	
	{InvertValueList(testlist)}
	
	=== function InvertValueList(ref list)
	{not list:
		~ list = LIST_ALL(ValueList)
	- else:
		~ list = LIST_INVERT(list)
	}
交集

使用运算符 ^ 来获取交集

复制代码
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"

7) 横跨多个表的列表

列表变量中的值可以来自不同的表

用列表追踪对象
复制代码
	LIST Characters = Alfred, Batman, Robin
	LIST Props = champagne_glass, newspaper
	
	VAR BallroomContents = (Alfred, Batman, newspaper)
	VAR HallwayContents = (Robin, champagne_glass)
	
	{ describe_room(BallroomContents) }
	{ describe_room(HallwayContents) }
	
	=== function describe_room(roomState)
	{ roomState ? Alfred: Alfred 在角落里安静地站着。 } 
	{ roomState ? Batman: Batman 的存在感压倒一切。 } 
	{ roomState ? Robin: Robin 几乎被忽视了。 } <>
	{ roomState ? champagne_glass: 一只香槟杯被随意丢在地上。 } 
	{ roomState ? newspaper: 在桌上,一则标题高声喊出 WHO IS THE 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

这使得代码可以这样写:

复制代码
~ changeStateTo(kettleState, on) 
~ changeStateTo(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 或 c(顺序不可预测)

	{mixedList ? (a,b) }        // false
	{mixedList ^ LIST_ALL(a)}   // a, c
	
	{ mixedList >= (one, a) }   // true
	{ mixedList < (three) }     // false
	
	{ LIST_INVERT(mixedList) }  // one, b, two

8) List总结

列表的用法大致有以下三种:

标志(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++
属性(Properties)
  • 每个列表表示不同属性,每个属性包含其可能的状态值(如开/关、亮/灭等)
  • 改变状态时,先移除旧状态,再添加新状态
  • 使用 ?!? 进行测试

示例:

复制代码
LIST OnOffState = on, off
LIST ChargeState = uncharged, charging, charged

VAR PhoneState = (off, uncharged)

* {PhoneState !? uncharged } [插上手机充电]
	~ PhoneState -= LIST_ALL(ChargeState)
	~ PhoneState += charging
	你将手机插上充电器。
* { PhoneState ? (on, charged) } [给妈妈打电话]
相关推荐
小圣贤君3 个月前
从「选中一段」到「整章润色」:编辑器里的 AI 润色是怎么做出来的
人工智能·electron·编辑器·vue3·ai写作·deepseek·写小说