UML视图
任务的配表设置
主要参数简化如下:
代码命名 |
变量意义 |
填表示例 |
Id |
任务id |
|
#name |
任务名字 |
“副本挑战” |
icon |
任务图标 |
Image_task_id |
#describe |
任务描述 |
“快来挑战副本” |
type |
任务类型 |
主线/支线/日常/成就/… |
point |
各类型特有参数 |
进度/null/活跃点/成就点/… |
receiveLevel |
接取(解锁)等级 |
|
previousID |
前置任务ID |
|
contentType |
点击事件枚举 |
与NPC对话/打开界面/进入活动 |
contentArgs |
点击事件参数 |
NPC的ID/界面ID/活动ID |
conditionType |
任务完成条件类型 |
对话/升级/获取物品/指定操作/… |
conditionX |
完成条件参数X |
|
conditionY |
完成条件参数Y |
|
finishType |
任务完成领取奖励方式 |
自动完成/与NPC对话/用户点击 |
finishDescribe |
完成后的任务描述 |
“挑战成功,找{0}领奖” |
reward(简化后结构) |
任务完成奖励 |
记录这样的一个结构为 CfgTask。
考虑读取需求,除了直接通过 ID 获取任务外,用的次多的应该是通过 Type 获取所有该类型任务。所以存储时除了通过一个 _taskDic:Dictionary 存储,用Id-->CfgTask外。再建立一个Dictionary,用 Type—>CfgTask数组,数组顺序无意义。示意如下:
这样的双字典引用方式对于查找引用可以快速定位,而对于任务的顺序性在前段并不关注。
CfgTaskMgr中主要读取方法有:
/**通过Id获取任务基础数据*/
public function getCfgTaskById($id:int):CfgTask
/**获取某类型在接取等级范围内的所有数据*/
public function getAllTaskByType($type:int,$minLevel:int=-1, $maxLevel:int=-1):Array
模型中的数据存储与读取
常量定义类 TaskConst
在 TaskConst 中常量定义:
//任务状态类型 /**任务状态:未接取*/ public static const TASK_STATE_UNSHOW:int =; /**任务状态:已接取未完成*/ public static const TASK_STATE_ACCEPT:int =; /**任务状态:已完成未领奖*/ public static const TASK_STATE_COMPLATE:int= ; /**任务状态:完成且领奖*/ public static const TASK_STATE_FINISHED:int= ; //任务类型,与配表一致 /**任务任务:主线任务*/ public static const TASK_TYPE_TRUNK:int = ; /**任务任务:支线任务*/ public static const TASK_TYPE_BRANCH:int =; /**任务任务:日常任务*/ public static const TASK_TYPE_DAILY:int = ; /**任务任务:活跃任务*/ public static const TASK_TYPE_ACTIVE:int =; /**任务任务:新兵任务*/ public static const TASK_TYPE_NEWBIE:int =; /**任务任务:成就任务*/ public static const TASK_TYPE_ACHIEVMENT:int= ;
基础数据类 TaskData
与配表的双字典存储一样,在模型中的也是采用的双字典索引,只不过基本类型是在 Cfgtask 的基础上包装了一层用户数据,形成的 TaskData,其主要属性/方法如下:
/**任务Id*/ private var _taskId:int; /**任务对应配表*/ private var _cfgTaskData:CfgTask = CfgTaskMgr.getInstance().getCfgTaskById(_taskId); /**服务器发送的任务是否领奖状态*/ private var _state:Boolean; /**目标完成值,即用于与 ConditionX 比对确定任务是否完成*/ private var _target:int; /**任务状态*/ public function get state():int { if(_state== true) return TaskConst.TASK_STATE_FINISHED; else if(_target >= _cfgTaskData. ConditionX) return TaskConst.TASK_STATE_COMPLATE; else if(UserModule.getInstance().lv >= _cfgTaskData.ReceiveLevel) returnTaskConst.TASK_STATE_ACCEPT; else returnTaskConst.TASK_STATE_UNSHOW; } /**获得当前任务描述,依据当前状态变化,自带HTML属性*/ public functiongetTaskDataDescribe():String
模型类 TaskModule
与CfgTaskMgr 存储CfgTask 类似的方式,TaskModule 存储 TaskData 也是采用双字典引用,但不同的是,配表信息在读取时就已经确定了位置和数量(这里假定是一次性读取,后续文章会写需要时再读的优化),而 TaskModule 中存储的数据会随时变化,不仅是里面信息的变化,而且还是有 TaskData 元素的增删,而这就需要动态的维护。
类似的,该字典命名为_taskDataDic 和_taskDataByTypeDic。
获取方法与配表类似,主要有:
/**通过Id获取任务数据*/ public function getTaskDataById($id:int):TaskData /**获取当前类型的所有任务数据*/ public function getTaskDataByType($type:int):Array
数据可能变化,定义相应的事件类:public class TaskChange:event ,并且界面监听该事件,当收到(并判定为是该类型变化时)刷新页面。事实上,有些任务类型并不在玩家登陆后即获取(譬如成就),而是在玩家打开界面时再向服务器请求获取,这样打开成就界面时通过getTaskDataByType(TaskConst. TASK_TYPE_ACHIEVMENT) 的返回值为空,在该函数体内,判定为空后会向服务器请求数据,再在收到数据并完成存储后抛TaskChange 消息,从而界面收到消息后再次通过getTaskDataByType 请求数据才能获取到。即:
/**获取当前类型的所有任务数据*/ public functiongetTaskDataByType($type:int):Array { if(_taskDataByTypeDic[type] == null) { _taskDataByTypeDic[type] = []; requestGetTaskDataByType(type); } return _taskDataByTypeDic[type]; } /**向服务器请求某类型任务的数据*/ private fuction requestGetTaskDataByType($type:int):void { varmsg: request_get_task_data_by_type = new request_get_task_data_by_type(); msg.type= type; NetworkMgr.getInstance().send(msg); } /**收到服务器请求某类型任务的数据的回复*/ public functionon_request_get_task_data_by_type_result($res: request_get_task_data_by_type_result):void { …//数据转化成 TaskData 存储 EventMgr.getInstance().dispatch(newTaskChange()) }
Model 类中与服务器通信修改数据
协议的定义
上面已经展示了一个基本的前后端通信在Modele类中的处理。我们知道,Modele的生命周期伴随着整个游戏的生命周期,甚至早于玩家登陆前。每一个通信协议包都需要注册一个对应的Modele类,一个Module类可以注册多个通信协议包,虽然不建议这样做。
前端对socket通信采取了封装,具体另行再写,在模块中约定了三种形式:
向服务器发消息:NetworkMgr.getInstance().send(msg);
收到该消息的反馈是通过指定名字函数的方式接收,封装调用方式为:
function = modele[“on_”+协议名]
function.call(null, msg)
模块内则是通过创建诸如上述
public functionon_request_get_task_data_by_type_result($res: request_get_task_data_by_type_result):void
方法来接收的。
还有一种类似的服务器不需要客服端请求即发送给前段的 push 协议:
public function on_push_task_change($res: push_task_change):void。
任务模块的典型协议文件 task.proto 如下所示(意义如名):
package protos.task; message task_data{ requiredint32 task_id = 1; optionalint32 state = 2; //0:未领奖,1:已领奖 requiredint32 val = 3; //任务进度,对应 ConditionX 值 } message request_get_task_datas{ requiredint32 type = 1; } message request_get_task_datas_result{ requiredint32 type = 1; repeatedtask_data task_list = 2; //进行中的任务列表 repeatedint32 finished_list = 3; //已完成的任务Id列表 } message request_get_task_reward{ requiredint32 task_id = 1; } message request_get_task_reward_result{ requiredint32 task_id = 1; requiredint32 res = 2; //0:正常领奖,1:已领取,2:不能领取 } message push_task_val_change { repeatedtask_data task_list = 1; } message push_task_finished{ requiredint32 task_id = 1; } message push_new_task{ repeatedint32 task_id = 1; }
模型类处理
针对单个任务数据的处理函数示意简化如下:
private function taskDataChange($data:protos.task.task_data) { vartaskData:TaskData = _ taskDataDic[$data.task_id]; var type:int =CfgTaskMgr.getInstance().getCfgTaskById($data.task_id).type; if(taskData==null) //添加操作 { var TaskData:TaskData = new TaskData($data); _ taskDataDic[$data.task_id] = TaskData; if(_taskDataByTypeDic[type]==null) _taskDataByTypeDic[type]= []; _taskDataByTypeDic[type].push(TaskData); } else { taskData.update($data); } Time.addDelayCall(100,dispatchTaskChange); } private function dispatchTaskChange():void { Time.deleteDelayCall(dispatchTaskChange); EventMgr.getInstance().dispatch(newTaskChange()) }
该函数在项目中实际约有150行,主要还处理了对任务的删除和计数,以及对消息内容的逻辑判断和特定情况下抛其它消息。
最后采用延迟抛消息是因为服务器发送的任务消息变化可能是分在几个msg里在一瞬间先后到达的,为避免这时可能会频繁刷新界面而卡死的问题才这样设计的。
需要注意的是away3d.time.Time.addDelayCall 内部是用字典实现的,键值为该延迟回调函数,因此在回调前如果再次对该函数注册会覆盖掉之前设定的时间,达到时间后延的效果,这点不同于 flash.utils.setTimeout 函数。另外,因为这是模型的单例类,其实不调用 deleteDelayCall 也不会有内存泄露的问题。
如此有了公共的对基本数据处理的函数,则对每个消息转化为taskDataChange 的所需的参数类型task_data 循环调用即可。譬如:
public function on_push_task_val_change($res:push_task_val_change):void { for each(var t:task_data in $res.task_list) taskDataChange(t); } public function on_push_task_finished($res:push_task_finished):void { var t:task_data = new task_data(); t.task_id = res.task_id; t.state = 1; taskDataChange(t); } public function on_push_new_task($res:push_new_task):void { for each(var taskId:int in $res.task_list) { vart:task_data = new task_data(); t.task_id= taskId; t.state = 0; t.val= 0; taskDataChange(t); } }