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
- test2 Option A
其实就是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
这一行做了三件事:
第一,定义了三个表项 ------cold、boiling 和 recently_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
这段代码创建了这三个表项:cold、boiling、recently_boled。
一个InkListItem的主体由两个字符串组成originName、itemName。
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) } [给妈妈打电话]