关于事件处理模块的设计(作者:许畅)

一个完整的RPG 游戏应该具有一个专门的事件处理模块,该模块能实现事件之间各种复杂的逻辑关系,确保故事逻辑的正常发展,并有一定的容错功能。

初步设想是,在游戏的运行过程中同时存在着许多事件Event ,每个事件的发展有不同阶段,我们当时称之为进程Process (这个名词与操作系统中的“进程”意义不同,是状态、阶段的意思),事件的发展表现为进程的推进,这里有些状态机的味道。同时事件是依附于事件主体Event_Body上的,事件主体一般是指人物或动物等生命体,如李逍遥、草怪等,但有时是特殊的物品,如装有蜂王蜜的木桶、用于切换场景的砖块等。

举个例子来说,有一个看不见砖块,放在从村子去菜场的入口,开始的时候,不允许李忆如去菜场,只要李忆如一踩上那砖块,刘晋元就说:“小妹妹,你家不是在村里么?”,同时李忆如反弹一步;当李忆如已经带刘晋元去见过李逍遥后,那个砖块的作用已变,只要一踩上去,立刻把场景由村子切换为菜场。在这个例子中事件主体是那个看不见的砖块,砖块上依附的事件是一个抽象的概念,但我们能够感觉得到,这个事件的发展有两个阶段,刚开始事件处于第一阶段,或称为初始状态,后来随着情节的发展,某个其它事件激发了这个事件,使其变为第二阶段,或称为下一个状态,我们当时称之为进程推进了。

图示:

事件A:进程0――进程1――进程2――......

进一步考虑事件Event的每个进程Process的属性,可分为三个部分:条件Condition、内容EventId、影响Effect。条件满足的事件进程才可执行其内容,接着产生其影响。条件不满足,事件的进程无法推进。

图示:

事件进程B:[条件―内容―影响]

一. 条件分为三类:键盘触发条件、情节条件、特殊条件。

1. 键盘触发条件的引入是出于对何类键盘消息响应的考虑,一般情况下,我们要和某个人对话,是控制主人公走到那个人面前,然后按下空格键,这占了百分之八十以上的情况,我们称之为“空格键触发”;有时我们想进屋子,假如门是开的,只要按着方向键不放,就能自动切换场景,这时要按空格键是多此一举,我们称之为“方向键触发”;还有一种情况,不需要按下任何键事件就能被触发,是“自动触发”,这种特殊的触发为了便于实现,我们将其移入影响中,换名为“虚触发”,如我们要把大段的对话、描述、动画等连起来播放就要用到“虚触发”,一个典型的例子是《仙剑新编》开始的一大段序幕。

2. 情节条件就和故事内容紧密相关了,我们给三个简单的例子:

a.要与A说话,必须先与B说过话。

b.要与A说话,必须先与B和C说过话。

c.要与A说话,必须先与B或C说过话。

注意上面例子b与c的差别。

例子a说明A身上的事件的当前进程只有一个情节条件;例子b说明A身上的事件的当前进程有两个为“与”关系的情节条件;例子c 说明A身上的事件的当前进程有两个为“或”关系的情节条件。

在实现上,我们给每个事件的每个进程分配一个不定长的情节条件链,链上的节点之间为“与”关系。

图示:

进程a的情节条件:Ca1

进程b的情节条件:Cb1、Cb2

进程c的情节条件:Cc1

例子a中,与B说过话后,产生一个影响:删除Ca1。

例子b中,与B说过话后,产生一个影响:删除Cb1;与C说过话后,产生一个影响:删除Cb2。

例子c中,与B说过话后,产生一个影响:删除Cc1;与C说过话后,产生一个影响:删除Cc1。

(通过删除同一个情节条件节点来实现“或”关系)

可以看出,通过情节条件和影响(这里实际是情节影响)的各种组合可以实现更复杂一些的逻辑关系。

这里要顺便提一下在程序实现中进程是如何推进的,我们仅使用情节条件和情节影响来说明(情节影响在后面还要细说,这里只要知道它是和情节条件配合使用的即可)。我们使用一个例子:

村子里有两个人:小兰和大虎,如果我们先和大虎对话,大虎会说:“今天天气不错。”,并且反复对话反复重复;如果我们先和小兰对话,小兰会说:“不知道大虎哥今天会到哪儿去?”,并且反复对话反复重复。但是我们先和小兰对话后在去找大虎对话,大虎就会说:“今天我想去打猎。”

我们来考虑这个例子的实现。

首先设计好对话资源,并分配资源号Id:

对话1.“今天天气不错。”

对话2.“今天我想去打猎。”

对话3.“不知道大虎哥今天会到哪儿去?”

