懷疑是最強(qiáng)大的敵人恒界。 --劍圣
每個(gè)游戲睦刃,幾乎都有任務(wù)系統(tǒng),比如王者榮耀十酣,它有成長任務(wù)涩拙,成就系統(tǒng),戰(zhàn)令系統(tǒng)耸采,每日任務(wù)兴泥,還有活動任務(wù)。這些本質(zhì)都是任務(wù)系統(tǒng)虾宇,其余的游戲搓彻,或多或少,都有其一二種嘱朽。一個(gè)游戲的任務(wù)系統(tǒng)旭贬,應(yīng)該如何設(shè)計(jì)實(shí)現(xiàn)呢?
我們可以先想想游戲里一些任務(wù)的大致內(nèi)容搪泳,比如:
1.等級升至15級稀轨。
2.總計(jì)獲得10萬金幣。
3.使用戰(zhàn)士完成三局游戲岸军。
4.與好友組隊(duì)達(dá)到100把奋刽。
5.獲得王昭君瓦侮。
……
看到這些任務(wù)的時(shí)候,我們應(yīng)先思考以下幾點(diǎn):
1.它們的觸發(fā)點(diǎn)是在哪里佣谐?
比如等級升至15級肚吏,那應(yīng)該是在獲得經(jīng)驗(yàn)的時(shí)候,如果獲得經(jīng)驗(yàn)升級了狭魂,則可以觸發(fā)這個(gè)任務(wù)了罚攀;總計(jì)獲得10W金幣,是在獲得獎(jiǎng)勵(lì)的時(shí)候趁蕊,不管哪里獲得獎(jiǎng)勵(lì)坞生,只要是金幣,就去觸發(fā)計(jì)算是否累積獲得掷伙;使用戰(zhàn)士完成三局游戲是己,應(yīng)該是在進(jìn)入游戲或游戲結(jié)算時(shí)觸發(fā),把本局的職業(yè)帶過去檢驗(yàn)任柜;與好友組隊(duì)100把卒废,也可以在進(jìn)入游戲或游戲結(jié)算時(shí)觸發(fā),如果是與好友組隊(duì)的宙地,則觸發(fā)摔认;獲得王昭君,也可以在獲得獎(jiǎng)勵(lì)時(shí)觸發(fā)宅粥,檢查獎(jiǎng)勵(lì)的實(shí)質(zhì)內(nèi)容有沒有包含指定的獎(jiǎng)勵(lì)参袱。
2.代碼應(yīng)該如何區(qū)分這些不同的任務(wù)?
程序和策劃應(yīng)該對不同類型的任務(wù)都約定一個(gè)指定任務(wù)類型targetType秽梅。比如升級抹蚀,程序和策劃約定該種任務(wù)的類型為1;類似的企垦,總計(jì)獲得金幣环壤,約定任務(wù)類型為2;使用戰(zhàn)士钞诡,約定任務(wù)類型為3郑现;好友組隊(duì),約定任務(wù)類型為4荧降;獲得英雄接箫,約定任務(wù)類型為5。
3.如何提高任務(wù)的通用性朵诫?
提高任務(wù)的通用性列牺,就是要想策劃以后可能的擴(kuò)展改動。比如總計(jì)獲得10W金幣拗窃,如果策劃以后要改成總計(jì)獲得10W鉆石呢瞎领?難道策劃和程序都再新增一個(gè)任務(wù)類型嗎?雖然可以這么做随夸,但是程序又得加代碼了九默,所以我們代碼不如再做進(jìn)一步區(qū)分,新增個(gè)targetId宾毒,比如1指金幣驼修,2指鉆石,那么具體累積獲得哪種貨幣诈铛,或以后新增貨幣乙各,也只需策劃填入即可,我們只需在代碼里判斷傳遞過來的貨幣類型和任務(wù)配置中的類型是否一致就可達(dá)到只寫一次代碼就可以適應(yīng)策劃各種貨幣配置的目的幢竹,值得注意的是耳峦,這里的貨幣類型定義,應(yīng)該與玩家的屬性定義保持一致焕毫,不然還得作映射轉(zhuǎn)換蹲坷,太麻煩了。比如這里1指金幣2指鉆石邑飒,但是玩家屬性早已定義101是指金幣循签,102指鉆石,那這里還需要有個(gè)1->101疙咸,2->102的映射才行县匠,還不如直接在targetId中填101指金幣,102指鉆石和玩家屬性定義保持一致撒轮,免得再轉(zhuǎn)一次乞旦。
同理,使用戰(zhàn)士完成三局游戲腔召,以后策劃很可能會再要求使用法師完成三局游戲的杆查,如果任務(wù)類型為3專指使用戰(zhàn)士完成游戲次數(shù),那么改成使用法師時(shí)代碼又得新增個(gè)任務(wù)類型了臀蛛,同理可以在targetId填入職業(yè)類型亲桦,比如1指戰(zhàn)士,2指法師等浊仆,同理這里的職業(yè)類型也應(yīng)當(dāng)與游戲定義的職業(yè)類型保持一致客峭,免得再轉(zhuǎn)。
獲得指定英雄抡柿,也與上述一樣的道理舔琅,要做好配置的通用性。免得以后再動代碼洲劣。
通過第2和3點(diǎn)可以設(shè)計(jì)任務(wù)配置如下:
接下來看代碼如何實(shí)現(xiàn):
在第1點(diǎn)中备蚓,我們知道了任務(wù)在哪里觸發(fā)的课蔬,那代碼該如何寫呢?比如在獲得獎(jiǎng)勵(lì)的邏輯里郊尝,我們是取出身上所有的任務(wù)數(shù)據(jù)出來二跋,一個(gè)個(gè)循環(huán)與任務(wù)配置對比,然后設(shè)置任務(wù)進(jìn)度嗎流昏? 如(錯(cuò)誤示范):
public void reward(long rid, List<Item> rewards){
List<Task> tasks = taskService.getAllTasks(rid);
for(Item item : rewards){
if(item.getItemType() == ItemType.PROPERTY){//如果獎(jiǎng)勵(lì)是屬性類型
player.getPropertyData().addProperty(item.getItemId(), item.getNum());
for(Task task : tasks){
TaskConfig config = TaskConfig.getConfig(task.getTaskId());
if(config == null){
log.error("任務(wù)配置不存在扎即, taskId:{}", task.getTaskId());
continue;
}
if(config.getTargetType == TaskDefine.TARGET_TYPE_TOTAL_MONEY){
if(item.getItemId() == config.getTargetId()){
task.setNum(task.getNum() + item.getNum());
if(task.getNum() >= config.getTargetNum()){
task.setStatus(TaskStatus.DONE_UNREWARD);//完成未領(lǐng)取
}
}
}
}
}else if(item.getItemType() == ItemType.HERO){//如果獎(jiǎng)勵(lì)是英雄類型
player.getHeroData().addHero(item.getItemId(), item.getNum());
for(Task task : tasks){
TaskConfig config = TaskConfig.getConfig(task.getTaskId());
if(config == null){
log.error("任務(wù)配置不存在, taskId:{}", task.getTaskId());
continue;
}
if(config.getTargetType == TaskDefine.TARGET_TYPE_GET_HERO){
if(item.getItemId() == config.getTargetId()){
task.setNum(task.getNum() + item.getNum());
if(task.getNum() >= config.getTargetNum()){
task.setStatus(TaskStatus.DONE_UNREWARD);//完成未領(lǐng)取
}
}
}
}
}
}
}
雖然這代碼還可以優(yōu)化况凉,但這種做法肯定是不可取的谚鄙,觸發(fā)任務(wù)和發(fā)獎(jiǎng)邏輯耦合到一起了,游戲里觸發(fā)任務(wù)的地方有很多刁绒,而任務(wù)又有很多個(gè)闷营,這樣在效率上來說也是不可取的,如果以后新增任務(wù)類型膛锭,找起來也麻煩粮坞,有可能遺漏不說,還可能需要修改代碼初狰,非常不好維護(hù)莫杈。
因此,要換種思路奢入,我們應(yīng)根據(jù)任務(wù)目標(biāo)類型targetType筝闹,任務(wù)目標(biāo)Id targetId去尋找是否有觸發(fā)的任務(wù),即在任務(wù)模塊提供觸發(fā)任務(wù)的接口腥光,供其他模塊調(diào)用关顷,如(正確示范):
public class TaskService{
public void triggerTask(long rid, int targetType, int targetId, int num){
List<Task> tasks = getTasks(rid, targetType, targetId);
for(Task task : tasks){
if(task.getStatus() == TaskStatus.DONE_REWARD || task.getStatus() == TaskStatus.DONE_UNREWARD){//已完成已領(lǐng)獎(jiǎng)的不再觸發(fā)
continue;
}
TaskConfig config = TaskConfig.getConfig(task.getTaskId());
if(config == null){
log.error("任務(wù)配置不存在, taskId:{}", task.getTaskId());
continue;
}
task.setNum(task.getNum() + item.getNum());
if(task.getNum() >= config.getTargetNum()){
task.setStatus(TaskStatus.DONE_UNREWARD);//完成未領(lǐng)取
}
}
}
}
這樣武福,在發(fā)獎(jiǎng)時(shí)就可以這樣寫了:
public void reward(long rid, List<Item> rewards){
GamePlayer player = playerService.getPlayer(rid);
for(Item item : rewards){
if(item.getItemType() == ItemType.PROPERTY){
player.getPropertyData().addProperty(item.getItemId(), item.getNum());
taskService.triggerTask(rid, TaskDefine.TARGET_TYPE_TOTAL_MONEY, item.getItemId(), item.getNum());
}else if(item.getItemType() == ItemType.HERO){
player.getHeroData().addHero(item.getItemId(), item.getNum());
taskService.triggerTask(rid, TaskDefine.TARGET_TYPE_GET_HERO , item.getItemId(), item.getNum());
}
}
}
這才是任務(wù)系統(tǒng)的正確實(shí)現(xiàn)邏輯议双。即應(yīng)該由任務(wù)目標(biāo)類型targetType,任務(wù)目標(biāo)id targetId去觸發(fā)它所有能觸發(fā)的任務(wù)捉片,而不是把所有任務(wù)都取出來一個(gè)個(gè)對照平痰。
這種實(shí)現(xiàn),任務(wù)的緩存模型應(yīng)該為如下形式伍纫,一個(gè)觸發(fā)任務(wù)的緩存宗雇,用于服務(wù)端任務(wù)的觸發(fā);一個(gè)請求領(lǐng)取任務(wù)獎(jiǎng)勵(lì)的緩存莹规,因?yàn)榭蛻舳耸且匀蝿?wù)id請求領(lǐng)獎(jiǎng)的赔蒲。而不是僅一個(gè)總?cè)蝿?wù)緩存:
//觸發(fā)任務(wù)緩存
//targetType -> targetId -> Set<Integer> taskSet
Map<Integer, Map<Integer, Set<Integer>> triggerTasks = new HashMap<>();
//或者 targetType_targetId -> Set<Integer> taskSet
Map<String, Set<Integer>> triggerTasks = new HashMap<>();
//所有任務(wù)緩存taskId -> Task
Map<Integer, Task> tasks = new HashMap<>();
拓展及優(yōu)化:
因?yàn)橐粋€(gè)游戲中可能有多個(gè)任務(wù)系統(tǒng),如成長任務(wù),成就系統(tǒng)舞虱,戰(zhàn)令系統(tǒng)欢际,每日任務(wù),活動任務(wù)砾嫉,這些系統(tǒng)歸根結(jié)底都是任務(wù)系統(tǒng)幼苛,各個(gè)系統(tǒng)分開寫也可以,因?yàn)楫吘谷蝿?wù)配置可能不在同一個(gè)文件中焕刮,可能配置的字段數(shù)量也不一樣,還有存儲的數(shù)據(jù)庫表也可能不在同一個(gè)表墙杯,請求的協(xié)議也可能不一樣配并,只要把代碼復(fù)制一遍就可以實(shí)現(xiàn)了,但是如果一個(gè)任務(wù)系統(tǒng)的代碼寫錯(cuò)了高镐,那其他任務(wù)系統(tǒng)的代碼就都得改溉旋,所以好的方案是盡量把它們做成一個(gè)總的任務(wù)系統(tǒng),此后添加其他的任務(wù)系統(tǒng)嫉髓,只需修改少量代碼即可观腊。
現(xiàn)在我們就假設(shè)這些任務(wù)系統(tǒng)的配置都不共用一張配置表,任務(wù)的數(shù)據(jù)db存儲表也不是同一個(gè)表算行,任務(wù)的請求協(xié)議也不一樣梧油,那又該如何改動適配呢?
任務(wù)觸發(fā)的方式還是和上面一樣的州邢,只是任務(wù)的緩存模式改變一下就可以了儡陨。如:
//觸發(fā)任務(wù)緩存
//targetType_targetId -> Set<taskId_taskType>
Map<String, Set<String>> triggerTasks = new HashMap<>();
//所有任務(wù)緩存 taskType -> taskId -> Task
Map<Integer, Map<Integer, Task>> tasks = new HashMap<>();
嫌字符串拼接不好的,也可以用Pair:
//觸發(fā)任務(wù)緩存Pair<targetType,targetId> -> Set<Pair<taskId,taskType>>
HashMap<Pair<Integer, Integer>, Set<Pair<Integer, Integer>>> triggers = new HashMap<>();
或者合并兩個(gè)int為long:
HashMap<Long, Set<Long>> triggerTask = new HashMap<>();
public static long intMergeToLong(int hig, int low){
long value = 0L;
value = ((long) hig) << 32;
value |= low;
return value;
}
public static int getLongHig(long value){
return (int) (value >> 32 & 0xffffffff);
}
public static int getLongLow(long value){
return (int) (value & 0xffffffff);
}
然后取不同任務(wù)系統(tǒng)里的任務(wù)數(shù)據(jù)時(shí)量淌,可以定義枚舉來做骗村,比如取不同的配置,取不同db表的數(shù)據(jù)呀枢,存儲不同db表的數(shù)據(jù)胚股,方案有很多,自行擇優(yōu)實(shí)現(xiàn)即可裙秋。