游戏中事件可分为两种,SDL事件和游戏事件。SDL事件指由SDL系统产生的事件,像鼠标左键被按下,键盘R键被按下。游戏事件指玩游戏时产生的事件,像攻战了某一城市,某一部队被消灭。[event]块对应的是游戏事件。
一、处理事件顶层逻辑
程序(C++)内部有个可处理事件集,它定义了游戏支持的所有事件类型,具体是事件的触发时机、类型名称。MOD关卡(WML)内部有个可处理事件集,它定义了此个关卡关心的事件名称、触发条件、以及该事件一旦被触发执行的操作。玩家在玩游戏过程中满足触发时机时,C++向事件处理模块(程序内一个模块)抛出该事件,后者发现正在玩的关卡是关注该事件并满足触发条件,于是就调用关卡中定义的操作。
以刘备传之长坂坡之战中两个事件来进一步理解处理事件逻辑。(WML代码见<kingdom-src>/data/campaigns/Legend_of_Bei_Liu/scenarios/01_changbanpo.cfg)
事件attack_end的WML代码
[event] name=attack_end first_time_only=no [filter] side=3 [/filter] [filter_second] side=1 hp=yes master_hero=50 [/filter_second] [message] speaker=43 message= _ "My ability can not compare with Cao Cao, I surrender." [/message] [kill] master_hero=50 a_side=3 animate=yes [/kill] [/event]
C++程序约定一旦攻击结束后会产生attack_end事件,即该事件触发时机是攻击结束后、名称是attack_end。MOD关卡定义了一个name=attack_end的[event]块(见以上WML代码),它表示此个关卡关心attack_end事件,但要执行操作还要满足(即触发条件)攻击方是属于曹操势力(side=3),防御方是属于刘琮势力(side=1)、并且防御单位主将是襄阳(master_hero=50)、攻击后襄阳没被催垮(hp=yes),而一旦满足条件后将执行两个动作,第一个动作是弹出一个消息框([message]),框中内容是刘琮(speaker=43)说“我的才能不及曹操,愿献城投降”,第二个动作是襄阳改为归属攻击方([kill]),即曹操势力。
事件comeinto的WML代码
[event] name=comeinto [filter] must_heros = 216 [/filter] [filter_second] must_heros = 4 [/filter_second] [sideheros] side=2 heros=0,4,5,10,31,32,40,42,58,59,60,61,80,81,82,162,209,213 [/sideheros] [endlevel] result=victory [/endlevel] [/event]
C++程序约定一旦单位(一般是部队)进城后会产生comeinto事件,即该事件触发时机是单位进城后、名称是comeinto。MOD关卡定义了一个name=comeinto的[event]块(见以上WML代码),它表示此个关卡关心comeinto事件,但要执行操作还要满足(即触发条件)进入砦这个城市(must_heros=216)、进入的部队中有刘备这个武将(must_heros = 4),而一旦满足条件后将执行两个动作。第一个动作是调整刘备势力武将([sideheros]),把heros字段指定的武将归属刘备势力,而不是的武将则让退出,第二个动作是触发关卡结束([endlevel]),result=victory表示玩家胜利通过本关卡。
二、触发条件
在关卡中定义一个事件,即写了一个event块,什么条件下该事件会被触发?——这个触发条件归纳起来有两条(需要同时满足):C++代码触发了事件、满足[event]中写的条件。
- 条件一:C++代码触发了事件。
像攻击结束后会抛出attack_end事件,这个触发条件更准确说是定义了时机,它是由C++代码内定的,编写MOD时不能改它。 - 条件二:满足[event]中写的条件。
具体说是[event]块中的[filter]、[filter_second]块。为什么要补上这个条件呢?——例如attack_end事件,对于C++代码来说只要游戏攻击操作一结束它反正会抛出attack_end,从而进入关卡中定义的name=attack_end的[event]块,而攻击这个操作在游戏中经常发生,关卡要能让执行“私自”操作应该有更强条件,[filter]、[filter_second]用于补上这个更强条件。
[event]可自写条件,这让同一个关卡能产生同名却不同操作事件。例如对于以上的comeinto事件,已写的comeinto是刘备队进入砦后就让胜利通过本关卡,假如还要支持一个事件:一旦曹操队进入襄阳,就让曹操势力所有部队补满HP,要支持它也是通过写一个name=comeinto的[event]块,只是当中条件、操作不同罢了。
让深入说下C++代码。C++代码对关卡关注的事件有个数组((game_events.cpp)event_handlers),数组内每个元素对应一个[event]块(对于相同name字段那也是不同[evnet]块),一旦触发一个事件,像攻击结束满足触发attack_end时机,它就根据这个事件名以及当前上下文从头到尾去逐个“处理”数组中的[event]。“处理”包括匹配判断和执行操作,匹配判断包括两个步骤,一个是name要一致,二是[event]中条件要满足,如果匹配就执行“处理”的第二步,即执行该[event]中定义的操作;否则不执行定义的操作,继续数组中的下一个[event]块(当然,即使此次匹配满足并且执行了操作,它还是会继续下一个的)。
说说first_time_only字段。在attack_end、comeinto两个[event]中都看到了first_time_only=no,引入first_time_only是为了提高C++处理事件效率。从以上C++代码如何处理事件可以看出,C++代码一旦时机一到抛出一事件了,它就在关卡注册的[event]数组中找,[b]这个找是要整数组逐个元素地处理![/b]针对这种逐个处理方式让考虑种情况,假如有那么个事件它是只要处理过一次,此个关卡就不需要再处理了,那为了提高效率可以处理该事件后就把该[event]块从数组中删除,接下逐个处理时就可以少去这个已是肯定冗余的操作。first_time_only就用于这个目的,first_time_only=yes时表示这是一个一次性事件,处理过一次就可以从数组中删除了,no则指示这是个重复性事件,first_time_only默认值是yes,即[event]块没有first_time_only时表示它是一个一次性事件。
明确下first_time_only中的执行概念,它的一次执行是指满足两条件后执行那些个操作,像comeinto中的[sideheros]、[endlevel],不是C++只要抛出name对应的事件就算执行。
事件上下文
知道了条件包括时机和[event]块中写的条件,对于两条件中时机较好理解,C++程序会给出这个时机,那[event]块中条件该怎么写?很显然,这个条件往往是和当前状态相关,像attack_end,它往往和攻击方、防御方相关,要能让关卡编写者写出“好”条件就应该有办法让他们知道事件产生时攻击方和防御方细节(更进一步的话还有攻击时采用的战法等),即事件触发时环境,称之为事件上下文。对attack_end上下文是攻击方、防御方,换到comeinto就是进入到城市、进入的部队,而这个上下文由谁提供,无疑是C++代码,C++代码得有一种方法让关卡编写者访问到事件上下文。
- C++代码向关卡编写者提供的访问上下文方法是让在[event]内写[filter]、[second_filter]块,在块中只要使用约定字段,就可充当上下文判断条件。
- 不论是[filter]还是[second_filter],它们表示一个单位,这个单位可能是部队,可能是城市,可能是建筑物,块内一个字段名表示了该单位一种属性,而一条“字段名=值”语句表示了一个判断。像exist_reside_troop=4语句,exist_reside_troop是表示城内部队属性,整条语句则表示了城市要存在有武将是刘备的部队。
- 虽然[filter]、[second_filter]都表示单位,但不同事件自定义[filter]、[second_filter]意义。像attack_end,[filter]表示攻击部队,[second_filter]表示防御部队,对comeinto,[filter]表示进入到的城市,[second_filter]则无定义。
由以上C++对事件上下文处理可看出,编写关卡时要利用上下文写“好”条件,就需要知道各个事件下C++如何定义[filter]、[second_filter]内容,以及[filter]/[sencond_filter]内容许出现的字段。对前一部分,不同事件时都可说不同的,对后一部分,即[filter]/[sencond_filter]内容许出现的字段,由于两个块都表示单位,对不同事件还是较有统一性的。以下是各字段小结。
字段名 | 值(默认值)[空值]注1 | |
hp | yes/no(无要求)[no] | yes时要求单位hp>0,no时则无所谓 |
must_heros | 以“,”组合的武将编号(无要求)[无要求] | 单位中须存在编号中的武将 |
speaker | 等同master_hero,将废弃 | |
last_city | yes/no(无要求) | 一旦存在,要求单位须是城市。yes时要求它是该势力最后一个城市 |
exist_reside_troop | 武将编号(无要求) | 一旦存在,要求单位须是城市。城市存在武将(不必一定是主将)是武将编号的部队 |
id | 字符串(无要求) | 将废弃 |
filter_location | WML块 | 单位所在地形 |
x | x坐标范围(无要求) | 单位所在x坐标满足坐标范围 |
y | y坐标范围(无要求) | 单位所在y坐标满足坐标范围 |
type | 字符串(无要求) | 一旦存在,须是有效的兵种。单位须是字符串指定的兵种 |
ability | 以“,”组合的字符串(无要求) | 一旦存在,须是单位技能标识。单位须有字符串指定指能中的一种 |
race | 字符串(无要求) | 单位须是字符串标识的种族 |
gender | 字符串(无要求) | 单位须是字符串标识的性别 |
side | 以“,”组合的势力编号(无要求) | 单位所属势力需在编号中 |
has_weapon | 以“,”组合的战法名称(无要求) | 单位拥有组合中的一种战法 |
role | 将废弃 | |
ai_special | 将废弃 | |
canrecruit | 将废弃 | |
level | 数值(无要求)[-1] | 单位等级须是数值 |
defense | 数值(无要求)[-1] | 单位在该格子上防御力须等于数值 |
movement_cost | 数值(无要求)[-1] | 单位在该格子上消耗移动力须等于数值 |
filter_wml | WML块 | |
filter_vision | WML块 | |
filter_adjacent | WML块 | |
find_in | 将废弃 | |
formula | 将废弃 | |
lua_function | 将废弃 |
注:
1:默认值指的是[filter]、[second_filter]块不存在该字段时C++会赋给的值,空值指的是存在该字段但把该字段等于空值时C++会赋给的值
三、[store_unit]
功能:把单位存进一个变量。
- [filter]:滤波块。滤坡块是个标准unit判断器,外加cityno、index字段。cityno不存在时,index字段被忽略,这时要存的单位是城外部队、城市或建筑物,[filter]作为标准unit判断器。cityno存在时(可能是无效城市),这时要存的单位是城内部队,index是城内部队索引,此时cityno、index必须有效,否则此次[store_unit]将存不下单位。
- variable:单位要存到的变量。如果不设置默认存去unit。
- mode:当系统中已存在要设置到的变量时,指示如何处理该原变量。always_clear(默认):清除变量。replace:不清除原变量,新清量覆盖原变量,如果原变量是个数组,新变量从索引0开始覆盖,对两变量对应同一个单元原变量中有而新变量中没有的字段将被保留。append:不清除变量,新变量添加在原变量尾部。
当前状态下如果满足filter的单位能有多个,那么这些个单位就会[b]都[/b]被存进变量,它们以数组形式被引用。对于要引用,其实都应该是数组形式,只是当只存有一个单位时,固定用索引0而已。
注:store_unit一旦存下单位,它会在lua子系统生成一个unit对象。这个unit对象要等到lua退出时才释放(play_controller析构函数中),也就是说删除变量不会释入这个unit对象。
[store_unit] [filter] cityno = 1 index = 0 [/filter] variable = reside [/store_unit] {TARGET_TYPE $reside[0].type}
此个[store_unit]作用是把cityno=1这个城市的第一只城内部队存入reside这个变量。如果之前已有reside变量,首先清除(mode=always_clear)。cityno=1城市中有城内部队的话,reside变量会存也只会存一只部队,但在引用时需使用$reside[0]这种数组格式。
clear_value可以删除多个单位的数组变量。
四、触发后执行的操作
回看下attack_end、comeinto事件,一旦满足条件后,attack_end会执行两个操作,显示消息框([message])、城市归属到曹操势力([kill]),comeinto则是调整刘备势力内武将([sideheros])、触发通过本关卡([endlevel])。当然,只要符合自个关卡设计要求,你也可以把[sideheros]、[endlevel]中的一个或两个写入attack_end。对触发后执行的操作,C++(严格说还有lua,参考底下“lua”)定义了可使用的操作原子,像[message]、[kill]、[sideheros]、[endlevel],关卡编写者按自个要求使用原子,游戏不限制一个事件执行的操作可执行哪些原子、多少个原子。
一个操作原子对应一个WML块,C++程序内定了块叫什么(即操作原子叫什么)、执行什么功能、块中可存在哪些字段、以及字段可取什么值。块中存在的字段叫操作原子参数,对每种操作它会自定义参数。C++内定操作有不少,将来会不断补充。
lua
注:以下要叙内容涉及到lua、还需要理解游戏中处理事件的C++代码,相对来说较为难懂,对普通关卡编写者来说可不去理解此个部分(不理解也已能写[event]块),对要理解此中内容的请结合“[url=http://www.freeors.com/bbs/forum.php?mod=viewthread&tid=21213&extra=page%3D1]第三章 WML和Lua[/url]”中的“1.4 C和Lua相互调用”。
游戏为让编写MOD更灵活,可使用lua编写事件,在处理事件方面,lua代码主要实现两个功能:执行事件通用逻辑、实现部分操作原子的私有逻辑。
- 功能一:执行事件通用逻辑。
通用逻辑,故名思意是对于这些操作原子,这个处理逻辑是一样的。像如何逐个调起事件中的操作原子。 - 功能二:实现部分操作原子的私有逻辑。
私有处理就是该操作自己的处理。像[objectives]这个操作原子是实现设置势力在此关卡的胜利、失败条件。
实现操作原子私有逻辑,有些是全由C++完成,有些是lua、C++共同完成(这里C++实现的是库代码功能,它被lua调用,因而往往说是lua完成了操作原子的私有逻辑)。哪些原子是C++完成,哪些是lua完成?
- 识别该原子操作是谁完成看该操作是否出现在lua脚本中,如果出现在lua脚本,由lua完成,否则是C++代码完成。
- lua实现原子操作标志是实现了wml_actions["<name>"]或wml_actions.<name>,像wml_actions["if"],wml_actions.objectives。
- C++实现原子操作标志是代码中出现类似WML_HANDLER_FUNCTION(<name>, ev, cfg),像WML_HANDLER_FUNCTION(unit, ev, cfg),WML_HANDLER_FUNCTION(sideheros, ev, cfg)。
- 如果两处都实现了,lua优先级高于C++。
- 不管在何处实现,lua都要执行通用逻辑,像要调起命令都要通过lua全局表“wesnoth”下的“wml_actions”子表,wml_actions子表下的command项,都要执行wml-tags.lua中的handle_event_commands函数
由以上分析可看出,通过写lua可以改变已有操作原子的功能、参数,也可增加操作原子。但要通过这种方法修改操作原子时,须要足够了解lua以及lua可调用的、本游戏实现的C++库函数。
事件 | 时机 | [filter] | [filter_second] | [filter_hero] |
attack_end | 攻击后*1 | 攻击单位 | 防御单位 | -- |
last_breath | 攻击中出现一单位死亡*2 | 死亡单位 | 攻击单位 | -- |
moveto | 移动后*3 | 移动的部队 | -- | -- |
comeinto | 回城后*4 | 回到的城市 | 回城的部队 | -- |
post_due | 单挑结束后*5 | 左侧武将所在部队 | 右侧武将所在部队 | -- |
注1。攻击后是整次攻击的攻击后。此次攻击造成一方死亡时也会触发attack_end。[filter]、[filter_second]中unit的hp是攻击后的值,当出现死亡时,对应单位hp<=0。攻击方使用移格攻击时,[filter]、[filter_second]是移格前的位置。
注2。触发attack_end后才触发last_breath。触发last_breath时不会判断[filter]、[filter_second]有效性,在attack_end中慎用[kill]。攻击方用环攻时可能会造成多个对方死亡,只有防御单位的死亡才会触发attack_end,但所有其它的死亡都会触发last_breath。
注3。moveto发送时机是在部队从起点移动到终点后。[filter]中的unit已位在终点。
注4。[filter_second]中的回城部队已在城市中,属于城内部队。
注5。单挑一定会有玩家参与。左侧是玩家能控制武将,左侧武将可能不是主动触发单挑的武将,像AI攻击玩家而触发的单挑。在编写MOD时,post_dule过程中会生效一个config变量:duel。
[duel] left:整数值。左侧武将编号。 right:整数值。右侧武将编号。 percentage:整数值,[0, 100]。以左侧武将视角计算的单挑结果。左侧武将被打下马时,值是0;右侧武将被打下马时,值是100,50指示是平局。 [/duel]