其次设计好人物资源,并分配资源号Id:

人物1. 大虎

人物2. 小兰

然后设计好各事件的每一进程的原因、内容、影响,为说明问题起见,我们作一些简化,原因只有情节原因,内容只有对话,影响只有情节影响,并对每一进程分配资源号Id:

进程1. ――分配给大虎

Condition:无

EventId:对话1

Effect:无

进程2. ――分配给大虎

Condition:(2,3)――意义:小兰(Id为2)须先完成进程3

EventId:对话2

Effect:无

进程3. ――分配给小兰

Condition:无

EventId:对话3

Effect:(1,2)――意义:把大虎(Id为1)激发至进程2

接着分配各人物的初始事件进程,下面我们都使用资源号Id:

人物1――进程1 意义:大虎(Id为1)初始事件进程是1

人物2――进程3 意义:小兰(Id为2)初始事件进程是3

我们模拟一下事件处理模块的执行过程:

(1).初始化,大虎事件进程为1,小兰事件进程为2。

(2).触发大虎,大虎当前进程为1,内容为对话1,故执行对话1,说“今天天气不错”,无情节影响。

(3).再触发大虎,大虎当前进程不变仍为1 ,说“今天天气不错”。

(4).触发小兰,小兰当前进程为2,内容为对话2,故执行对话2,说“不知道大虎哥今天会到哪儿去?”,有情节影响,为(1,2),意义为:把大虎(Id为1)激发至进程2,故查找进程2的条件,发现有一条:(2,3),意义为小兰(Id为2)须先完成进程3,这个影响和条件正好配对,于是删除进程2的那个条件(2,3),这时再检查进程2 的情节条件链,发现已为空,于是将大虎(Id为1)的当前进程变为2,我们称为成功地把大虎激发至进程2 ;不成功的激发是指虽然删除了某个条件节点,但情节条件链仍不为空,这时的效果是减少了一个条件,但事件进程没有推进。

(5).再触发大虎,大虎当前进程为2,内容为对话2,故执行对话2,说“今天我想去打猎”,无情节影响。

要注意到的一点是:事件进程是在情节条件删除完时提前推进的,在执行进程内容时不须再检查情节条件。这与下面要引入的特殊条件是本质上不同的。

3. 特殊条件的引入是为了满足一些稀奇古怪的条件,我们给出一些特殊条件的例子:

a.李逍遥积累了三十六只傀儡虫后圣姑才答应救林月如。

b.小鱼儿等级达到二十级后才能拔出村口的雷霆刀。

c.主人公只有身上装备有玄铁剑才能砍开大石门。

d.若李忆如身上的钱不少于一百文,刘晋元就不再给钱。

这些例子的困难之处不在于如何用数据结构表达这些条件以及判断条件是否满足,而是在于无法在条件满足的一瞬间删除特殊条件节点,即提前删除特殊条件节点(如同情节条件一样)。

我们注意到,在小兰和大虎的例子中,大虎的进程推进是在小兰刚说完话后激发的,即是由满足大虎下一个进程的最后一个情节条件的事件主体激发的,我们将其称为提前触发。在特殊条件的例子中,麻烦就来了:

a.我们无法在李逍遥得到三十六傀儡虫时激发圣姑的事件进程,除非第三十六只傀儡虫与众不同,事实上哪只傀儡虫是第三十六只傀儡虫是无法预先知道的。

b.等级二十与雷霆刀的关系实在牵强,除非引入新的处理机制,在升级时还需照顾故事情节。

c.只要主人公一装备上玄铁剑就推进大石门的进程?那么主人公一卸下玄铁剑还得把大石门的进程退回去?实在是不合理。

d.最好是刘晋元给钱之前先察看一下李忆如身上是否有一百文钱,然后再随机应变;否则在任何的李忆如身上的钱在一百文上下波动的时候都要改变事件进程实在是不可能。

于是我们决定特殊条件的判断应放在执行进程的内容之前。

完整的应是这样:只要情节条件已满足,进程就可推进,但在执行进程内容之前还需判断特殊条件是否满足,若满足,执行之;若不满足,重复执行前一个进程的内容,为此需保留前一个进程的内容,同时前一个进程内容的执行之前不用检查任何条件(无需检查),之后也不能产生影响(影响不能重复产生)。

为更清楚地说明问题,我们扩展前面的例子:增加一个人物王老爹,与其对话:“小伙子,我送你一杆猎枪”,同时主人公得到一杆猎枪;另外大虎要说第二句话需增加一个特殊条件――主人公须有一杆猎枪。这样与小兰对话后,大虎仍不能说第二句话,除非拜访过王老爹,得到猎枪。

我们把新的实现列在下面:

