JavaScript must be enabled to play.
Browser lacks capabilities required to play.
Upgrade or switch to another browser.
Loading…
幻梦酱
[img[这是一枚可爱的芙芙|img/可爱的芙芙.jpg]]
这里是幻梦酱的新手教程
[[打开目录|Catalog]]
[[这里也可以打开目录|Catalog]]
- [[开始|Start]] ---- - [[第一章:片段|Chapter1]] - [[第二章:链接|Chapter2]] - [[第三章:多媒体内容|Chapter3]] - [[第四章:相对路径|Chapter4]] - [[第五章:变量|Chapter5]] - [[第六章:链接宏和更新宏|Chapter6]] - [[第七章:条件宏|Chapter7]] - [[第八章:循环宏|Chapter8]] - [[第九章:捕获宏|Chapter9]] ---- - [[第十章:特殊段落和标签|Chapter10]] - [[第十一章:widget宏和include宏|Chapter11]] - [[第十二章:数组和对象|Chapter12]] - [[第十三章:层叠样式表(选择器部分)|Chapter13]] - [[第十四章:层叠样式表(属性部分)|Chapter14]] - [[第十五章:JavaScript(简要介绍)|Chapter15]] ---- - [[各种实用小工具|tool]]
<<set $SpecialName to { PassageDone:{ class:"故事片段渲染后", text:"这个片段的内容会在每个故事片段完成显示后执行一次,适用于需要在每个片段显示后执行的代码,比如重新设置变量、重新设置片段内容的样式等,由于此时片段内变量内容已完成渲染,直接修改相关变量并不会更新片段内相应内容的显示。", output:false }, PassageHeader:{ class:"故事片段渲染时", text:"这个片段的内容会附加到每个故事片段的顶部,适用于需要在每个片段顶部显示的内容,比如导航链接、章节标题等。", output:true }, PassageReady:{ class:"故事片段渲染前", text:"这个片段的内容会在每个故事片段开始渲染前执行一次,适用于需要在每个片段开始渲染前执行的代码,比如初始化变量、设置片段内容的样式等,由于此时片段内容还没有开始渲染,因此修改相关变量会更新片段内相应内容的显示。", output:false }, StoryAuthor:{ class:"故事开始时", text:"这个片段用于填充界面栏中的作者署名区域,在故事标题下方,元素的ID为story-author", output:true }, StoryBanner:{ class:"故事开始时", text:"这个片段用于填充界面栏中的故事横幅区域,在故事标题上方,元素的ID为story-banner", output:true }, StoryCaption:{ class:"故事开始时", text:"这个片段用于填充界面栏中的故事标题区域,它就是故事标题,元素的ID为story-caption", output:true }, StoryDisplayTitle:{ class:"故事开始时", text:"这个片段用于设置浏览器的标题栏和UI栏中的显示标题,元素的ID为story-title,如果省略,会默认使用故事标题作为显示标题。", output:true }, StoryInit:{ class:"故事开始前", text:"这个片段的内容会在故事开始前执行一次,之后不会再次运行,适用于需要在故事开始前执行的代码,比如初始化变量。", output:false }, StoryMenu:{ class:"故事开始时", text:"这个片段用于填充界面栏中的菜单区域,元素的ID为story-menu,这个部分虽然会显示内容,但仅显示按钮和链接等内容,不能显示文本内容。", output:true }, StorySubtitle:{ class:"故事开始时", text:"这个片段用于填充界面栏中的故事副标题区域,在故事标题下方,元素的ID为story-subtitle", output:true }, widget:{ class:"故事片段渲染时", text:"这个标签用于将段落注册为小组件片段,它会在游戏启动时进行加载(时机早于StoryInit片段),注册为小组件片段后,与其他特殊片段一样,不能在故事中通过链接进行访问,但可以在其他片段中使用{{{<<widget>>}}}宏来调用。", output:true }, nobr:{ class:"故事片段渲染时", text:"这个标签会在段落渲染之前删除前导/尾随换行符,并将所有剩余的换行符序列替换为单个空格,相当于将整个段轮廓包裹在{{{<<nobr>>}}}宏内。", output:true } }>>
恭喜你,当你看到这一行文字,意味着你的电脑浏览器可以正常使用,拥有了调试的环境,正所谓好的开始是成功的一半,这说明了你已经取得了一半的胜利。 但是,不要骄傲,接下来还有很多东西需要你去学习。 首先,你需要确定你已经安装了最新版本的Twine软件,并且已经安装了SugarCube格式的故事格式。 如果你还没有安装Twine软件,可以访问<a href="https://twinery.org/" target="_blank">Twine官网</a>下载最新版本。 如果你不确定使用的是否为SugarCube格式,可以在Twine软件中点击“Twine-故事格式”按钮,然后选择“SugarCube 2.37.3”,并点击“设为默认故事格式”。 在确认上述步骤已经完成后,你可以点击下面的链接,进入基础部分内容的学习。 若你已经熟悉了基础部分的内容,可以直接跳转到进阶部分的学习。 [[开始基础章节|Chapter1]] [[进入进阶章节|Chapter10]] [[查看实用小工具|tool]]
Twine的基础是片段和链接,片段是Twine故事的基本构建块,而链接则是连接不同片段的桥梁。 在Twine中,片段是一个独立的文本块,它可以包含任何内容,包括文本、图片、音频等。 每个片段都有一个唯一的标题,用于标识和引用该片段。 片段的标题可以是任何文本,但通常建议使用简短且具有描述性的标题,以便于在故事中进行引用和链接。 片段的内容可以是纯文本,也可以包含HTML、CSS和JavaScript等代码。 现在你看到的内容就是一个片段的内容,它的标题是“Chapter1”,你可以在Twine中创建更多的片段来丰富你的故事。 [[下一章|Chapter2]]
恭喜你,刚刚成功的点击了Twine的链接,在Twine中,链接是非常重要的一个概念,它可以让你在不同的片段之间进行跳转。 在Twine中,链接的语法非常简单,只需要使用双括号包裹住链接的文本即可。 例如,[[新页面]]表示点击“新页面”这个链接后,会跳转到名为“新页面”的章节。它的写法是:{{{[[新页面]]}}} 在Twine中,直接使用双括号包裹住片段标题是最简单的创建连接的方式,但这样做也有一个缺点,那就是显示的链接文本是片段标题本身,这可能不是你想要的效果。 为了更好地控制链接的显示文本,你可以使用以下语法:{{{[[链接文本|片段标题]]}}} 这样,你可以将“链接文本”替换为你想要显示的文本,而“片段标题”则是你想要跳转到的章节标题。 例如[[跳转到新页面|新页面]],表示点击“跳转到新页面”这个链接后,会跳转到名为“新页面”的章节。 链接的片段可以是你故事中任意片段,包括所在的这个片段本身,如果链接跳转的片段不存在,Twine会自动创建一个新的片段。 在Twine中,链接是一个十分重要的功能,任何一个片段如果没有链接指向它,它就无法被访问到。 [[下一章|Chapter3]]
这里是一个新页面 [[返回上一页|Chapter2]]
除了文字外,Twine还支持在片段中插入图片、音频等多媒体内容。 例如:[img[这是一枚可爱的芙芙|img/可爱的芙芙.jpg]] 你可以使用HTML标签来插入这些内容,例如使用{{{<img>}}}标签来插入图片,使用{{{<audio>}}}标签来插入音频。 你也可以使用Twine的内置宏来实现相同的功能,例如{{{<<audio>>}}}来插入音频文件。 Twine没有图像宏,但有更加简便的插入图像方式:{{{[img[image.jpg]]}}},其中image.jpg是你想要插入的图片文件名。 如果你希望插入的图片在点击时可以跳转到其他片段,可以使用{{{[img[image.jpg|链接文本]]}}}的方式。 例如,{{{[img[image.jpg][新页面]]}}} [[下一章|Chapter4]]
在Twine中,插入媒体内容需要写出文件所在的路径,通常我们使用的是相对路径。 相对路径的写法很简单,以你游戏的HTML文件所在位置为基准,逐级写出文件夹和文件名即可。 若是当前目录,用{{{.\}}}表示。 若是上级目录,用{{{..\}}}表示。 若是根目录,则用{{{\}}}表示。 >game >>HTML >>>你的游戏.html >>>img >>>>img1.png >>>>img2.png >>src >>>button.js >>>link.js 例如这样的文件结构,你的游戏位于game文件夹下HTML子文件夹,名为{{{你的游戏.html}}} 如果你想引用当前所在文件夹下img文件夹里的文件img.png,你可以写成{{{.\img\img1.png}}} 其中{{{.\}}}代表''当前文件夹'',{{{img\}}}代表''img文件夹'',{{{img.png}}}代表''img1.png''文件。 如果你想引用和你游戏所在文件夹同级的src文件夹下的文件button.js,你可以写成{{{..\src\button.js}}} 其中{{{..\}}}代表''上一级文件夹'',{{{src\}}}代表''src文件夹'',{{{button.js}}}代表''button.js'' [[下一章|Chapter5]]
在Twine中,你可以使用变量来存储和管理数据。 变量可以用来存储用户的选择、游戏状态等信息,以便在不同的片段之间进行传递和使用。 你可以使用{{{<<set>>}}}宏来创建和设置变量,例如{{{<<set $variableName to "value">>}}}。 变量名通常以$符号或_符号开头,以便于区分它们与普通文本。 在上面的例子中{{{$variableName}}}是变量的名称,"value"是你想要存储的值。 $开头的变量是全局变量,可以在任何片段中访问和修改。 _开头的变量是局部变量,只能在当前片段中访问和修改。 Twine中变量支持的类型与JavaScript类似,包括字符串、数字、布尔值等,也可以存储数组和对象等复杂数据结构。 你可以直接使用裸变量来引用变量的值,例如{{{$variableName}}},Twine会自动将其替换为变量的实际值。 也可以使用{{{<<print $variableName>>}}}或{{{<<=>>}}}宏来输出变量的值。 除了使用{{{<<set>>}}}宏来设置和修改变量外,你还可以在Twine的链接中对变量进行操作。 例如,{{{[[跳转页面|新页面][$variableName to "value"]]}}}表示在跳转到“新页面”时,将变量{{{$variableName}}}的值设置为"value"。 同理,图片链接也可以使用类似的方式,例如{{{[img[image.jpg][新页面][$variableName to "value"]]}}}。 变量也可以作为片段标题的一部分来使用,例如{{{[[$variableName]]}}},这表示点击链接后会跳转到名为“$variableName”的变量的值的片段。 你可以尝试点击下面的链接来测试变量的使用。 将{{{$variableName}}}变量设置为:(<<nobr>><<set $variableName to "新页面A">> <<link "新页面A">><<set $variableName to "新页面A">><<redo>><</link>> / <<link "新页面B">><<set $variableName to "新页面B">><<redo>><</link>> / <<link "下一章">><<set $variableName to "Chapter6">><<redo>><</link>> <</nobr>>) 观察下面的跳转链接的变化,你可以点击它看看会进入什么页面: <<do>>[[$variableName]]<</do>>
这个片段的标题是新页面A [[返回上一页|Chapter5]]
这个片段的标题是新页面B [[返回上一页|Chapter5]]
这个片段的标题是新页面C [[返回上一页|Chapter6]]
这个片段的标题是新页面D [[返回上一页|Chapter6]]
你是否发现,上一章出现了点击后不会跳转到新片段的情况? 这是因为使用了不同于{{{[[新页面]]}}}的另一种链接方式:{{{<<link "链接文本">>链接内容<</link>>}}}。 在{{{<<link>>}}}宏中,除了可以设置和{{{[[新页面]]}}}一样的跳转链接外,还可以在链接中添加其他内容,例如设置变量、执行代码等。 例如{{{<<link "新页面A">><<set $variableName to "新页面A">><<redo>><</link>>}}},它的作用不是跳转到新页面A,而是设置变量{{{$variableName}}}的值为“新页面A”。 现在你可以尝试点击下面的链接,看看会发生什么: 将{{{$variableName}}}变量设置为:(<<nobr>><<set $variableName to "新页面C">> <<link "新页面C">><<set $variableName to "新页面C">><<redo>><</link>> / <<link "新页面D">><<set $variableName to "新页面D">><<redo>><</link>> <</nobr>>) 现在{{{$variableName}}}的值为:<<do>>$variableName<</do>> 再次观察下面的跳转链接的变化: [[$variableName]] 是不是发现无论将{{{$variableName}}}设置成什么,上面的链接都没有发生变化? 问题出在了哪里? 这是上一章的链接部分: {{{ 将$variableName变量设置为:(<<nobr>><<set $variableName to "新页面A">> <<link "新页面A">><<set $variableName to "新页面A">><<redo>><</link>> / <<link "新页面B">><<set $variableName to "新页面B">><<redo>><</link>> / <<link "下一章">><<set $variableName to "Chapter6">><<redo>><</link>> <</nobr>>) 观察下面的跳转链接的变化,你可以点击它看看会进入什么页面: <<do>>[[$variableName]]<</do>> }}} 这是这一章的链接部分: {{{ 将$variableName变量设置为:(<<nobr>><<set $variableName to "新页面C">> <<link "新页面C">><<set $variableName to "新页面C">><<redo>><</link>> / <<link "新页面D">><<set $variableName to "新页面D">><<redo>><</link>> <</nobr>>) 现在$variableName的值为:<<do>>$variableName<</do>> 再次观察下面的跳转链接的变化: [[$variableName]] }}} 是不是发现,相比于上一章,这一章的跳转链接部分没有使用{{{<<do>>}}}宏? 这就是问题所在。 在Twine中,{{{<<do>>}}}宏用于在{{{<<redo>>}}}宏被触发的时候,执行内部包裹的代码并刷新结果。 如果没有使用{{{<<do>>}}}宏和{{{<<redo>>}}}宏,Twine就不会重新渲染片段内容,因此即使修改了变量的值,当前片段内使用变量的部分也不会更新,这在使用{{{<<link>>}}}宏时尤其明显。 所以,如果你想要在当前片段中修改已显示的变量的值并实时更新,必须使用{{{<<do>>}}}宏来包裹住变量的值,并在触发的链接中加入{{{<<redo>>}}}。 [[下一章|Chapter7]]
变量除了用在链接中之外,还可以用在其他地方,比如根据变量的值进行判断,显示不同的内容。 这就要用到条件宏了。 在Twine中,条件宏分为两种,第一种为{{{<<if>>}}}宏,第二种为{{{<<switch>>}}}宏。 {{{<<if>>}}}宏用于根据条件判断来执行不同的代码块,而{{{<<switch>>}}}宏则用于根据变量的值来选择执行不同的代码块。 {{{<<if>>}}}宏的语法如下: {{{ <<if (条件语句)>> (条件为真时执行的代码) <<elseif (条件语句)>>(可选,可重复多次) (条件为真时执行的代码) <<else>>(可选) (条件为假时执行的代码) <</if>> }}} {{{<<switch>>}}}宏的语法如下: {{{ <<switch (变量名或表达式)>> <<case (值)>> (可选,可重复多次) (值匹配时执行的代码) <<default>>(可选) (没有匹配到任何值时执行的代码) <</switch>> }}} {{{<<if>>}}}宏和{{{<<switch>>}}}宏都可以嵌套使用,也可以和其他宏结合使用。 其中{{{<<if>>}}}宏的条件语句的值必须为布尔值(true或false),而{{{<<switch>>}}}宏的变量名或表达式可以是任何类型的值。 常用的条件语句包括比较运算符(如{{{==}}}、{{{===}}}、{{{!=}}}、{{{!==}}}、{{{<}}}、{{{<=}}}、{{{>}}}、{{{>=}}})和逻辑运算符(如{{{&&}}}、{{{||}}})。 特别注意,{{{==}}}和{{{===}}}的区别在于,前者会先对运算符右边的值进行类型转换后比较,而后者不会。 而{{{!=}}}和{{{!==}}}的区别在于,前者会先对运算符右边的值进行类型转换后比较,而后者不会。 {{{=}}}是赋值运算符,而不是比较运算符,所以在条件语句中不能使用{{{=}}}。 同样,在同一个片段内的变量发生变化时,想要同步更新条件判断的结果,也需要使用{{{<<do>>}}}宏来包裹住条件判断的代码块,并在触发的链接中加入{{{<<redo>>}}}。 [[下一章|Chapter8]]
除了{{{<<if>>}}}和{{{<<switch>>}}}之外,Twine还提供了另一种条件宏,也就是{{{<<for>>}}}宏。 {{{<<for>>}}}宏的用途是重复多次执行其包含的内容,直到满足某个条件为止。 {{{<<for>>}}}宏有三种形式: {{{ <<for [conditional]>> … <</for>>(写法最简便,适用于循环内容自带条件判断的情况) <<for [init] ; [conditional] ; [post]>> … <</for>>(适用性最广泛,可直接设定循环次数,也可以根据当前情况灵活判断循环次数) <<for [[keyVariable ,] valueVariable] range collection>> … <</for>>(适用于数组元素遍历) }}} 其中conditional为条件语句,每次循环开始前都会判断,如果为真则执行循环,init为初始化语句,第一次执行循环前对循环变量进行初始化,post为后置语句,每次循环后执行一遍,keyVariable为键变量,初始会被设定为0,每次循环后加一,valueVariable为值变量,是当前循环时数组的元素,range为范围关键字,collection为要执行循环的数组变量,循环过程中不会进行修改。除了range和collection外,其他参数都是可选的。 {{{<<for>>}}}宏用于循环执行代码块,直到条件不满足为止。 {{{<<for>>}}}宏的第一种形式和第二种形式类似于JavaScript中的for循环,而第三种形式则类似于Python中的for循环。 {{{<<for>>}}}宏可以嵌套使用,也可以和其他宏结合使用。 下面是一个使用{{{<<for>>}}}宏遍历数组{{{$collection}}}按顺序输出数组每个元素的示例,分别使用三种形式: {{{ // 第一种形式 <<set _i to 0>> <<for _i < $collection.length>> $collection[_i] <<set _i to _i + 1>> <</for>> // 第二种形式 <<for _i = 0 ; _i < $collection.length ; _i++>> $collection[_i] <</for>> // 第三种形式 <<for _value range $collection>> _value <</for>> }}} 在{{{<<for>>}}}宏中,可以使用{{{<<break>>}}}宏来提前结束循环,使用{{{<<continue>>}}}宏来跳过当前循环的剩余部分并开始下一次循环。 [[下一章|Chapter9]]
上一章所介绍的{{{<<for>>}}}宏是Twine中最常用的循环宏之一,它可以用于显示数组元素、执行重复操作等。 然而,若要在循环完成后使用循环中的变量,可能会遇到一些问题。 其中一个问题是,循环结束后,循环变量的值可能不是你期望的结果。 例如上一章的这个循环语句,如果想要用数组元素的值作为链接跳转的片段名: {{{ <<for _value range $collection>> [[_value]] <</for>> }}} 它会输出数组{{{$collection}}}中的每个元素,并创建相应的链接,但循环结束后,由于变量{{{_value}}}的值将是数组的最后一个元素,无论点击其中哪一个链接,跳转的结果都会是最后一个元素的值所代表的片段。 为了解决这个问题,Twine提供了{{{<<capture>>}}}宏。 {{{<<capture>>}}}宏可以捕获循环中的变量值,在宏的内容中创建其值的本地化版本,并在循环结束后使用。 {{{<<capture>>}}}宏可以用于捕获任何变量的值,包括全局变量和局部变量。 它的语法如下: {{{ <<capture variableName>>内容<</capture>> }}} 其中,{{{variableName}}}是你想要捕获的变量名,内容是你想要捕获的内容。 因此,上面的循环可以改为: {{{ <<for _value range $collection>> <<capture _value>>[[_value]]<</capture>> <</for>> }}} 这样,每次循环时,{{{_value}}}的值都会被捕获并创建相应的本地化版本,点击链接时就会跳转到对应的片段,而不是最后一个元素的值所代表的片段。 [[进入进阶章节|Chapter10]]
在Twine中,除了基本的片段和链接外,还有一些特殊的片段和标签。 其中特殊片段是Twine内置有着特殊作用的片段,它们有着各自的功能,不应该在故事中通过链接进行访问。 以下是Twine内置的特殊片段和标签,你可以点击这些链接查看其具体功能:<<set _name to $SpecialName.PassageReady>> <div id="Chapter10-body"> <div id="Chapter10-left"> <br>Passage片段<br><br> <<link "PassageReady">><<set _name to $SpecialName.PassageReady>><<redo>><</link>><br> <<link "PassageHeader">><<set _name to $SpecialName.PassageHeader>><<redo>><</link>><br> <<link "PassageDone">><<set _name to $SpecialName.PassageDone>><<redo>><</link>><br> <br>Story片段<br><br> <<link "StoryInit">><<set _name to $SpecialName.StoryInit>><<redo>><</link>><br> <<link "StoryMenu">><<set _name to $SpecialName.StoryMenu>><<redo>><</link>><br> <<link "StoryCaption">><<set _name to $SpecialName.StoryCaption>><<redo>><</link>><br> <<link "StoryAuthor">><<set _name to $SpecialName.StoryAuthor>><<redo>><</link>><br> <<link "StoryBanner">><<set _name to $SpecialName.StoryBanner>><<redo>><</link>><br> <<link "StoryDisplayTitle">><<set _name to $SpecialName.StoryDisplayTitle>><<redo>><</link>><br> <<link "StorySubtitle">><<set _name to $SpecialName.StorySubtitle>><<redo>><</link>><br> <br>特殊标签<br><br> <<link "widget">><<set _name to $SpecialName.widget>><<redo>><</link>><br> <<link "nobr">><<set _name to $SpecialName.nobr>><<redo>><</link>><br> </div> <div id="Chapter10-right"><<do>> <div id="Chapter10-right-class">运行或渲染时机:_name.class</div> <div id="Chapter10-right-output">是否会显示内容:<<= _name.output ? "是" : "否" >></div> <div id="Chapter10-right-text"><<= _name.text>></div> <</do>></div> </div> [[下一章|Chapter11]]
在我们进行游戏创作的过程中,经常会遇到某段代码或内容需要在多个场景中使用的情况。 如果在每个场景内都将相同的内容写一遍的话,所需的时间成本过高,而且一旦需要修改这些内容,更是相当麻烦。 为了避免这种情况发生,我们可以使用以下两个内置宏 {{{<<widget>>}}}宏和{{{<<include>>}}}宏。 {{{<<widget>>}}}宏用于定义代码小组件,它的语法如下: {{{<<widget widgetName [container]>> … <</widget>>}}} 其中{{{widgetName}}}为字符串,也就是你给这个小组件取的名字。 而{{{container}}}为可选关键字,若包含该关键字,表示这个小组件是否为容器小组件,也就是需要结束标签,若不包含则为单标签。 {{{<<widget>>}}}宏的内容即为你想要复用的代码的内容。 需要注意,定义小组件需要在非故事片段中,并给这个片段加上标签{{{widget}}}(注意是标签而不是片段名)。 一个带{{{widget}}}标签的片段可以存放一个或多个小组件定义,但不建议将所有小组件都放在一个{{{widget}}}标签片段中。 完成小组件定义后,在其他片段中就可以使用{{{<<组件名>>}}}或{{{<<组件名>>…<</组件名>>}}}(取决于是否有{{{container}}}关键字)的方式调用小组件内容。 相比之下,{{{<<include>>}}}宏的使用方式就简单一些了。 {{{<<include>>}}}宏用于在当前片段显示其他片段的内容,因此无需提前定义。 只需要在任意片段使用{{{<<include passageName [elementName]>>}}}即可。 其中{{{passageName}}}为字符串,也就是你需要显示的目标片段名称。 而{{{elementName}}}为可选字符串,是包装目标片段内容的HTML元素。 [[下一章|Chapter12]]
在Twine中,数组和对象是两种常用的数据结构。 数组是一种有序的数据集合,可以存储多个值,每个值都有一个索引。 它的结构为:{{{[value1, value2, value3]}}},其中{{{value1}}}、{{{value2}}}和{{{value3}}}是数组中的元素,它们的索引从0开始依次递增。 若要访问数组中的元素,可以使用{{{$array[index]}}}的方式,其中{{{$array}}}是数组的名称,{{{index}}}是元素的索引。 对象是一种无序的数据集合,可以存储多个键值对,每个键都有一个唯一的名称。 它的结构为:{{{{"key1": "value1", "key2": "value2"}}}},其中{{{key1}}}和{{{key2}}}是对象中的键,{{{"value1"}}}和{{{"value2"}}}是对应的值。 若要访问对象中的值,可以使用{{{$object.key}}}的方式,其中{{{$object}}}是对象的名称,{{{key}}}是键的名称。 你可以使用{{{<<set>>}}}宏来创建和设置数组和对象。 例如,{{{<<set $array to [1, 2, 3]>>}}}表示创建一个名为{{{$array}}}的数组,包含三个元素1、2和3。 同样,{{{<<set $object to {key1: "value1", key2: "value2"}>>}}}表示创建一个名为{{{$object}}}的对象,包含两个键值对,键为{{{key1}}}和{{{key2}}},对应的值为{{{"value1"}}}和{{{"value2"}}}。 数组的元素可以是任何类型的值,包括字符串、数字、布尔值、对象等。 对象的值也可以是任何类型的值,包括数组、字符串、数字、布尔值等。 你可以根据实际情况灵活运用,将数组和对象结合使用来存储和管理数据。 例如这样的变量: {{{ <<set $testvar to { name: "测试变量", value: 123, tags: ["tag1", "tag2"], nestedObject: { key1: "value1", key2: "value2" } }>> }}} 当你需要访问它的name属性时,可以使用{{{$testvar.name}}},访问value属性时可以使用{{{$testvar.value}}},访问tags数组的第一个元素时可以使用{{{$testvar.tags[0]}}},访问nestedObject对象的key1属性时可以使用{{{$testvar.nestedObject.key1}}}。 [[下一章|Chapter13]]
当我们想要修改页面元素的样式时,可以使用CSS(层叠样式表)。 在Twine中,你可以使用内置的“故事-故事样式表”来修改页面元素的样式。 想要修改特定元素的样式,你需要先确定该元素的CSS选择器。 CSS选择器是用来选取页面元素的规则,它可以是元素的标签名、类名、ID名等。 例如,若要选取所有的段落元素,可以使用{{{p}}}选择器;若要选取所有具有特定类名的元素,可以使用{{{.classname}}}选择器;若要选取具有特定ID的元素,可以使用{{{#idname}}}选择器,若要选取所有元素,可以使用{{{*}}}选择器,这些叫做“简单选择器”。 ---- 除此之外,CSS选择器还支持更复杂的关系选择器。 例如,有下列HTML结构: {{{ <div class="container"> <h1 class="title">这是标题</h1> <p class="text">这是一个段落。</p> <div id="content"> <span class="highlight">这是第一个高亮文本。</span> <span class="highlight">这是第二个高亮文本。</span> </div> <p class="text">这是一个段落。</p> </div> }}} 当你需要选择类名为container的元素下的所有段落元素时,可以使用{{{.container p}}}选择器;若要选择ID为content的元素下的所有span元素,可以使用{{{#content span}}}选择器。 这种组合选择器是后代选择器,可以选取特定元素下的所有特定子元素(子代、子代的子代等等,相当于这个标签内包裹的所有符合要求的内容),它通常用单个空格(" ")表示)。 当你需要选择类名为container的元素下的直接子段落元素时,可以使用{{{.container > p}}}选择器;若要选择ID为content的元素下的直接子span元素,可以使用{{{#content > span}}}选择器。 这种组合选择器是子选择器,可以选取特定元素下的直接子元素(只包括子代,不包括子代的后代),它通常用大于号(">")表示。 当你需要选择紧跟在类名为text的元素后面的div元素时,可以使用{{{.text + div}}}选择器,若要选择紧跟在ID为content的元素后面的段落元素,可以使用{{{#content + p}}}选择器。 这种组合选择器是接续兄弟选择器,可以选取特定元素后面的直接兄弟元素(只包括紧接在后面的,拥有同一个父元素的兄弟元素),它通常用加号("+")表示。 当你需要选择类名为title的元素后面的所有兄弟元素时,可以使用{{{.title ~ *}}}选择器,若要选择ID为content的元素后面的所有兄弟元素,可以使用{{{#content ~ *}}}选择器。 这种组合选择器是一般兄弟选择器,可以选取特定元素后面的所有兄弟元素(包括紧接在后面的和后面所有的,拥有同一个父元素的兄弟元素),它通常用波浪号("~")表示。 如果希望将相同的属性应用到多个选择器上,可以使用逗号(",")分隔选择器。 例如,若要将相同的样式应用到所有段落和标题元素上,可以使用{{{p, h1, h2}}}选择器。 若要选择选择类名为title的元素后面的直接兄弟段落元素和ID为content的元素下的所有span元素,可以使用{{{.title + p, #content span}}}选择器。 这叫做选择器列表,基本上等同于将列表中的选择器分别写出来,但要注意的是选择器列表中若有任意选择器是无效的,则整个列表中其他选择器也都会被忽略。 ---- 在应用CSS样式时,有时会遇到特异性(Specificity)的问题。 特异性是指当多个选择器匹配同一个元素时,浏览器会根据特异性来决定哪个样式优先应用,当多个选择器拥有某个相同属性时,特异性值更高的会被应用。 特异性是一个三元组,表示ID选择器、类选择器和元素选择器的数量。 特异性分值的计算规则如下: - ID:选择器中包含 ID 选择器,则百位得一分。 - 类:选择器中包含类选择器、属性选择器或者伪类,则十位得一分。 - 元素:选择器中包含元素、伪元素选择器,则个位得一分。 - 通用选择器(*)、组合符(+、>、~、' ')和调整优先级的选择器(:where())不会影响优先级。 最终按照选择器的特异性值总和计算优先级。 //PS:内联样式的特异性分值为1000,也就是ID选择器的十倍,而在属性值后面写{{{!important}}}更是会无视特异性值保证绝对优先生效,但这两种方法都不利于样式管理,因此除非迫不得已,最好不建议使用这两种方式。// 例如,选择器{{{#id .class p}}}的特异性为(1,1,1),表示有1个ID选择器、1个类选择器和1个元素选择器,它的特异性值为111。 而选择器{{{.class p}}}的特异性为(0,1,1),表示有0个ID选择器、1个类选择器和1个元素选择器,它的特异性值为011。 如果你想要计算一个CSS选择器的特异性,可以使用下面的工具: <label for="selectorInput">输入 CSS 选择器:</label> <input id="selectorInput" type="text" /> <button id="checkSpecificity">确定</button> <div id="specificityResult"></div> [[下一章|Chapter14]]
上一章我们介绍了层叠样式表的选择器部分,这一章我们介绍层叠样式表的属性部分。 由于HTML元素类型非常多,每种元素可用的属性也非常多,本章篇幅有限就不一一介绍了。 若对相关内容感兴趣(包括上一章提到的CSS选择器部分),并且想深入学习层叠样式表相关知识,可以在<a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS">这里</a>进行学习。 此处,仅对CSS属性的基本概念进行简单介绍。 上一章我们介绍了层叠样式表的选择器部分,但一个完整的CSS代码语句不仅仅只有选择器,还有同样^^也许更加^^重要的属性部分。 就比如以下这个CSS代码: {{{ span { border: 1px solid black; background-color: lime; } }}} 可以看到,它的第一行内容,是上一章学过的选择器,它会选中页面中所有{{{<span>}}}元素。 在选择器后面,有一个{{{“{”}}}符号,它意味着从这里直到下一个{{{“}”}}}符号为止,中间的内容全部视为{{{span}}}这个选择器的属性。 而在{{{“{}”}}}中间的两行的属性部分,也有相应的格式,{{{[属性名]:[属性值];}}}(注意是英文的冒号和分号)。 每个CSS代码语句内的属性语句数量不限,如果其中一条属性有错误,也不会影响到其他正确的属性的生效,同一个CSS语句中可以有相同的属性,如果出现相同的属性,最下方的会优先生效。 [[下一章|Chapter15]]
既然已经介绍了CSS(层叠样式表),又怎么能不介绍它的兄弟JS(JavaScript)呢? 然而,JavaScript的内容相比CSS就更加复杂了,仅仅通过一章内容就能从零开始教会你使用它,实在是不现实==,更何况我自己对JavaScript的了解也不算太多==。 如果你想要深入学习JavaScript,可以在<a href="https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Core/Scripting/What_is_JavaScript">这里</a>进行学习。 在Twine中,有专门存放JavaScript的片段,你可以点击“故事-JavaScript”按钮来打开它,并将你游戏中所用到的JavaScript代码全部放进去。 还记得上上一章那个计算CSS选择器特异性的小工具吗,那就是用JavaScript制作的,而它的代码如下所示: {{{ /** * 这个脚本用于统计CSS选择器的特异性(优先级)。 * 用户可以输入一个CSS选择器,点击确定后会显示该选择器的特异性值。 * 特异性值的计算规则如下: * - ID选择器特异性为100 * - 类选择器、属性选择器和伪类选择器特异性为10 * - 元素选择器和伪元素选择器特异性为1 * - 通配符选择器(*)特异性为0 */ // 用户点击按钮时触发 $(document).on('click', '#checkSpecificity', function () { const selector = $('#selectorInput').val(); try { const specificity = getSpecificity(selector); $('#specificityResult').text('优先级为:(' + specificity.join(', ') + ')'); } catch (e) { $('#specificityResult').text('无效的选择器:' + e.message); } }); // 主函数:计算选择器优先级 function getSpecificity(selector) { let idCount = 0; let classAttrPseudoClassCount = 0; let elementPseudoElementCount = 0; // 替换 split(','),防止 :is(.a, span) 被错误拆分 const selectors = splitSelectors(selector); for (const sel of selectors) { const s = sel.trim(); // Step 1: 提取伪类函数并计算参数优先级 const pseudoFunctionRegex = /:(not|is|has|where)\(([^()]+(?:\([^()]*\)[^()]*)*)\)/g; let match; while ((match = pseudoFunctionRegex.exec(s)) !== null) { const name = match[1]; const args = match[2]; if (name === 'where') continue; // :where() 不计优先级 const argSelectors = splitSelectors(args); // ← 修正这也用 splitSelectors let maxSpec = [0, 0, 0]; for (const arg of argSelectors) { const subSpec = getSpecificity(arg.trim()); maxSpec = compareSpecificity(maxSpec, subSpec); } idCount += maxSpec[0]; classAttrPseudoClassCount += maxSpec[1]; elementPseudoElementCount += maxSpec[2]; } // Step 2: 清除伪类函数部分,避免重复计数 let sCleaned = s.replace(/:(not|is|has|where)\(([^()]+(?:\([^()]*\)[^()]*)*)\)/g, ''); // ID 选择器 idCount += (sCleaned.match(/#[\w-]+/g) || []).length; // 类选择器 const classMatches = sCleaned.match(/\.[\w-]+/g) || []; // 属性选择器 const attrMatches = sCleaned.match(/\[[^\]]+\]/g) || []; // 普通伪类(不含伪元素和函数伪类) const pseudoClassMatches = (sCleaned.match(/(?<!:):[\w-]+(\([^)]+\))?/g) || []) .filter(p => !/^:(not|is|has|where)\(/.test(p)); classAttrPseudoClassCount += classMatches.length + attrMatches.length + pseudoClassMatches.length; // 元素选择器(如 div, p) const elementMatches = sCleaned.match(/(^|[\s>+~])\s*[a-zA-Z][\w-]*/g) || []; // 伪元素(如 ::after) const pseudoElementMatches = sCleaned.match(/::[\w-]+/g) || []; elementPseudoElementCount += elementMatches.length + pseudoElementMatches.length; } return [idCount, classAttrPseudoClassCount, elementPseudoElementCount]; } // 比较两个 specificity 三元组,返回优先级更高的一个 function compareSpecificity(a, b) { for (let i = 0; i < 3; i++) { if (a[i] > b[i]) return a; if (a[i] < b[i]) return b; } return a; } // 安全拆分选择器,避免拆到伪类参数内的逗号 function splitSelectors(selectorString) { const result = []; let current = ''; let depth = 0; for (let i = 0; i < selectorString.length; i++) { const char = selectorString[i]; if (char === '(') { depth++; } else if (char === ')') { if (depth > 0) depth--; } else if (char === ',' && depth === 0) { result.push(current.trim()); current = ''; continue; } current += char; } if (current.trim() !== '') { result.push(current.trim()); } return result; } }}} [[查看实用小工具|tool]]
我会在这里上传一些实用的小工具,大家可以随意使用。 若显示格式出现问题,推荐使用nobr标签或全局nobr设置。 我使用的是另一种替代方案: {{{ Config.passages.onProcess = function (p) { return p.text.replace(/[\t\n]+/g, ''); }; }}} 它可以在渲染页面的时候自动去除页面内容中的制表符与换行符(不会影响<br>换行)。 它的优点是不会像内置nobr功能那样将空白字符替换为单个空格,(就像 这样 一样,在 代码 比较多的 片段中,看起来很 奇怪)。 缺点是因为不会处理空格符号,你在编写游戏代码的时候,为了提高代码可读性而格式化代码,必须使用制表符。 # [[通用时间系统|time_widget]] # [[通用天气系统|weather_widget]]
StoryInit部分,变量初始化: {{{ <<set $time = { minute:0, hour:8, day:31, weekDay:4, month:8, season:2, year:2018, show:{ minute:0, hour:8, weekDay:"周五", season:"秋天" }, dayChange:0, daySum:0, seasonRules:["春天","夏天","秋天","冬天"], weekRules:["周一","周二","周三","周四","周五","周六","周日"], monthRules:[31,28,31,30,31,30,31,31,30,31,30,31] }>> }}} ---- widget部分: {{{ <<widget "timeWidget">> /* 处理分钟数进位 */ <<for $time.minute >= 60>> <<set $time.minute -= 60>> <<set $time.hour += 1>> /*所有每天特定小时更新的内容放在这里,不过你需要自己写判断小时数的代码*/ <</for>> /* 处理小时数进位以及星期数进位 */ <<for $time.hour >= 24>> <<set $time.hour -= 24>> <<set $time.day += 1>> <<set $time.daySum += 1>> /*所有需要每天凌晨更新的内容都写在这里就行了*/ <<set $time.weekDay += 1>> <</for>> <<if $time.weekDay > 6>> <<set $time.weekDay = $time.weekDay % 7>> <</if>> /* 处理日期数进位以及年份进位,由于无法直接处理星期数,需要避免直接对$time.day变量的增加操作 */ <<for $time.day > ($time.monthRules[$time.month - 1] + ($time.month == 2 && ($time.year % 4 == 0 && ($time.year % 100 != 0 || $time.year % 400 == 0)) ? 1 : 0))>> <<set $time.day -= ($time.monthRules[$time.month - 1] + ($time.month == 2 && ($time.year % 4 == 0 && ($time.year % 100 != 0 || $time.year % 400 == 0)) ? 1 : 0))>> <<set $time.month += 1>> <<switch $time.month>> <<case 2 3 4>> <<set time.season = 0>> <<case 5 6 7>> <<set time.season = 1>> <<case 8 9 10>> <<set time.season = 2>> <<case 11 12 1>> <<set time.season = 3>> <</switch>> <<if $time.month > 12>> <<set $time.month = 1>> <<set $time.year += 1>> <</if>> <</for>> /* 格式化分钟 */ <<set $time.show.minute = $time.minute>> <<if $time.minute < 10>> <<set $time.show.minute = "0" + $time.minute>> <</if>> <<set $time.show.hour = $time.hour>> <<set $time.show.weekDay = $time.weekRules[$time.weekDay]>> <<set $time.show.season = $time.seasonRules[$time.season]>> <</widget>> }}} ---- StoryCaption部分(或者其他你想要显示时间的地方): {{{ <<timeWidget>>今天是:第 $time.year 年 $time.month 月 $time.day 日<br> $time.show.weekDay<br> 现在是$time.show.season<br> $time.show.hour : $time.show.minute<br> }}} ---- [[返回|tool]]
StoryInit部分: {{{ <<set $weather = { rules:[ ["晴天","多云","阴天","小雨","中雨","大雨","雷雨"], ["晴天","多云","阴天","小雨","中雨","大雨","冰雹"], ["晴天","多云","阴天","小雨","中雨","大雨","雷雨"], ["晴天","多云","阴天","小雪","中雪","大雪","暴雪"] ], today:"晴天", tomorrow:"小雨" }>> }}} ---- widget部分: {{{ <<widget "weather">>//天气变更 <<set _weatherKey = null>> <<if [例外条件]>> /* 特殊情况下天气处理方式 */ <<else>> <<set _weatherKey = random(0,$weather.rules[$time.season].length)>> <</switch>> <<if $weather.tomorrow != null>> /* 已经通过某种方式确定第二天的天气的情况 */ <<set $weather.today = $weather.tomorrow>> <<set $weather.tomorrow = null>> <<else>> /* 直接随机的情况 */ <<set $weather.today = $weather.rules[$time.season][_weatherKey]>> <</if>> <</widget>> <<widget "prediction">>//预测,或者预言第二天的天气 <<if _args[0] != null && $weather.rules[$time.season].includes(_args[0])>> <<set $weather.tomorrow = _args[0]>> <<else>> <<set _weatherKey = random(0,6)>> <<set $weather.tomorrow = $weather.rules[$time.season][_weatherKey]>> <</if>> <</widget>> }}} ---- 具体使用: {{{<<weather>>}}}放在时间系统中你希望变更天气的地方,例如小时进位天数增加的代码中。 {{{<<prediction>>}}}放在天气预报,或者其他可以预知/确定下一次天气变更情况的地方,不带参数使用为随机确定下一次天气,带参数使用为设定下一次天气 ---- PS:其中{{{$time}}}开头的变量为上一章中时间系统变量,建议和时间系统搭配使用,若你有其他时间系统代码,可以将相应变量进行修改后使用。 [[返回|tool]]