# 事件 ?> 在这一节中,让我们来了解事件系统的基本原理 ## 事件编辑(地图选点,快捷键X) ![image](img/eventsx.jpg) 样板所有的事件都是依靠“触发器”完成的。例如,勇士碰到(绑定好的)楼梯可以触发changeFloor,碰到门可以触发openDoor,碰到怪物可以触发battle,碰到道具可以触发getItem,碰到箱子可以触发pushBox,走上冰面(背景层)可以触发ski. 这些触发器都是系统自带的(称为系统事件),已经存在处理机制,不需要我们操心。我们真正所需要关心的,其实只是自定义的事件(如NPC)。 地图选点(快捷键X)一共有七项,在地图中以图块左下角用七色小方块标记。 * **红色:**普通事件 * **橙色:**自动事件 * **黄色:**战后事件 * **绿色:**楼层转换 * **青色:**获得道具后事件 * **靛色:**不是事件,为勇士站在该点不能朝哪些方向走 * **紫(粉)色:**开门后事件。 此外还有一些事件不在地图上,如“首次到达”、“每次到达”(这两个的触发原理,详见“脚本编辑——切换楼层后”)、“道具效果”、“公共事件”和全塔属性中那五个事件。 ### 事件的机制 地图上的所有事件都存在两种状态:启用和禁用。 * 启用状态下,该事件才处于可见状态,可被触发、交互与处理。 * 禁用状态下,该事件几乎相当于不存在,不可见、不可被触发、不可交互,只能通过“插入事件”指令远程调用。 所有事件默认情况下初始都是启用的,只有普通事件可以通过不勾选“启用”来初始隐藏。 在事件列表中使用“显示事件”和“隐藏事件”可以将一个禁用事件给启用,或将一个启用事件给禁用,甚至直接从地图中删除。 **【重要警告】:** 1. 打怪开门捡道具后,如果有对应的坐标,则该点的事件会被从地图中删除(重生怪除外,它只会被暂时隐藏)。然后会尝试执行该点的战后事件、开门后事件或(拾获)道具后事件(您可以勾选“轻按时不触发”)。如果执行,则“当前点坐标”(core.status.event.data的坐标)也会变为该点。 * 所以,这三个XX后事件,其实是有可能被多次触发或意外触发的(如打死或炸掉某个有战后事件的怪,推了个阻击怪过去打死),请自行注意。 2. “移动事件”(如阻击和捕捉)和“跳跃事件”(如支援)会将起点处的事件从地图中删除,如果不勾选“不消失”(如阻击),则会在终点处“转变图块”并“显示事件”。但事件的内容不会被跟着移动到终点,推箱子同理,但在终点只转变图块而不予显示。 ### 楼梯、传送门事件 ![image](img/changefloor.jpg) 当您在地图上绘制楼梯、或绘制四种三帧箭头并右击绑定后,就创建了一个“楼层转换”事件,您可以在事件编辑器右侧看到一行代码(json),请注意对照。 此事件与`core.changeFloor(floorId, stair, heroLoc, time, callback)`函数相对应,只是多了一项“穿透性”。 每个这样的事件都是上图的写法,下面逐一讲解其各项的含义和用法: 1. **目标楼层(floorId):**如题,如果选择了“楼层ID”就需要在第二个框中输入目标楼层的ID(会自动提示补全,也可以双击整个紫色块从地图选点)。如果选择了其他三种,则json代码中`:before`、`:next`、`:now`分别表示“前一楼”、“后一楼”(顺序由“全塔属性——楼层列表”指定,上下楼器同理)和“当前楼”,函数中也是一样的写法。 2. **目标位置(stair):**如果选择了“坐标”则需要在后面两个框里输入(同样可以地图选点,在函数中则将`stair`填`null`并填写`heroLoc`),也可以选择下拉框中的其他几项(在函数中则将`heroLoc`填`null`),如保持不变(`:now`)、中心对称(`:symmetry`)、左右对称(`:symmetry_x`)、上下对称(`:symmetry_y`)或任何一个图块ID. * 填写三种对称位置时请注意,坐标计算以当前楼层而不是目标楼层为准,尤其要注意防止勇士出界。 * 填写图块ID时请注意,“上下楼梯”提供在了下拉框中,实际传送时会优先尝试取用目标层的“楼层属性——上下楼点”。其次尝试像其他ID一样从目标楼层搜索,因此请确保该图块在目标楼层中存在且唯一。 3. **朝向:**有八种写法,可以直接填写改变后的朝向(`up, down, left, right`),也可以填写转身的角度(`:left, :right, :back`)或不变。 4. **动画时间:**指黑屏的毫秒数,可以填0或不小于100的整数,不填则使用玩家设定值。 5. **穿透性:**见[系统开关](start)允许穿透楼梯。 值得注意的是,绑定了楼层转换事件的门、怪物、道具、箱子,其系统行为(开门、战斗、拾取、推行)将不再发生(阻激夹域等地图属性不受影响),而是被覆盖为上下楼。 ## 普通事件(红) 普通事件在启用后可以被勇士碰触而触发,它们都有着这样的开头: `覆盖触发器(Y/N) 启用(Y/N) 通行状态(Y/N?) 显伤(Y/N)` 1. **覆盖触发器:**如前所述,门、怪物、道具、箱子都已经被绑定了系统触发器。如果您需要让地图上的个别这类图块在被勇士碰触时不发生系统行为(开门、战斗、拾取、推行),而是执行您在下面自定义的事件(比如触碰boss先进行一段对话再强制战斗),请勾选此项。(阻激夹域等地图属性不受影响) 2. **启用:**如果不勾选此项,则此事件初始时是隐藏的。 * 如前所述,非常不推荐仅仅为了初始隐藏一些图块(如战利品、陷阱门墙、埋伏怪等)就去绑定一堆没有指令的普通事件。 * 更安全的做法是“从0转变图块”(支持淡入效果)和“关门”(这样还有音效,多香)。 3. **通行状态:**除怪物和道具外,所有图块本身都具有“可通行性”属性。 * 您可以设置此项去覆盖地图中这一点的可通行性,譬如强行让一格空地不可通行,勇士撞击时出现一堵墙(51层魔塔第23层的效果)。 4. **显伤:**在对怪物使用覆盖触发器后,表面上看不出它和一般怪物的不同。 * 如有必要(比如触碰这怪物根本不战斗),可不勾选显伤以示区别。 * 如数量较多,可注册一个相同素材的NPC,比如样板中的仙子。 * 如需在一段剧情中关闭所有显伤,可以简单移除怪物手册 ## 自动事件(橙) 在“右键绑定机关门”中,我们已经初见了自动事件的端倪。 在游戏中,我们经常需要监听大量乱七八糟的状态,根据状态的变化去做出及时的处理,比如勇士生命小于等于0会触发游戏失败。比如51层魔塔第39层监听9扇黄门的状态来触发中心对称飞行器的出现、第49层监听8名魔法警卫的状态来封印假魔王。 如果给9扇黄门都绑定类似的开门后事件、给8名警卫都绑定类似的战后事件,就非常繁琐。 而自动事件则不然,它不需要您去主动考虑所有“改变状态”的原因,而是简单地在每次刷新状态栏时,检查是否满足触发条件,满足的话就执行。 每个自动事件具有以下几个属性: 1. **触发条件:**自动事件最重要的属性,为支持冒号缩写量的逻辑表达式。 * 譬如上述两个例子中,触发条件就是“某两个点没有图块而某7个点图块为黄门”和“某四个点没有图块而某四个点图块为魔法警卫”。 * 关于冒号缩写量,详见[只读块]值块和冒号缩写量。 2. **优先级:**一般不需要改动。当多个自动事件的条件同时满足时,优先执行此级别较大的。 * 此级别也相等时,允许跨层触发则优先执行楼层较矮的。 * 同一楼层优先执行横坐标较小的,横坐标相等时优先执行纵坐标较小的,同一点处优先执行页码较小的。 3. **仅在本层检测:**如题,一般不需要改动。除非您要制作一些全局的效果,如“勇士生命低于上限的1/3时改变背景音乐”。 4. **事件流中延迟执行:**勾选此项后,在执行其他事件时触发此自动事件则会将其内容追加到待执行事件的末尾,否则插队到待执行事件的开头。 5. **允许多次执行:**如果不勾选,则此事件最多只会被触发一次。即使勾选了,每次执行的过程中也会暂时禁止自己被触发。勾选后,请记得在此事件的指令中将条件变得不满足。 每个点初始提供了两个自动事件页,您还可以再在数据区最上面单击“添加自动事件页”。 ## 公共事件(逗号键) 有几个事件,如战后加点、毒衰咒处理、回收钥匙商店,会在战后脚本或全局商店等处频繁用到。于是它们被整理成了公共事件,您也可以追加自己的。 公共事件在内容上和其他事件并无不同,只是在插入公共事件时(如`core.insertCommonEvent("回收钥匙商店")`)可以提供一个`参数列表`。 参数列表是一个一维数组,其每项会被依次代入`core.status.hero.flags.arg1、arg2、……`,而`arg0`则会记录公共事件的中文名。 在整个事件流结束后(即勇士恢复行动,请注意不是公共事件结束后),这些以arg加纯数字命名的变量、以及以@temp开头的临时变量都会被清空。 ## 滑冰事件 滑冰(ski)是一个非常特殊的触发器,使用此触发器的图块需要画在背景层。 冰上可以画门、怪物、道具等任何图块,您还可以在前景层画上四种单向通行箭头(样板1层右下方那些),把三个图层的游戏性都发挥出来。 勇士走上冰面后,将一直“前进一格或撞击”。直到滑出冰面或无法再前进,如撞到墙或事件。 比如撞到怪物会强制战斗,打不过直接游戏失败。走到道具上会捡起来,撞到门则要看有没有钥匙,有的话会把门打开。 默认情况下,触发事件后勇士会停下来。如果要继续滑冰,可以在这些点的战后事件、开门后事件、(拾获)道具后事件(这里最好勾选“轻按时不触发”)中判断勇士还在不在冰上(`core.onSki()`函数),在冰上的话就命令“勇士前进一格或撞击”。 ## 事件编辑器 ![image](img/blockly.jpg) 当您从数据区单击任何一个事件类属性的“编辑”按钮时,就会呼出事件编辑器,上图给出了事件编辑器的全貌。 左下角为指令类别,可以点击它们呼出各类指令菜单,如左上方的值块菜单。 中央为指令区,被一个标识事件类型的紫色块包覆(即左下角的“入口方块”,图中为开门后事件),所有的指令都塞在里面。 右上角为json区,会随着指令区的编辑而自动变化。`core.insertAction()`函数的自变量就是它们,所以学有余力的话也建议您经常左右对照去了解每个指令对应的json写法。 右下角为调色器,当您单击指令块中的颜色按钮时(如图中“画面闪烁”指令的白块)就会自动呼出,可以进行诸如十进制和十六进制颜色格式互转等操作。 左上角为功能区,各功能的含义和用法如下: 1. **确认(Ctrl+S):**将指令区的指令组翻译为json写回表格和文件,并关闭事件编辑器。如果手动修改了json区却没有点击变黄的解析按钮,或指令区存在异步事件没有用“等待所有异步事件执行完毕”(图中第二条褐色指令)去等待,则单击确认按钮会弹窗警告。此时如果想放弃对json区的修改,请随便点一下指令区的一个块即可。 2. **解析:**将手动修改后的json区翻译回指令组同步到指令区,一般用于跨作品复制事件,或长距离批量挪动和复制指令。因为blockly使用Ctrl+V粘贴出的块总是出现在原块的旁边,而拖动块时块又跟不上滚轮。 3. **取消:**放弃本次编辑,关闭事件编辑器,表格和文件会停留在打开事件编辑器前的状态。 4. **搜索事件块:**当您想不起来需要的指令在哪个类别时,请使用此项,如输入“勇”字就会列出所有带有“勇士”一词的指令。 5. **地图选点:**在编辑事件尤其是大篇幅事件的过程中,如果需要频繁保存并退出事件编辑器去浏览地图尤其是浏览别的楼层就很不方便。单击此按钮,您可以浏览所有楼层及其全景。通过点击地图去确认一个点的坐标,并复制任何一个楼层ID以填入需要的地方。需要填坐标的指令也可以直接双击其指令块从地图选点填入坐标和楼层,从而避免了肉眼数格子和手敲的麻烦和出错的隐患。 6. **变量出现位置搜索:**您可以搜索所有以“变量:xxx”形式出现在事件中的变量的出现位置,请注意“形式”和“事件”。以下形式都是无法被搜索到的(比如仇恨值、阻激夹域捕的禁用开关): 7. **开启中文名替换:**勾选此项后,“flag、status、item、switch、temp、global、enemy”等冒号缩写量的前缀以及怪物和道具的ID才会允许用中文在指令块中书写,如“变量、状态、物品、独立开关、临时变量、全局存储、怪物”。当然,json中还是只能用英文的。此外,如果您需要使用中文名相同的道具或怪物,或在`${}`以外的文本中将上述几个词和冒号连用,则也需要关闭此功能以避免blockly和json互译出错。 8. **展开值块逻辑运算:**在值块菜单中我们可以看到第一个就是二元运算块,但是它并不完整。比如没有位运算(包括逻辑异或)和取余,因此如果解析的优先级与js的优先级不一致,请在表达式中适当的位置添加圆括弧,或取消勾选此项。 ## 值块和冒号缩写量 值块并不能单独构成指令,但它们可以被嵌入数值操作、设置怪物属性和很多事件控制类指令。而冒号缩写量的用法就更多了,比如用于`${表达式计算}`和其他任何支持填表达式的地方。 值块大体上分为三种:勾选框与运算块、只读块、可读写块,其中后两类对应着冒号缩写量。 ### 勾选框与运算块(附js比较运算大图鉴) 包括勾选框块(true和false两个逻辑常量)、否定运算块和二元运算块。 否定运算块(一个“非”字,脚本中写作一个叹号)会把“false、0、null、undefined、NaN和空字符串”(以下简称广义false)变为true,而把别的量都变为false. 二元运算块包括四则运算、取余、乘方、比较运算和三种逻辑运算。 四则运算中,两个整数相除会得到小数。两个0的乘方会得到1(数学上无意义) 八种比较运算中,四种比大小的运算如果两项中有一项是数字,就会把另一项也转换成数字去比,所以会出现这些: `'2' < 10; 10 <= '10'; '10' < 2; null >= 0; null <= 0; 1/0 > 1/-0` ![image](img/compare.jpg) 弱(不)等于(==和!=)非常难用,建议放弃,统一使用(不)等于(===和!==)。 NaN一般是0/0或字符串瞎做减乘除得到的,它跟谁都没有可比性,包括自己。 纯数字字符串(包括十六进制)和对应的数字,都是弱相等关系。如`'0x10' == 16` 数组和对象,跟自己都是既大于等于又小于等于的关系(因为不是同一个)。 单个数字(或什么都没有)用方括号还是引号括起来,对外的比较行为都一致。 undefined倒是不用担心,它除了和null弱相等外,和其他值都没有可比性。 三种逻辑运算的原则是,“且”的优先级高于“或”,它俩都不满足交换律。 若干个由“且”连起来的量中,取第一个广义false(没有则取最后一个量), 若干个由“或”连起来的量中,取第一个不是广义false的量(没有则同上), 所以样板中充斥着形如x=x||y和z=x||y的代码,y算是x的默认值。 若干个由“异或”连起来的true或false,总结果取决于其中true的个数的奇偶,奇数个true则总结果为1,偶数个true则总结果为0. ### 只读块(怪物属性、图块ID、图块类别、装备孔) 尽管这几个值块是只读的,但您仍然可以使用“设置怪物属性”、“穿脱装备”和“转变图块”等指令去改变它们。 1. **怪物属性:**冒号缩写量写作`怪物:绿头怪:生命`,json代码中写作`enemy:greenSlime:hp`。怪物中文名有重复的话请在事件编辑器顶部关闭中文替换功能,道具中文名同理。 * 常见的怪物属性都提供在了下拉框中,如果下拉框里没有(如通过配置表格新增的属性),可以修改`_server\MotaAction.g4`文件最后的那些表来追加其中文简称,也可以放弃解析回中文块而是就用缩写量。 2. **图块ID:**冒号缩写量写作`图块ID:x,y`,json代码中写作`blockId:x,y`。请注意逗号始终要用英文的,此值块实际直接调用的API为`core.getBlockId(x,y)`,即本值块和对应的冒号缩写量只支持本层,可以获知本层某个点的图块ID. 3. **图块类别:**冒号缩写量写作`图块类别:x,y`,json代码中写作`blockCls:x,y`。同上,实际调用`core.getBlockCls(x,y)`,可以获知本层某个点的图块类别。 4. **装备孔:**冒号缩写量写作`装备孔:n`,json代码中写作`equip:n`。实际调用`core.getEquip(n)`,可以获知勇士第n个装备孔中的装备ID. ### 可读写块(status、item、flag、switch、temp、global) 比起只读块,可读写块允许作为“数值操作”指令的左块: 1. **状态:**冒号缩写量为`状态:生命`等,json代码中写作`status:hp`等。 * 作为左块时会调用`core.setStatus()`,其他时候会调用`core.getStatus()` 2. **物品:**冒号缩写量为`物品:炸弹`等,json代码中写作`item:bomb`等 * 作为左块时会调用`core.getItem()`或`core.setItem()`,其他时候会调用`core.itemCount()`来统计背包中的道具数量。 * 此外,最常用的9个勇士状态和三色钥匙提供在了下拉框中。您可以修改`_server/MotaAction.g4`文件最后的`FixedId_List`来追加,向“楼层转换”、“门信息”和“装备属性”的下拉框追加新的楼梯ID、钥匙ID和勇士状态名也是一样的道理,当然不追加也不影响手写。 3. **变量:**冒号缩写量为`变量:hatred`等,json代码中写作`flag:hatred`等。 * 作为左块时会调用`core.setFlag()`,其他时候会调用`core.getFlag(’xxx’, 0)` * 请注意:变量类型支持中文,如`变量:开门个数` * 这两个API实际操作的就是前文多次提到的`core.status.hero.flags`,只不过在事件中未定义的变量都视为0,更为安全。 * 如果您忘记了自己在哪些事件用过哪些变量,请善用事件编辑器顶部的“搜索变量出现位置”。 4. **独立开关:**如果您有一大批NPC都具有“首次对话不同于之后各次对话”之类的特点,那么为他们设置一大批不重名的变量就很繁琐。独立开关(叫独立变量更准确)就是为这种场合而生的,它对每层楼(首次到达和每次到达中)和每个坐标都是独立的。 * 冒号缩写量写作`独立开关:A—Z`,json代码中写作`switch:A—Z` * 每个坐标处的独立开关有26个,用大写拉丁字母A—Z表示。MTn层(x,y)点的独立开关A,本质上是一个flag变量——`flag:MTn@x@y@A`,如果需要在其他点访问,就需要用这样的写法 5. **临时变量:**冒号缩写量写作“临时变量:A—Z”,json代码中写作`temp:A-Z`, * 临时变量泛指所有以`@temp@`开头的flag变量,一共也有26个。 * 临时变量一般用于计数循环和数组迭代,每当事件流结束(勇士恢复行动)时,临时变量和参数变量(以arg+纯数字命名)都会被清空。 6. **全局存储:**冒号缩写量写作`全局存储:xxx`,json代码中写作`global:xxx`, * 和变量一样,冒号右侧本身支持中文。全局存储一般用来保存一些跨存档的信息,如成就系统、回想和音像鉴赏的收集进度、多周目数据等。 * 用于左块时会调用`core.setGlobal()`,其他时候会调用`core.getGlobal()` ========================================================================================== [继续阅读下一章:事件指令](instruction)