对话资源:

对话1.“今天天气不错。”

对话2.“今天我想去打猎。”

对话3.“不知道大虎哥今天会到哪儿去?”

对话4.“小伙子,我送你一杆猎枪。”

人物资源:

人物1. 大虎

人物2. 小兰

人物3. 王老爹

物品资源:

物品1. 猎枪

进程的原因、内容、影响:

进程1. ――分配给大虎

Story_Condition:无 (情节条件)

Specail_Condition:无 (特殊条件)

EventId:对话1 (事件内容)

Story_Effect:无 (情节影响)

Special_Effect:无 (特殊影响)(下同)

进程2. ――分配给大虎

Story_Condition:(2,3) (小兰(Id为2)须先完成进程3)

Specail_Condition:(1,1,1)

1――主人公身上需有一种物品

1――该物品是猎枪

1――数量为1

EventId:对话2

Story_Effect:无

Specail_Effect:无

进程3. ――分配给小兰

Story_Condition:无

Special_Condition:无

EventId:对话3

Story_Effect:(1,2) (把大虎(Id为1)激发至进程2)

Specail_Effect:无

进程4. ――分配给王老爹

Story_Condition:无

Specail_Condition:无

EventId:对话4

Story_Effect:无

Specail_Effect:(2,1,1)

2――给主人公增加一种物品

1――该物品是猎枪

1――数量为1

各人物的初始事件进程:

人物1――进程1 意义:大虎(Id为1)初始事件进程是1

人物2――进程3 意义:小兰(Id为2)初始事件进程是3

人物3――进程4 意义:王老爹(Id为3)初始事件进程是4

以上的实现中增加了特殊条件和特殊影响,它们也是配对的。

我们再模拟一下事件处理模块的执行过程:

(1).初始化,大虎事件进程为1,小兰事件进程为2。

(2).触发大虎,大虎当前进程为1,无特殊条件,内容为对话1,故执行对话1 ,说“今天天气不错”,无情节影响,无特殊影响。

(3).再触发大虎,大虎当前进程不变仍为1 ,说“今天天气不错”。

(4).触发小兰,小兰当前进程为2,无特殊条件,内容为对话2,故执行对话2 ,说“不知道大虎哥今天会到哪儿去?”,有情节影响,为(1,2),意义为:把大虎(Id为1)激发至进程2,故查找进程2的条件,发现有一条:(2,3),意义为小兰(Id为2)须先完成进程3 ,这个影响和条件正好配对,于是删除进程2的那个条件(2,3),这时再检查进程2的情节条件链,发现已为空,于是将大虎(Id为1)的当前进程变为2。

(5).再触发大虎,有特殊条件,因为无猎枪,所以条件不满足,执行大虎的前一个进程,为进程1,内容为对话1,故执行对话1,仍说“今天天气不错”。

(6).再触发大虎,特殊条件仍不满足,仍执行大虎的前一个进程,说“今天天气不错”。

(7).触发王老爹,王老爹当前进程为4 ,无特殊条件,内容为对话4,故执行对话4,说“小伙子,我送你一杆猎枪”,无情节影响,有特殊影响,给主人公增加一杆猎枪。

(8).再触发大虎,因为主人公身上有猎枪,所以特殊条件满足,执行当前进程内容对话2 ,说“今天我想去打猎”,无情节影响,无特殊影响。

以上是三类条件:键盘触发条件、情节条件、特殊条件。

图示:

    [键盘触发条件]

条件  [ 情节条件 ]

    [ 特殊条件 ]

下面是事件进程的内容。

二. 内容分为六类:对话、描述、动画、选择、战斗和空事件。

1. 对话是最主要的一类事件内容,占了事件内容的五成以上,前面我们的例子中事件的内容都是对话。

这里面有一点要说明,先看一段主人公李忆如与水果贩的对话:

水果贩:新鲜的水果,只卖二十五文钱,快来买呦!

李忆如:这水果到处都能捡到,还卖这么贵,给我几个好不好?

水果贩:你帮我挑几次担子,我倒可以考虑,想不劳而获,没门

李忆如:这么小气,算了吧

看似两个人在你一句我一句地交谈,其实在实现上,我们把上面一大段李忆如和水果贩的对话视为一个整体,全都被安在了水果贩的身上,李忆如的作用只是去触发水果贩;如果李忆如自己也能以某种方式被触动(尽管困难大一点),我们就要考虑主人公自言自语的效果了。

2. 在实现上,描述是对话的一个特例。对话需说话人的名字、肖像,对话框的位置也是可以选择的;描述则大大简化,无需说话人的名字、肖像,文字框的位置可以固定在屏幕中间。描述约占事件内容的10%。

