游戲任務(wù)系統(tǒng)實(shí)現(xiàn)思路

懷疑是最強(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ù)配置如下:

任務(wù)配置.png

接下來看代碼如何實(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)即可裙秋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末琅拌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子残吩,更是在濱河造成了極大的恐慌财忽,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泣侮,死亡現(xiàn)場離奇詭異即彪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門隶校,熙熙樓的掌柜王于貴愁眉苦臉地迎上來漏益,“玉大人,你說我怎么就攤上這事深胳〈掳蹋” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵舞终,是天一觀的道長轻庆。 經(jīng)常有香客問我,道長敛劝,這世上最難降的妖魔是什么余爆? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮夸盟,結(jié)果婚禮上蛾方,老公的妹妹穿的比我還像新娘。我一直安慰自己上陕,他們只是感情好桩砰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著释簿,像睡著了一般亚隅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辕万,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天枢步,我揣著相機(jī)與錄音,去河邊找鬼渐尿。 笑死醉途,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的砖茸。 我是一名探鬼主播隘擎,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼凉夯!你這毒婦竟也來了货葬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤劲够,失蹤者是張志新(化名)和其女友劉穎震桶,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體征绎,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蹲姐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柴墩。...
    茶點(diǎn)故事閱讀 39,711評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡忙厌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出江咳,到底是詐尸還是另有隱情逢净,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布歼指,位于F島的核電站爹土,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏东臀。R本人自食惡果不足惜着饥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望惰赋。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至祖今,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背击你。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谎柄,地道東北人丁侄。 一個(gè)月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像朝巫,于是被迫代替她去往敵國和親鸿摇。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 游戲任務(wù)是游戲制作中的必須環(huán)節(jié)劈猿,那么游戲任務(wù)是怎么完成的呢拙吉?游戲任務(wù)的制作又有那些需要注意的問題呢?我們一起看看:...
    牧野在冊閱讀 12,546評論 0 6
  • 曾經(jīng)十分沮喪揪荣, 因?yàn)闆]有鞋子筷黔, 可是就在街上, 遇到?jīng)]有腳的男子仗颈。 沒有腿還能高興快樂自信著佛舱, 自信富有充滿著大腦...
    水日皿君閱讀 98評論 0 1
  • 今天回家,爸爸在做飯,我把一些好玩的東西給我弟弟名眉,我和他一起玩彩泥粟矿。那個(gè)水晶彩泥,剛好可以當(dāng)我們玩的牛肉损拢。...
    蘑菇卿_5ca9閱讀 400評論 0 0