前言
先前在開發 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的優點是查找快速, 缺點是資料增減效率差(資料不連續)
上班中待續...