关于游戏任务系统的一点猜想
关键词:游戏,任务系统
作者:BIce 创建时间:2012-11-27 01:26:54
最近在有挺多事要做的情况下,玩了两周的魔兽世界,玩了个熊猫人,另外把自己的猎人从70升到80,用了不少时间,也做了很多任务,虽然自己对游戏编程不算特别了解,就借着这个机会凭空想象一下游戏任务系统的大概构成吧,光玩了,很惭愧啊。下面的讨论皆以魔兽世界为例。
玩游戏,做任务是必不可少的一个方面,相信大家都经历过,它的涉及方面很多,包括怪物击杀,玩家个人信息维护,以及地图场景管理,可算是游戏逻辑中最重要的一部分,今天就简单想象一下基本的任务系统是什么样子的。
我们知道,玩家在做任务的时候,需要如下几个步骤:
- 玩家进入地图区域后,在可探索的地图范围内,发现有可以接任务的NPC(头上顶黄色叹号的那个),表示玩家可以在此NPC处获取任务。
- 玩家与此NPC进行对话,在可领取的任务列表中,选择一个任务,玩家阅读任务说明,决定是否接收任务,确定后完成接收任务的动作(不接受就没得玩了,这篇博客就写不下去了)。
- 玩家按照任务说明,完成达成任务所需要的要求条件,任务条件可能有很多种,如怪物击杀类,物品收集类,触发机关类,到达某区域的侦查类,与某NPC进行对话类,使用道具完成某些条件类。在比较复杂的情况中,完成某个任务还需要借助道具进入特殊的任务地图"位面"才可以。
- 此处省略1w字,在玩家历尽千辛万苦,达成了所有的任务条件后,在玩家进入交付任务地图所在的NPC的所在范围后,发现可以交付任务的NPC(头上顶着黄色问号的那个),表示玩家可以交付任务。
- 玩家与此NPC进行对话,在可交付的任务列表中,选择一个任务,玩家完成于NPC例行的唠叨后,进入到领取任务奖励的界面,在可领取的物品奖励中选取其中的一个或几个,点击完成任务,此时系统会将玩家选择的物品奖励连同任务的金钱和经验奖励一起发放给玩家(金钱和经验的计算与玩家的信息相关,如有传家宝或者招募状态则经验翻倍,如果是满级玩家则会将经验换算成金钱支付给玩家),完成交付任务的过程。至此玩家终于完成了一个任务,拿到了想要的东西。(真是好累啊Orz)
上面说的有可能有些复杂,不过完成一个任务确实需要这么多的步骤哦,有可能还只多不少呢,上面用玩家的角度说了这么多,那么任务系统来怎么完成玩家的需求呢,下面我们一点一点想象(因为毕竟还是没有做过嘛,只能yy下了)下吧。
1. 任务的存储形式
据博主所了解,游戏的实际数据存储一般是存储在文件与数据库中的,而逻辑相关的数据(如玩家信息)则一般是存储在数据库中的,而在游戏服务器开启之时,为了加速数据的访问与处理能力,大多数的信息都是被缓存在内存中的。这些本来存储在文件或者数据库中的数据,都会被缓存在内存对象中,由相应的数据管理器来进行管理。游戏任务信息也是一样,在存储方面,一样有物理版本(数据库)和逻辑版本(内存对象),在本次讨论中,不对其物理版本进行讨论,仅针对其逻辑版本,也就是任务系统相关的内存对象进行讨论。
计算机系统中,所有的东西都是数据,都有其对应的存储形式,而游戏任务的存储形式为何呢,在博主看来,任务大概是这样一个东西(本文所有表述图方面为方便都用UML图表示):
在上述表述中,都好理解。博主觉得要好好描述一下的只有preConditions与conditions这两项:
-
preConditions:某个玩家如果想领取这个任务所需要的条件,这些条件大概有两类,
-
一是与玩家个人信息相关的,如本任务是萨满的职业任务,则此任务的要求是玩家是萨满职业,且等级必须满足限定条件才可;
- 此类约束的实现可以用约束公式的形式实现。约束公式类似: player.roleType=‘shaman’ and player.level>=50;,在查看某个玩家是否满足要求时,可以将player替换为实际对象,来进行公式的检测
-
二是与玩家当前完成任务的情况相关的,即此任务是某些任务的后续任务,需要玩家完成某些任务才可以领取;
- 此类约束需要对任务的前置任务列表进行指定,如task.preTaskId=10000 and task.preTaskId=10001,(可以使用约束公式或者其他的形式实现),在整体上任务的构成是一个拓扑结构。而任务系统在查看玩家是否满足要求时,需要获取玩家的已完成任务列表。
-
一是与玩家个人信息相关的,如本任务是萨满的职业任务,则此任务的要求是玩家是萨满职业,且等级必须满足限定条件才可;
- conditions:玩家完成任务需要的条件,像前面说的,这些条件的种类很多,也涉及的方面很多,在后面单独介绍.
在给出任务的表示同时,我们也应该简单的给出玩家的任务相关信息表示,如下:
其中需要说明的属性是curTaskInstanceList,此属性描述玩家的当前任务列表,其中元素为TaskInstance对象,代表玩家当前进行中的一个任务的信息,在玩家接受任务时加入,放弃任务或完成任务时消除
一个任务实例代表着一个正在进行中的任务,其中conditionsStatus记录任务中各个条件的完成情况,与conditions配对,具体用法在后面说明。
2. 完成任务流程,任务系统需提供的API
在一开始,我们就给出了玩家完成一个任务的整个流程,要完成这个流程,就需要服务器提供很多的服务(API),现在我们就简单考虑一下,其实也就是任务系统的功能性需求。
-
需求1:可根据地图的Id,提供该地图中所有可接受的任务Id集合A
- 输入:mapId
- 输出:taskIds
-
需求2:可根据玩家的Id,提供该玩家当前所有已完成的任务列表B
- 输入:playerId
- 输出:taskIds
-
需求3:根据玩家的Id,地图Id,提供玩家在此地图可以领取的任务列表C
-
输入:
- 输出:taskIds
-
输入:
-
需求4:根据玩家请求,接受某个NPC提供的任务
-
输入:
- 输出:修改玩家的当前任务列表,并将任务物品交付到玩家手中
- 约束条件:玩家未完成该任务,并且玩家状态正常(所在地图与NPC的地图一致等等)
-
输入:
-
需求5:根据玩家请求,放弃某个任务
-
输入:
- 输出:修改玩家的当前任务列表
-
输入:
-
需求6:根据玩家执行的动作事件和当前状态,计算玩家完成任务的情况
-
输入:
- 输出:根据具体的Event信息,修改玩家当前任务列表中TaskInstance的conditionsStatus,更新完成任务情况的信息。Event的具体实现在后面描述。
-
输入:
-
需求7:根据玩家的请求,完成某个任务
-
输入:
- 输出:检查玩家是否完成了任务需要的条件,如果完成了任务,在NPC处交付任务,则将玩家选择的奖励物品,和奖励金币和经验一起发放给玩家,并修改玩家的已完成任务列表,通知物品管理系统销毁玩家的不可重用类别任务物品。
-
输入:
上面简单列出了几个作为玩家可见的API,而任务系统与其他系统(如地图场景管理,怪物击杀及掉落等系统的交互则省略掉了)。
3. 缓存,任务系统的非功能性需求
对于游戏系统而言,最重要的一个非功能性需求就是性能了,对于像魔兽世界这种超大型网络游戏尤其如此。以前些年的统计数字来看,魔兽世界国内在线人数的峰值能达到500w左右,如此庞大的玩家数量,会产生多么大的请求也就不言而喻了。而任务系统作为事务性系统中非常重要的一部分,与玩家每天的在线活动息息相关(每天的日常啊魂淡),与各个其他系统(如怪物击杀,物品掉落,地图场景管理,玩家物品管理等系统)联系非常紧密,它势必会有非常高的性能要求与安全要求。下面就简单讨论下任务系统的缓存方法。
- 首先讨论下任务信息的缓存
也就是TaskMetaData的存储,刚才说过此类信息一定要缓存在内存中才可以。而且TaskMetaData不受具体玩家是否在线的限制,又是需要频繁访问的数据,可以在任务系统启动之时就加载到内存中,可以用map进行存储,以taskId为键,来加速对它的访问(平衡树或者HashMap),此数据是固定数据,基本不会在服务器运行时改变
- 接下来是玩家信息的缓存
也就是Player数据的存储,当然也要缓存到内存中。其余信息没什么说的,在玩家登录游戏时加载到内存中即可。比较复杂的就是玩家已完成任务列表finishedTaskIdList的实现,由于玩家在领取任务和交付任务时需要对其进行比较多的访问,而玩家已完成的任务偏偏数量随着玩家等级的提升会不断变多(当然你不是刷怪升级的话),我们假定平均一个在线玩家完成的任务列表大小为5000,这样以500w在线玩家进行计算,就共需要25G个任务Id条目缓存在内存中,如果考虑加速访问的话(如使用平衡树来保存已完成的任务集合),就需要更多的内存,就算100G吧,不过还好不是一台服务器来响应这些请求,由于有分区分服的设定,这100G被分配下来到几十个服务器,压力也还是不大的。(主要操作为插入和查询)
- 关于玩家的当前任务列表
玩家的当前任务列表,也是需要缓存到内存中的,不同的是对于此列表的更新操作会比较频繁,主要是接受任务,放弃任务时的修改,以及对于conditionsStatus项的修改(读写频繁)。
4.达成任务的条件,Event
最后,总算说到了对于达成任务条件的说明,由于达成任务条件种类多样,下面我们一次说明。为了简化描述,我们给出了Event的概念,类似Windows编程中的消息,我们使用Event消息作为其他系统请求任务系统API的参数传递格式。采用触发的方式来完成任务达成条件的检查:在玩家执行某些满足条件的动作的时候,其他系统会为我们会为其触发一些Event,请求API来执行条件状态的修改。
-
怪物击杀类条件:C-Kill
-
Event数据内容:
,依次为玩家Id、怪物类别Id,怪物流水序号Id,地图Id -
Task.Condition内容:
,依次为怪物类别Id,需要的计数数目 -
TaskInstance.ConditionStatus内容:
,为当前击杀数 - 执行流程:在玩家击杀了某个怪物之后,会随之触发一个Event给任务系统,由任务系统查询玩家的当前任务列表,查找有无monsterTypeId匹配击杀类型Condition,如果有的话,修改玩家对应的ConditionStatus击杀数。
-
Event数据内容:
-
怪物击杀掉落物品类条件:C-Drop
- Event内容与击杀类一致
-
Condition内容:
,依次为掉落的物品类别Id,需要的计数数目 -
ConditionStatus内容:
,为当前收集数目,向物品管理系统查询 - 物品掉落执行流程:在玩家击杀类某个怪物之后,会触发一个Event,由任务系统查询任务列表中是否有掉落类条件的任务,有的话通知怪物击杀掉落系统,在掉落物品中加入任务物品,关于此类物品的拾取,放在C-Collect中一起进行。
-
物品收集类条件:C-Collect
-
Event数据内容:
,依次为玩家Id,物品类别Id,物品流水序号Id,地图Id -
Condition内容:
,依次为所需物品类别Id,需要的计数数目 -
ConditionStatus内容:
,当前收集数目,向物品管理系统查询 - 拾取执行流程:在玩家拾取某物品时,触发拾取Event,任务系统检查玩家的当前任务列表,查找是否有需要该任务物品的任务,有则将其加入玩家物品列表,并更新其ConditionStatus.curNum(包括世界物品收集以及上面的怪物掉落物品收集),没有则拒绝拾取动作
-
Event数据内容:
-
侦查类条件:C-Explore
-
Event数据内容:
,玩家Id -
Condition内容:
,任务指定的相关Position Id,碰撞空间的半径 -
ConditionStatus内容:
,标识是否到达过目标地点 - 执行流程:地图场景管理器查询任务系统,对地图中具有此类任务条件的任务进行处理,根据每个positionId,生成一个用于检测玩家是否到达任务区域的碰撞球。在玩家进行移动时,向任务系统检测玩家是否有侦查类任务,如果有则触发移动类Event,由playerId可得当前玩家位置,检测是否有碰撞行为,如果有则修改flag为真,否则不修改。
-
Event数据内容:
-
对话类:C-Chat
-
Event数据内容:
,依次为玩家Id,对话的NPC Id,交谈的Id -
Condition内容:
,玩家使用的chatId -
ConditionStatus内容:
,标识是否完成交谈 - 执行流程:在玩家与某个有交谈类任务的NPC进行交谈的时候,触发交谈类Event,任务系统检测玩家当前任务列表,如果有相关NPC交谈的任务,并且使用的chatId也正确,则将ConditioinStatus的flag为真,否则不修改。
-
Event数据内容:
-
机关触发类(其实就是世界固定物品使用):C-Use
-
Event数据内容:
-
Condition内容:
,使用物品的类别Id,使用次数 -
ConditionStatus内容:
,当前使用数 - 执行流程:在玩家使用物品时,触发使用类Event,任务系统检测玩家当前任务列表,如果有满足类型的物品使用类条件,则修改其对应的ConditionStatus,否则不变。
-
Event数据内容:
-
使用道具类:C-ItemUse
- 此类条件也是最复杂的条件,因为有很多任务都是基于道具的:如在使用玩道具之后,会修改玩家的外观(如伪装类任务,使得敌对关系转化);再比如有的任务要求对某个人/或者怪物,使用某种道具后,再进行击杀等等;还有的任务在使用道具后,可能导致玩家进入不同的"位面",周围的场景发生变化。此类条件可以与其他条件进行组合,对其他条件进行修饰,十分复杂
-
Event数据内容:
,依次为玩家Id,物品的使用目标类别,目标流水序号Id,物品类别 - Condition内容:玩家是否满足条件使用了道具
-
ConditionStatus内容:
,标识玩家是否满足条件使用了道具 -
执行流程:此条件目前我也没有想好具体的方法,只给出简单的想法(其实最简单的做法是把它化为简单类别任务,使用道具的限制在客户端,现考虑复杂点的实现,)。在物品管理系统接受到用户使用某任务物品的请求时,其生成一个Event交付给任务系统,任务系统查询玩家的当前任务列表,找到此类条件任务:
- 首先查看此条件是否是一个修饰其他条件的条件,如果是,则记录ConditionStatus为真。在被修饰的条件被处理时,会查看其C-ItemUse的ConditionStatus,如果为真,则正常处理,否则予以跳过。
- 再查看是否使用此物品需要进入特殊位面,如果需要,则通知地图管理器,将玩家移动至特殊位面,进行任务的后续条件。
- 如果只是普通的物品使用类条件,则执行使用逻辑(物品的属性中应包含),如果是一次性物品则,使用之后通知物品管理系统从玩家的物品列表中销毁此物品。
以上皆是我在任务达成条件的一点简单想法,如有错误实属正常,请大家不吝赐教哈
5. 地图场景管理器的交互
由于任务系统是在和地图场景管理器(本人对游戏编程不甚了解,不清楚地图管理器和场景管理器的区别,就一起说了,此处指负责管理玩家、怪物、野外物品和NPC的子系统)实在有很多的关系,这里就单独拉出一部分说了。
-
任务领取及交付的提示
- 在我们游历艾泽拉斯世界的时候,一定不能少了的就是没事看看小地图,来看看有没有任务可接,或者到没到交任务的NPC附近。而这个就需要任务系统和地图场景管理器一起合作才能完成了。
- 在我们控制玩家发生移动时候,由地图场景管理器来实际负责响应我们的移动行为,而此时它也不断向任务系统进行请求,查看我们控制的角色的当前任务列表,以及可领取的任务列表,查看在玩家的可视范围(小地图)内,是否有NPC是从属与这两类任务的,如果有,则该标叹号的标叹号,该标问号标问号了
-
世界的整合
- 在我们玩游戏的时候,尤其是魔兽世界,基本是无缝的世界,不需要进行任何的地图切换,但魔兽中有这样一个特性不知道您知不知道:任务的完成与否与我们所"见"到的世界是有关系的。比如在查拉索盆地和海加尔山,刚进入此地图时,有一个区域的范围全是怪物,而在完成了一系列任务之后我们发现原来是怪物的地方已经变成了很多NPC的营地(更鲜明的例子请看DK出门任务).
- 以上的特性说明,我们所见到的世界是一个拼接起来的地图场景,应该是一个由基本世界和重叠与其上的不同位面整合而成的一个"世界",而地图管理器在决定我们所属于哪个位面之时,会查询任务系统,玩家是否完成了某些任务,进而决定玩家所在的位面。
-
任务的位面
- 在接受了某些任务之后,我们会莫名其妙的进入某个平常见不到的"位面",进而在这个位面中完成任务,在完成任务之后或者离开位面区域一定距离之后,我们即可从脱离,进而回到原来的世界。
- 这个位面与上面说到的整合出世界的位面,我觉得没什么不同,只不过是一些从属于主地图的附属地图,一些小的空间而已。这些位面的产生都是一开始就存在,由地图场景管理器进行管理,玩家藉由不同的条件进入不同的空间而已,而此回就是任务系统通知任务管理器将玩家"放"到某个特殊的位面中去的吧
6. 最后
罗哩罗嗦说了这么多,也只不过是我的yy而已,供大家一笑,如有错误请大家指正,没事闲着侃侃大山呗.
我又想玩我的猎人去啦, 继续任务去吧
PS:上面提到的Player部分信息,应该是由多个子系统进行整合而得,如items,gold应归属物品管理系统,而finishedTaskIdList和curTaskInstanceList则应归属任务系统,其余玩家基本信息则应归属玩家信息管理系统进行管理