如何使用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 这一行做了三件事: 第一,定义了三个表项 ------`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) } [给妈妈打电话]