Github地址:暗黑破壞神詞綴實現思路-範例程式碼
暗黑類遊戲非常經典,之前玩過很多,也嘗試過寫過實現的思路
最近又在之前的思路下有了新的想法。
我們先來分析下該型別遊戲的特點和其詞綴機制:
暗黑類遊戲
我玩過的暗黑類遊戲主要有:暗黑破壞神,火炬之光,流放之路。我認為暗黑類遊戲的最突出的特點,就是各種各樣的詞綴,讓玩家刷刷刷,按照自己的策略刷出合適的詞綴搭配和提升其數值,從而獲得割草和挑戰更高數值怪物的快感。
詞綴
詞綴按照我的理解就是修飾器,它可以修飾(或覆蓋)原本的各種機制(屬性,技能,狀態...),下面我們舉幾個有趣的例子:
可以看到,詞綴五花八門。有些詞綴非屬性型別的詞綴比如(不會被暴擊/50%避免中毒)也是可以通過屬性或者狀態來實現,但有些還需要其他機制處理(如標記追加傷害,需要在戰鬥模組進行處理)。
在暗黑類的眾多詞綴中,其中很多都是關聯屬性和狀態的,而狀態和屬性在我的實現中比較像(後面會提到),所以這裡詳細說下我對屬性模組和其修改器的實現思路,一些思想會應用於其他模組,並會簡要的提出其他模組可能會不同的地方。
我使用c++語言進行實現,其實思想都是一樣的,使用lua/python等在編碼效率等方面會更好些。
從上述中,詞綴影響到的機制非常的多。在實現時,可以選擇更加靈活的語言(lua/python等)進行實現。設定方面,配表+指令碼(一般使用配表,一些複雜的效果必要時呼叫指令碼)是可行的,如果編寫編輯器的話可能會更好一些(當然,程式側的開發維護成本會增加,但如果遊戲內容多的話,總體成本應當是下降的)。
Entity下掛載了一組Comp元件,包含屬性、狀態等。裝備、Buff等掛載一組Affix詞綴,詞綴又包含了一組修改器Modifier(可能有屬性、狀態、甚至是外貌、動作等修改器),修改器在應用的時候作用到各個元件的業務中(比如,屬性修改器作用的屬性元件的屬性範例中,如增加攻擊力)。若是使用觀察者模式,則類似圖中AttrBinder。外面把Binder註冊進來,當屬性變化時主動通知各個Binder屬性變化。
在角色相關的系統中,EC模式(Entity-Component)是比較常見且好用的,它把(這裡是角色,但是Entity不僅限是角色)Entity的各個業務拆分開來,降低程式碼的複雜度和耦合性。
這裡有一個使用什麼作為存component的key的問題,我考慮了三種方式:
使用RTTI類名字串
/*取類名String*/
#include <typeinfo> //注意標頭檔案
struct ClassName
{
template <typename Ty>
static string Get()
{
static string name = typeid(Ty).name();
return name;
}
};
/*獲取元件*/
template<class T>
std::shared_ptr<T> Entity::GetComp<T>()
{
string name = ClassName::Get<T>();
return std::dynamic_pointer_cast<T>(comp_map[name])
}
三種方式對比分析
2比較方便,程式碼量較少,但1更加規範尤其是多人合作專案推薦使用方式1。
方式3同2一樣方便(在c++上其實比2更加方便),不像2那樣容易出錯(有程式碼檢查和提示),但是不像列舉那樣羅列了所有元件型別,且RTTI依賴編譯器,不確定是否有些情況會有問題。
我總結了下原則:
在跨系統模組中,或者是動態生成的東西,使用字串作為引數更加靈活和方便,其他情況使用列舉保證方便維護和合作
如結構圖示:
AttrComp
掛載在Entity
上,管理了一堆屬性Attr
Attr
可以接收Binder
繫結器和Modifier
修改器。當Modifier
進來會重新收集所有Modifier
的資料並計算,並通知Binder
。需要說明的是:
Attr
沒有所謂的預設值,如果角色天生帶有一些基礎屬性,則由角色/職業相關元件新增Modifier
進來Binder
的思想是觀察者模式,Binder是在觀察者的回撥函數上進一步的封裝,以減少重複的邏輯。比如多個面板有屬性數值顯示,就可以把獲取屬性數值,賦值給UI控制元件封裝成一個Binder在多個面板上覆用,只需傳入控制元件和屬性型別。也可以傳入lambda表示式作為一般的回撥使用,如這裡的AttrBinderLambda
。注意Binder在剛繫結時也會觸發回撥。Affix
詞綴包含了多個Modifier
,在Apply
函數中應用到Entitt
的各個模組中,如屬性應用到AttrComp
指定型別的屬性Attr
中應用範例
AttrData範例:
struct AttrData
{
int fix = 0;
int more = 0;
int total = 0;
int pct = 0;
int override = 0;
bool bOverride = false;
int final = 0;
};
int raw = fix * (1 + more) * (1 + total) + (1 + pct);
int final = bOverride ? override : raw;
詞綴效果應用:
Binder
和Modifier
的實現:int AttrUtil::GetRawOverride(const AttrData& data)
{
int tmp = GetRawPct(data);
tmp *= (1 + data.pct / 100.f);
return tmp;
}
int AttrUtil::GetRawPct(const AttrData& data)
{
int tmp = 0;
tmp += data.fix;
tmp *= (1 + data.more / 100.f);
tmp *= (1 + data.total / 100.f);
return tmp;
}
void AttrModifyIncByAttr::Modify(AttrData& data)
{
data.fix += v;
}
void AttrModifyIncByAttr::Init()
{
auto func = [this](const AttrData& data)
{
if (target == from)
return;
int tmp = (AttrUtil::GetRawOverride(data)) * (pct / 100.f);
SetVal(tmp);
};
bind = std::make_shared<AttrBinderLambda>(func);
}
void AttrModifyIncByAttr::Apply(const SP(Entity)& in_ent)
{
if (in_ent)
{
auto comp = in_ent->GetComp<AttrComp>(EComp::Attr);
if (comp)
{
comp->AddBinder(from, bind);
}
}
else
{
if (auto lock = ent.lock())
{
auto comp = lock->GetComp<AttrComp>(EComp::Attr);
if (comp)
{
comp->RemBinder(from, bind);
}
}
}
AttrModify::Apply(in_ent);
}
void AttrModify::SetVal(int in)
{
if (v == in)
return;
v = in;
Upd();
}
void AttrModify::Upd()
{
if (auto lock = ent.lock())
{
auto comp = lock->GetComp<AttrComp>(GetCompTy());
if (comp)
{
comp->UpdMod(target);
}
}
}
可以看到:這裡在初始化時,建立了一個Binder,在回撥時根據攻擊力(from)計算修飾的值,SetVal時必要時會通知防禦力屬性(target)更新屬性。
即:攻擊力變化->修飾值變化->防禦力變化。
諸如其他的屬性詞綴如一半的閃避值轉化成攻擊力,同理。
(注意這裡防止轉化之間的巢狀,比如攻擊上升防禦的一半,防禦又上升攻擊的一半,需要根據需求防止迴圈)
這裡的設計主要是考慮複雜的需求和靈活:比如以後有什麼獲取所有裝備提供的攻擊力等需求可以快速的拓展。當然如果屬性系統沒有那麼多花樣,這裡雖然能滿足需求,但是在程式碼複雜度和效率上可能會差一些。
多數的情況下,修改器都是更新資料(如屬性、狀態、標誌位等),聯動到更新這些資料對應的業務,也有一些是在後續的邏輯中查詢這些資料(如戰鬥系統查詢追傷標記位(有可能是某個buf)追加傷害)
狀態系統
在我的設計中,狀態系統管理的多數是Bool值,如:
這些值往往使用乘法運算規則,如原本是可以行動,有個眩暈和封印技能同時新增狀態修改器,即val = 1 * 0 * 0 = 0
,值為0不能行動。
當然也有一些其他情況(如標記層數、中毒等)使用數位(Number)
戰鬥系統
在我的設計中,戰鬥系統和狀態、屬性系統是緊密關聯的。
戰鬥系統會頻繁的查改屬性和狀態。戰鬥系統主要負責戰鬥的流程處理和結算,並呼叫其他系統進行狀態變更和表現處理。如呼叫傷害計算公式結算傷害,並修改屬性系統HP值。