遊戲開發手記:戰鬥模塊設計

對戰是咱們遊戲中的核心功能,能夠說是全部模塊中的重中之重。這周準備着手進入開發了,今天仔細分析了下需求,制定出來了大致方案。
特別說明一下,本文所描述的戰鬥模塊設計方案源自於實際項目(卡牌手遊)的需求,可能並不適用於MMORPG、ARPG等類型遊戲。本文所涉及戰鬥的基本形態是:從遊戲環境中收集戰鬥所須要的數據,隨後在一個獨立封閉的環境中進行若干次迭代計算(其間可能會讀入玩家的指令輸入),最後達成某些條件後分出勝負戰鬥結束。若是感受這個描述太抽象,能夠參考一下三國志系列的戰鬥或者QQ鬥地主:P
本文重點分析戰鬥系統在模塊這個級別上的設計和取捨,不涉及具體遊戲的戰鬥計算邏輯。程序員

需求分析

項目戰鬥採用的是自動回合制戰鬥,即勢力雙方派出若干角色按照特定的隊形排好後依次進行攻擊,直到其中一方全部角色都陣亡則戰鬥過程結束。爲了提升戰鬥的趣味性和策略性,咱們加入了手動釋放技能做爲補充,即玩家可主動選擇釋放技能的時機(參考刀塔傳奇)。
從程序的角度來看,若是戰鬥過程全自動不須要玩家干預,那麼能夠用「秒算」的方式來作。也就是直接一個循環瞬間計算出完整的戰鬥過程,戰鬥過程保存下來後再慢慢播放。一旦引入玩家的手動操做,秒算就再也不湊效了,由於戰鬥過程的計算會受實際操做的影響。
按照策劃的設計,目前遊戲中戰鬥的類型能夠分爲3種。副本(PvE):由玩家勢力和配置的NPC勢力對戰,玩家能夠操做技能釋放;排位賽(PvP):由兩方玩家事先派出的勢力對戰,由於玩家不必定在線,因此技能是自動釋放的,再也不支持手動操做;挑戰賽(PvP):當兩方玩家都在線時發起對戰,這時雙方均可以操做技能釋放。
其中1)副本戰鬥要求在網絡鏈接不穩定時也能正常進行,須要由客戶端進行戰鬥計算,等到結束後再將結果上傳到服務器驗證。2)排位賽存在玩家不在線的可能,因此是服務器進行「秒算」,並保存戰鬥錄像供以後播放。3)挑戰賽在服務器計算,在戰鬥過程當中客戶端須要同步操做至服務器。
對於第1點需求我是存在異議的,爲了網絡不穩定狀況下的體驗,致使遊戲中最複雜的戰鬥部分須要服務器和客戶端各實現一遍,工做量增長不說,最重要的模塊的複雜程度急劇上升,須要同時支持三種模式:a)客戶端計算,服務器驗證 b)服務器秒算,客戶端播放 c)服務器計算,客戶端播放過程和讀取操做,並實時同步。此外戰鬥邏輯自己是很是複雜的,幾乎必定會出bug而且不容易測試、發現bug也不容易重現、重現了也不容易調試和修正,服務器和客戶端還要各自實現一遍致使這一系列成本成倍增長。(項目中服務器和客戶端編程語言不一致沒法共用代碼)
戰鬥模塊的設計,重點就在於爲這兩個棘手的問題提供一個解決方案。編程

從外部看戰鬥模塊

從外部看戰鬥模塊,就是規劃模塊的外部接口,劃定模塊與外部系統的界線。
用極簡的視角來看戰鬥模塊,能夠認爲它就是一個函數,輸入是戰鬥須要的全部數據(包括雙方勢力的戰鬥單位佈局,每一個戰鬥單位的出手速度、攻防血、技能等屬性),輸出是戰鬥結果(勝負狀況,可能還包括戰鬥過程記錄)。
戰鬥模塊不關心的是:戰鬥從哪裏觸發;戰鬥結束後要更新哪些數據;戰鬥勢力是玩家仍是NPC;戰鬥可否發生,如玩家是否有足夠的體力,玩家等級是否知足副本等級,是否領取了對應的任務等。
戰鬥模塊關心的是:戰鬥過程的迭代,戰鬥結果的斷定,戰鬥過程當中數據的網絡同步(若是須要),戰鬥過程的展現(客戶端)。
特別注意戰鬥做爲一個獨立模塊不該該有任何外部依賴。例如某單位的攻擊力是由配表中的數值加上其等級進行計算,再綜合各類加成得出的,那麼應當是在戰鬥模塊外部算出最終數值後再交給戰鬥模塊,而不該該由戰鬥模塊去調用外部接口進行計算。這是很天然的,由於咱們必定不想因爲某張配表變化或者某模塊數據結構的調整致使須要修改戰鬥模塊的代碼,最後引入bug帶來沒必要要的麻煩。服務器

從內部看戰鬥模塊

