創作內容

2 GP

[程式設計] Recount 傷害統計 資料結構設計

作者:夜下月│2013-03-12 18:10:42│巴幣:4│人氣:734
前言
先前在開發 Dragon's Prophet時, 有寫了一個仿WOW功能的Recount(傷害統計) UI,
不過當時是用來給內部企劃計算傷害用的,  所以就不知道日後玩家有沒有機會看到了XD

附註:此圖為WOW的Recount UI

//---------------------------------------------------------------------------------------------------------------//

以recount的功能性來說, 資料端需要從clinet上接收並處理大量的資料
舉例來說    這些資料通常包含了   名稱/Icon/數值效果/對象/施放者
然後還要依照功能性  (ex: 攻擊/治療/DPS...etc)作區隔
並且統計次數 還有資料排序(ex. 總輸出中 A玩家占多少輸出百分比)

在一場副本中, 累積起來的資料量  有上千筆甚至上萬筆也不奇怪
所以資料結構的設計與執行的效率  就格外重要

這篇會用C++來寫飯粒 但設計模式 用別的語言 也是通用的

---------------------------------------------------飯粒資料-------------------------------------------

{ "dmg" = 100, "speller" = "p001", "target" = "m001", "spellName" = "FireBall", "Dot" = 0 }  //火球
{ "dmg" = 20, "speller" = "p001", "target" = "m001", "spellName" = "Burn", "Dot" = 1 }  //燃燒
{ "dmg" = 20, "speller" = "p001", "target" = "m001", "spellName" = "Burn", "Dot" = 1 }  //燃燒
{ "dmg" = 50, "speller" = "p002", "target" = "m001", "spellName" = "FlameWall", "Dot" = 0 }  //火牆
{ "dmg" = 50, "speller" = "p002", "target" = "m002", "spellName" = "FlameWall", "Dot" = 0 }  //火牆
{ "dmg" = 100, "speller" = "p002", "target" = "m003", "spellName" = "LightningArrow", "Dot" = 0 } //閃電箭
{ "dmg" = 0, "speller" = "p002", "target" = "m003", "spellName" = "Palsy", "Dot" = 0 }  //麻痺

-----------------------------------------------------------------------------------------------------------------

從上述飯粒資料中 可以得知 總共有5種法術
其中 火球跟附帶的燃燒  在程式處理中 是拆成兩個技能效果
閃電箭 附帶的麻痺 即使無傷害 但也是透過兩個技能效果組成

這些資料的結構 可以採用json的格式 或者Flash AS的Object 去儲存
每一筆資料 透過List/vector/Array去做管理

struct BaseData 當作原始資料的結構 包含幾個必要資訊
{
        string speller;  //施放者
        string targeter; //傷害目標
        string spellName;  //技能名稱
        float damage;  //技能傷害
        bool isDot; //是否為dot
}

接著建立資料總集合去管理所有收到的資料
vector<BaseData> dataGroup; //資料總集合
dataGroup.push(...); //每增加一筆資料 就push進去

然後 可以透過迴圈 去取得每一筆資料 然後做分析
for(int i=0;i<dataGroup.size();i++)
{
        //操作
}


但假如現在有一千筆資料 要從中撈出同一個speller的資料 意味著 就需要比對N次
而且還需要一個值去儲存紀錄 總輸出 還有總攻擊時間
不可能每加一筆資料 就要重跑N次 去作累加 這樣的效率太差

但如何將每一筆資料 跟對應的speller 還有target產生連結 達到快速的資料管理
所以我們另外建立兩個群組 一個是攻擊者(speller)group 還有被攻擊者(target)group
這邊採用map結構 並且 再增加一個跟speller相關的資料型別

struct SpellerData
{
        float totalDamage; //累計傷害
        vector<int> baseDataLink;  //建立與DataGroupk的關連表

        void init() //資料初始化
        {
                totalDamage = 0;
        };
}

透過這樣一個結構 當一筆資料 tmp新增時
//--------------------------------------------------------------------------------------------------------------

//資料集合處理區塊
List<BaseData> dataGroup; //資料總集合
map<string, spellerData> spellerGroup; //施法者資料集合

dataGroup.push_back( tmp); //新增增一筆資料到資料集合

//--------------------------------------------------------------------------------------------------------------

//施放者資料處理區塊
typedef map<string, spellerData> MAP_Speller; //將太複雜的型別縮短
MAP_Speller::iterator iter = spellerGroup.find(tmp.speller); //查找speller
SpellerData* pSpeller = &spellerGroup[tmp.speller]; //建立一個暫存指標去作存取

if( iter == spellerGroup.end() ) //若尚未有這個speller資料
{
        pSpeller->init(); //將必要資料初始化
}

pSpeller->baseDataLink.push_back( dataGroup.size() ); //將對應的資料位置 紀錄起來
pSpeller->totalDamage += tmp.damage;  //speller的總傷害累計

//--------------------------------------------------------------------------------------------------------------

透過baseDataLink這樣的關連表 可以快速取得跟這個speller相關的資料
所以 假如在1000筆資料中  "A" 是speller的資料有10筆
就不用比較1000次 而是直接取出這10筆資料就好了

同樣道理  我們希望能夠快速分析 target(被傷害者)的資料
也可以建立相同的資料結構去處理
map<string, targetData> targetGroup; //被攻擊者資料集合

