手游系统逻辑档案之任务系统

时间:2021-11-10 21:24:56

  做了一个从头开发的手游项目,整体上是个愉快的过程,因为可以遇到很多新的问题和挑战,自己动手解决这些问题是件快乐的事情。我在项目中负责后端逻辑,回想起来让我觉得有意思的地方,我都会记录于此,跟大家分享些心得。目前有计划的模块有:任务系统、商店系统、排行榜和竞技场、通信协议、数据库存储模块、活动时间表模块这几个。今天先从任务系统说起。任务系统是我做的第一个与其他模块交叉引用的模块,所以思路经历了一番转弯抹角,历时1周终于理顺。其中历程,还是很有意思的。

  任务系统分主线任务、日常任务和悬赏任务。

  •   主线任务的特点是“所有任务分成N条主线,每条主线上一个任务完成了,即使不领奖也可以完成下一个任务,每条线没有尽头”;
  •   日常任务的特点是“每天0点重置,共有N个任务,完成一个则可领奖,领奖后此任务消失”;
  •   悬赏任务比较复杂“每天有3次刷新机会,每次刷新出5个任务,任务完成领奖后消失。任务分1-5星,生星规则根据主角等级和星级概率动态生成。”;

  刚开始做的时候打算用一个统一的类包装所有的任务,对外提供生成任务和任务领奖的接口就行了,结果做完之后发现这三种任务的不同点多于共同点,放在一个类里面用三种结构处理,还是得用类型区分的,倒不如分开处理更直接。实际上这三类任务除了“生成任务”和“更新任务状态”、“完成任务领奖”这三个共同接口,其他操作完全没有关系。如果继承也只能继承自一个抽象类,而这个抽象类不能覆盖对外的全部接口(比如“刷新任务”只有悬赏任务才有),所以我决定把这三个类完全独立开,包括协议、存储、实现。他们共有的逻辑是任务完成的条件,以及对完成条件的检查。

  “任务完成条件”是譬如“杀死n只独角兽”、“收集n个x物品”、“通关第x关卡”之类的判定条件。稍作总结就发现其本质就是对某种特定事件的计数,像“等级到达20级”就是对级别的计数,或者是“等级到达20级1次”这个事件的计数,由于等级、关卡这种主要计数都是已经存在的,就不必单独计数了,直接拿过来判断即可。当时对任务完成条件的计数如何存储如何更新有过激烈的思想斗争,我大概想过这些方案:

a)         方案一:每个任务完成条件是一个对象,这个对象接收输入(玩家的指针或数据),输出完成结果。在配表的时候把所有任务的完成条件都预先初始化完毕,运行时传入玩家指针判断是否完成此任务。这样做有2个缺点,1任务完成条件不是按任务完成条件类型分的,而是按实现一一初始化的,导致内存浪费。2由于无法预先知道有多少对象,需要动态分配内存。

b)        方案二:每个任务完成条件是一个函数,这个函数是按任务完成条件的类型分的,所以函数个数是固定的。还有一个分配器,负责运行时根据任务ID去配置表取完成条件,从完成条件里面取出类型,再根据类型找处理函数,把配置的参数和玩家指针传入,输出完成结果。优点是节省了内存,简化了实现。缺点是每次要去配表找完成条件的参数。

c)         方案三:任务完成条件按照类型分为N个函数,每日任务记录5个计数值,悬赏任务记录5个计数值,主线任务记录10个计数值。任务完成领奖的时候,判断是否完成,比较计数值和配表值。

d)        方案四:目前的做法。任务完成条件是用于判定的数据,作为任务的一个属性。在所有任务完成动作的地方触发计数,客户端请求任务状态的时候,服务器遍历任务完成条件,设置完成状态判定是否完成。

  最后一个方案就是目前的做法,相当于把任务完成条件作为每个任务的一个附加数据项(一个任务可能有多个完成条件),任务完成条件作为一个结构存在:{完成类型,参数1,参数2,...}。一旦生成了一个任务(生成了就等于接收了任务),这个任务就按照配表初始化自己的任务完成条件。

  剩下的问题就是,每个任务的计数在哪里更新?我曾想过既然某个任务完成条件存在,必然要依赖其他逻辑模块计数,如果其他模块把我要计数的值都给我记着,我拿过来用不就行了?但是我想到了一个坑,悬赏任务的坑:不能在每个逻辑模块对要计数的变量计数,比如两个悬赏任务都是强化武器,一个30次一个40次,就有两个计数,逻辑模块不知道有几个计数。So,计数只能放在每个任务单元中,其他逻辑模块引用计数变量的地址计数。

  问题又来了,我想要的计数,其他模块未必直接提供给我了。比如通过第3关2次,显然我既不能要求关卡模块给我这个接口,也不能要求他在第三关的时候调用我的接口,只能是在通过每一关的时候调用我的一个“和通关有关”的接口,然后把第3关作为参数传给我。另外,我可能同时有多个任务都和通关有关,于是我的任务系统和关卡系统之间需要一个适配器来协调,适配器知道我哪些任务和通关有关,然后提供一个统一的接口给关卡系统调用,当通关的时候,适配器把所有和通关有关的任务计数更新。那么,任务系统和任务完成适配器之间要怎么配合呢?显然,生成任务的时候,要把任务通知给适配器,适配器给我按完成条件归类;完成任务的时候,要通知适配器检查是否已经完成任务,如果完成就删除这个任务的计数。

  我画了一个示意图,用来表示这个静态结构关系:

  第一层是任务id和任务单元的一个map表,第二层是每个任务单元的实体list,第三层是任务完成条件的管理器,也就是那个和其他模块配合的适配器,里面维护着多个链表,每个链表代表一种完成条件A,每个链表存储着某个任务的id和这个任务的完成条件(属于A类型的完成条件),存id是为了删除的时候方便。途中的每个方块代表一个任务完成条件的结构,每个任务单元有多个完成条件,每种完成条件类型对应一种颜色。

手游系统逻辑档案之任务系统

  如此,就解决了整个任务的生成-计数-完成-删除的生命周期管理。

  目前的方案不足的地方就是,如果要增加一种完成条件,就要在适配器中增加对应的代码。理想的情况是无论完成条件是什么,也只不过是一种类型和参数的组合,我只需要根据类型给他指定计数方案(就是在任务完成条件适配器中的位置),无需增加代码就能被归类管理起来。当然,前提是适配器与其他模块的接口稳定且健全,新增的功能是必然要增加接口的。