從內部看戰鬥模塊,也就是制定模塊的實現方法,重點是要同時支持3種戰鬥模式。
思考問題的過程其實特別快,有時候想法的產生來自於直覺沒有什麼特別的理由,因此這裏只介紹最後想出來的方案……
戰鬥模塊從內部劃分爲這麼幾個組件:數據,計算,展現(僅客戶端),輸入。
數據部分是從模塊外部傳入的,一部分數據會交給計算模塊進行迭代(戰鬥角色的排布和屬性等),一部分數據會交給展現組件用於界面展現(雙方名字等級等)。
計算組件負責戰鬥迭代,它的輸出是一系列戰鬥過程。
展現組件接收戰鬥過程,並在場景中展現出對應的模型、動畫、UI。
輸入組件負責讀取用戶輸入。
接下來咱們看看這些組件如何組合起來知足3種戰鬥模式。
1)副本:副本戰鬥徹底在客戶端運行,計算組件輸出的戰鬥過程發往展現組件,輸入組件讀取到的操做發往計算組件影響後續迭代。
2)排位賽:排位賽由服務器秒算,服務器直接計算出全部戰鬥過程後返回給模塊外部存儲下來。客戶端查看戰鬥記錄時,計算好的戰鬥過程以數據的形式發往客戶端戰鬥模塊,此時客戶端的計算組件退化爲「播放器」,只須要將服務器生成的戰鬥過程依次發往展現組件。
3)競技場:競技場模式操做和戰鬥過程都是經過網絡實時同步的。服務器這邊:輸入由客戶端經過網絡發送過來,計算出的戰鬥過程經過網絡發往客戶端的展現組件。客戶端這邊:展現組件從網絡接收戰鬥過程,輸入組件讀取到輸入後發往服務器。網絡

戰鬥過程的記錄方式

戰鬥過程同時用於組件間通訊和戰鬥錄像的保存,其數據結構有必要探討一下。
首先記錄方式應該是基於「打譜」而不是「快照」。二者的區別是這樣的,打譜相似於「回合1 A攻擊B產生10點傷害,回合2 B攻擊A產生5點傷害」,而快照相似於「回合1 A 100生命 B 90生命,回合2 A 95生命 B90生命」。基於打譜的緣由有二:其一是一般打譜產生數據量要遠小於快照,不妨想一下象棋的棋譜,幾十分鐘的一局對弈用棋譜記下來不過半頁紙;其二是基於事件的記錄形式更便於展現組件展示過程。
可是打譜的記錄方式很容易夾帶一些隱晦的問題,究其緣由戰鬥播放是依據「譜」的一個復現過程,播放到任意時刻的狀態是由前面一系列的步驟推演出來的,只要有一步出現誤差最後的結果就可能截然不同。爲了保證一致,咱們的戰鬥過程記錄必定要足夠直接。例如受到的傷害值減掉防護值得出HP的損耗,就必須直接記錄HP的損耗,而不能只記錄傷害值交給播放組件來進行計算。更好的作法是將打譜法和快照法結合着使用,同時記錄HP的損耗和最後剩餘的HP,這樣即便不慎出現了不一致也能很快恢復。
可能有同窗不理解,損耗=傷害-防護,如此一個簡單的計算怎麼會發生不一致呢?可能還有同窗會以爲自動戰鬥根本不必記錄戰鬥過程,由於沒有操做的影響,直接拿初始數據從新推演一下不就出來了?這是新人常見的思惟漏洞,他們忽略了一個重要因素,就是線上網絡遊戲是在不斷演化的。在今天損耗=傷害-防護,下個版本可能就變成損耗=攻擊-防護,錄像數據仍是原來的數據,因而版本一更新戰鬥錄像的過程就全變了,這可就太坑爹了。網絡遊戲迭代更新很快,必定要時刻提防着數據兼容的問題。數據結構

測試的困境

前面還提到了另一個棘手的問題,複雜的戰鬥邏輯被服務器和客戶端各實現一遍,給測試帶來了不小的壓力。其實換個思惟方式就能很巧妙的解決,一旦想通後甚至有一種「因禍得福,焉知非福」的感受!
首先要注意不論是服務器仍是客戶端,戰鬥計算組件的輸出都是同樣的:一份完整的戰鬥過程記錄。那麼若是兩邊都正確實現功能的話,給這兩個計算組件以相同的輸入,則必定能夠獲得相同的輸出(這裏不考慮計算過程當中的隨機數,或者能夠認爲隨機數種子看成參數傳入);反之若是對於相同的輸入獲得了不一樣的輸出,那就說明至少有一方的實現是有問題的。
基於這個思路,咱們能夠把兩邊的戰鬥模塊單提出來獨立編譯(由於戰鬥模塊的實現不依賴於其餘模塊,單提出來是很容易作到的),再寫個測試程序不斷隨機生成戰鬥初始數據分別發往兩份實現,收回兩邊的輸出後進行校對,這樣容易就能發現bug了。等到兩份實現能一致地處理大量隨機數據時,咱們基本上就能夠認爲兩份實現都是正確的了。畢竟兩個程序員分別使用不一樣的編程語言,很巧合地設計出了類似的代碼結構,並更加巧合地犯了同一個錯誤,這個機率應該是足夠小的。編程語言

一點題外話:這個系列已經寫了陸續寫了好幾篇了,從一些評論能夠看出,文中會出現一些描述不清或詳略不當的狀況。原先設想是根據評論反饋狀況不斷改進優化正文的,後來以爲這樣有點費精力,並且讀者有疑問仍是須要以回覆的形式解答的,總不能說我改了下正文你再去重讀一下吧……我如今的想法是在評論裏進行溝通,而後按期把比較有參考意義的評論附加在正文後面。因此若是有疑問或者意見或者想法就寫在評論裏哈~函數

相關文章
相關標籤/搜索