上面我們已經建立了基本的資料關聯性
但這樣的功能 無法滿足Recount
還需要有各個技能傷害的 最小/平均/最大值 min/avg/max
還有各技能的攻擊類型 ex hit/miss/crit/block ...等細節
攻擊類型 必須增加 資料來源的內容 才能辦到

struct BaseData 當作原始資料的結構 包含幾個必要資訊
{
        string speller;
        string targeter;
        string spellName;
        string spellType;  //技能類型
        float damage;
        bool isDot;
}


而各技能的傷害分析 建立技能關連資料表
所以就要在SpellerData中 增加Spell的資料結構

來增加一些資料結構
一個技能集合 底下會有不同技能傷害類行的集合 並且會有此技能的總傷害

struct SpellTypeData
{
        string typeName; //類型名稱
        float totalDamage; //累計傷害
        float min;  //最小傷害
        float avg;  //平均傷害
        float max; //最大傷害
        int times;  //累計次數

        void init() //初始化
        {
                totalDamage = 0;
                times = 0;
        }
}

struct SpellData
{
        float totalDamage;//累計傷害
        map<string, SpellTypeData> spellTypeGroup; //技能傷害類型集合

        void init() //初始化
        {
                totalDamage = 0;
        }
}

struct SpellerData
{
        float totalDamage;
        map<string, SpellData> spellGroup; //技能集合
        vector<int> baseDataLink;  //建立與DataGroupk的關連表

        void init() //資料初始化
        {
                totalDamage = 0;
        };
}


接著處理技能傷害

//--------------------------------------------------------------------------------------------------------------

//技能處理區塊
typedef map<string, spellData> MAP_Spell;
MAP_Speller *pSpellGroup = &pSpeller->spellGroup;// 指標暫存
MAP_Spell::iterator spellName_iter = pSpellGroup->find( tmp.spellName ); //查找spell
SpellData* pSpell = &pSpellGroup[tmp.spellName]; //建立一個暫存指標

if( spellName_iter == pSpellGroup->end() ) //若尚未有此技能記錄
{
        pSpell->init(); //將必要資料初始化
}

pSpell->totalDamage += tmp.damage;  //spell的總傷害累計

//--------------------------------------------------------------------------------------------------------------

//技能類型處理區塊
typedef map<string, spellTypeData> MAP_STD; //將太複雜的型別縮短
MAP_STD* pSpellTypeGroup  = &pSpell->spellTypeGroup;
MAP_STD::iterator spellType_iter = pSpellTypeGroup->find( tmp.spellType ); //查找spellType
SpellTypeData* pSpellType = &pSpellTypeGroup[tmp.spellType]; //建立一個暫存指標

if( spellType_iter == pSpellTypeGroup->end() ) //尚未有此spell類型資料
{
        pSpellType->init(); //將必要資料初始化
        pSpellType->min = tmp.damage; //最小傷害預設值
        pSpellType->max = tmp.damage; //最大傷害預設值
}
else //若關連資料已存在
{
        if( tmp.damage < pSpellType->min ) //此傷害大於已記錄最小值
        {
                pSpellType->min = tmp.damage; //更新資料
        }
        else if( tmp.damage > pSpellType->max ) //此傷害大於已記錄最大值
        {
                pSpellType->max = tmp.damage; //更新資料
        }
}
pSpellType->totalDamage += tmp.damage;  //某個spellType的總傷害累計
pSpellType->times++;

//--------------------------------------------------------------------------------------------------------------

到這邊為止 一個基本的技能樹狀結構 已經處理完了
資料處理的流程  透過speller 從spellerGroup取得對應的資料結構
接著透過spell  從spellGroup中撈出來
最後將spell的細節 分配到對應的spellTypeGroup底下

上班中待續...


DPS(每秒傷害平均)的資料處理會比較麻煩點
因為我們必須判斷 這是連續性的輸出 還是有間格的輸出
比如 5秒內 每秒連續造成100傷害 那 DPS 就會是100 dmg/s
但如果砍了一下造成100傷害 又過了5秒才砍第2下也造成100傷害   
DPS 也應該是100 dmg/s   而不是(100+100) / 5
所以我們就需要準確的判斷攻擊時間間格

附註:
array的優點 資料是連續性 所以資料增減快速 但查找慢
map的優點是查找快速, 缺點是資料增減效率差(資料不連續)




上班中待續...
引用網址:https://home.gamer.com.tw/TrackBack.php?sn=1932994
All rights reserved. 版權所有,保留一切權利

相關創作

同標籤作品搜尋:程式設計|遊戲開發

留言共 1 篇留言

StillDream
這是遊戲的log檔!
我目前很感興趣,請多寫點喔!


03-17 23:46

夜下月
這比較像是 封包資訊 由server跟client溝通好 需要處理的資訊內容 送出的package03-18 02:59
我要留言提醒:您尚未登入,請先登入再留言

2喜歡★ponderzs 可決定是否刪除您的留言,請勿發表違反站規文字。

前一篇:[程式設計] 戰鬥流程管... 後一篇:[企劃]第一次工作的面試...

追蹤私訊切換新版閱覽

作品資料夾

happy545你好~~
歡迎來我的小屋看看喔~~XD看更多我要大聲說昨天12:07


face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】