3. 动画类以下又可分为多种:卷屏、扭曲、震动、贴图、描述性动画、纯动画等。一开始称为动画类是因为以动画为主,后来我们又加入了其它的效果,如播放Mid音乐、播放Wav音效等,所以称为特技效果类比较好。

这里面最重要的是描述性动画,在游戏过程中我们经常看到主人公自动走路、说话、表演,周围的人物也可以参与进来,有时视角或镜头也可以移动,造成一种很生动的感觉,总之,人物动作的效果可以精心设计,但一切都是自动进行的,这就是描述性动画。我们可以事先设计好各人物的动作,协调好各人动作的同步、配合的关系,然后以行为指令流的形式记录下来,存入数据文件,因为这些动画都是以描述性语句的形式规定的,故名描述性动画。

动画约占事件内容的10%,其中描述性动画占一半。

4. 选择是实现故事多线索、多结局的必要手段,这里的选择主要是指显式选择,可以明显看到选择项,另一种隐式选择体现在主人公做不同的事将产生不同的影响,那主要在事件逻辑中实现。显式选择在整个游戏中出现得不多,约占3%。但少量的选择可以导致故事逻辑的复杂程度呈指数函数增长。

5. 战斗是事件内容的一类,另外战斗也可以作为影响而产生。由于同类怪物的战斗资源可以共享,所以战斗只占事件内容的2%。

6. 空事件实质上是无事件内容,游戏中许多场合下的事件本身没有内容,却产生大量的影响,如用于切换场景的砖块,还有大量看不见的颇富技巧性的控制物体,它们都用上了空事件。空事件约占事件内容的四分之一以上。

以上是六类内容:对话、描述、动画、选择、战斗和空事件。

图示:

内容:[对话/描述/动画/选择/战斗/空事件]

下面是事件进程的影响。

三. 影响分为两类:情节影响、特殊影响。

情节影响和特殊影响在前面讲条件的时候已经提到过,它们分别与情节条件和特殊条件配合使用,这里不再详细说明它们的功能,只提一下另外的细节。

1. 为了实现多线索、多结局,每个进程节点要准备不止一个的情节影响链:当事件进程的内容为选择时,对玩家的不同的选择应产生不同的情节影响。经研究,我们认为每个进程节点准备三个情节影响链是比较合适的,如果想产生更多的分支情节,只要把几次选择串联起来即可。

2. 为了配合多个情节影响,特殊影响也得准备多个。战斗也是一个隐含的选择:战斗的不同结局将产生不同的特殊影响――胜利会给我们带来经验值、物品;失败将导致重新取档;逃跑需要把怪物休眠片刻,否则又会进入战斗导致死循环。我们对战斗的结局不用程序特殊处理,而是像事件一样可以灵活设计,通过不同的特殊影响链可以产生不同的战斗结局的效果,这是很方便的。

图示:

    [ 情节影响 ]

影响

    [ 特殊影响 ]

这样我们就讲完了事件进程的条件、内容、影响,下面是一个事件进程节点的完整的结构:

   条件      内容       影响

[键盘触发条件]  [对话/描述/]  [ 情节影响 ]

[ 情节条件 ]  [动画/选择/]  [ 特殊影响 ]

[ 特殊条件 ]  [战斗/空事件]

为了使整个游戏能顺利地跑起来,还有一些地方要注意:

1.我们把游戏在故事情节上分为若干关World ,每个关包含几个场景Scene 。每个关的情节之间相对独立,只有一些重要事件的影响可以带入下一关,我们需要开辟一块重要事件区来存放和访问这些重要事件的数据,如从开始到李逍遥去苏州城之前可以算一关,李逍遥在苏州城的经历可以算一关;场景的概念很随意,一般把屏幕一黑换张地图再亮起来就认为是换了一个场景,如每进一个屋子都可以算一个场景,设计地图的基本单位是场景。

2.在每关开始前载入本关所有事件主体的初始事件进程,载入本关所有事件进程节点的数据结构(条件、内容、影响)。随着游戏的展开,事件主体不断被触发(表现为主人公不断地找人说话、做事情),如果该事件主体上的事件进程的条件都满足,该事件主体就会做出反应(表现为与主人公对话,或放一段动画等),再产生一些影响,或给你一些物品,或取走一些物品,或没有任何表面现象却在背后悄悄删除某个事件进程的条件),事件影响也包括动态删除和产生事件主体,表现为人物的消失和新人物的产生,也有改变人物的图片,表现为换一套衣服,如《剑侠情缘》中独孤剑与岳飞对话后就换成和岳飞一样的衣服。事件进程的条件、内容、影响的配合可以设计出非常丰富有趣的故事情节来。