宣告
此思路是個人的思路,不代表暗黑破壞神等游戲的實際實作思路,核心代碼示例使用C++語言,注意代碼示例可能是代碼片段,需要讀者有游戲業務基礎概念和C++語言基礎,
游戲業務思路往往是比較開放的,其實不僅是游戲業務思路,個人接觸到的游戲引擎Unity,Unreal,Cocos,還有以前公司的自研引擎對游戲世界的物件理解,gamePlay框架等很多方面的解決方案都存在差異,所以游戲行業不相信通用的解決方案,簡單來說,以最小的維護成本滿足需求才是王道,
概述
暗黑破壞神,流放之路,火炬之光等經典RPG游戲有令人眼花繚亂的角色屬性詞綴和相應的機制,搭配修改角色屬性的裝備,技能,Buff等形成很多有趣的流派,此文提供一種類似游戲的角色相關模塊的實作思路,以角色屬性子模塊實作為引,也會涉及到其他角色相關系統,
思路
總體思路和類圖

如圖所示,角色模塊大概會涉及到以下幾個模塊:
- Event:提供訊息機制,實作訊息傳遞,
- NVal:NumericValue數值物件,可以被修改器修改,計算所有修改器得到一個值,并向監聽者廣播數值更新訊息,
- Entity:角色模塊化,例如:屬性,狀態,Buff,技能等,
- Game:業務層,例如:角色,裝備,buff,技能等,
Entity: 角色模塊化
使用經典 Entity-Component 模式,以組合的思想來模塊化角色,
/*取類名String*/
#include <typeinfo> //注意頭檔案
struct ClassName
{
template <typename Ty>
static string Get()
{
static string Name = typeid(Ty).name();
return Name;
}
};
/*以類名為Key,使用map存盤所有組件*/
class IEntity
{
public:
template <typename Ty>
void AddComp(Ty& Comp)
{
string Name = ClassName::Get<Ty>();
// ... 省略一些檢查
CompDict[Name] = &Comp;
}
template <typename Ty>
void RemoveComp(Ty& Comp);
template <typename Ty>
Ty* GetComp();
protected:
map<string, IComponent*> CompDict;
};
Event: 訊息機制基礎
利用C++lambda特性繞開函式指標和類成員指的可呼叫物件,詳細功能見注釋,
/*
* 可以實作類似:
*class A
*{
*public:
* void F1()
* {
* Event<int> Eve = [this](int x) {this->F2(x); };
* Eve(10);
* }
*
* void F2(int x)
* {
* printf("x = %d", x);
* }
*};
*/
template <typename ...Params>
class Event
{
private:
class EventImplBase
{
friend class Event;
private:
virtual void Run(Params ... args) const = 0;
};
template <typename Ty>
class EventImpl : public EventImplBase
{
friend class Event;
private:
Ty FObj;
EventImpl(const EventImpl&) = default;
explicit EventImpl(Ty&& FObj) : FObj(std::forward<Ty>(FObj))
{
}
virtual void Run(Params ... args) const
{
FObj(std::forward<Params>(args)...);
}
};
EventImplBase* Impl;
public:
Event() : Impl(nullptr)
{
}
void operator = (const Event& Rhs)
{
if (Impl)
{
delete Impl;
}
Impl = new EventImpl(Rhs.Impl);
}
Event(const Event& Rhs) : Impl(new EventImpl(Rhs.Impl))
{
}
void operator = (Event&& Rhs)
{
if (Impl)
{
delete Impl;
}
Impl = Rhs.Impl;
Rhs.Impl = nullptr;
}
Event(Event&& Rhs) : Impl(Rhs.Impl)
{
Rhs.Impl = nullptr;
}
template <typename Ty>
Event(Ty&& Rhs) : Impl(new EventImpl<Ty>(std::forward<Ty>(Rhs)))
{
}
template <typename Ty>
void Bind(Ty&& Rhs)
{
if (Impl)
{
delete Impl;
}
Impl = new EventImpl<Ty>(std::forward<Ty>(Rhs));
}
~Event()
{
delete Impl;
}
void operator()(Params ... args) const
{
if (Impl)
{
Impl->Run(std::forward<Params>(args)...);
}
}
};
NVal 數值物件
考慮以下幾種機制:
- 玩家掉血同步到UI顯示,
- 玩家陷入眩暈狀態時打斷施法吟唱,
抽象:值變化時,拋出訊息,
- 攻擊力+10點/攻擊力+30%/【X】技能持續期間,攻擊力強制更改為100點,
- 【X】Buff眩暈1秒/【Y】Buff眩暈2秒,
抽象:值可被修改器修改,且修改器有優先級,比如上例中攻擊力強制更改為100點的優先級低于攻擊力+10點/攻擊力+30%,值得一提的是:中了兩個眩暈buff,其實是添加兩個眩暈狀態修改器,偽代碼為: 值 = 原值(False)& 修改器1 & 修改器2,
NVal需要以下特性:
- 值變化時,拋出訊息,
- 被修改器修改,有修改順序,
- 值 可以是 int,float,bool 型別,
/*值基于int的編碼解碼*/
template <typename Ty>
struct NValCodePol
{
static Ty Encode(int& Val)
{
return static_cast<Ty>(Val);
}
static int Decode(Ty& Val)
{
return static_cast<int>(Val);
}
};
template <>
struct NValCodePol<float>
{
static float Decode(int& Val)
{
return Val / 100.0f;
}
static int Encode(float& Val)
{
return (int)(Val * 100);
}
};
/*數值物件*/
using NValUpdEve = Event<int>;
template <typename Ty, typename CodePol = NValCodePol<Ty> >
class NVal
{
public:
int Val = 0;
int Raw = 0;
protected:
NValUpdEve UpdEve;
list<INValMod*> Mods;
public:
void SetVal(Ty Val)
{
this->Raw = CodePol::Encode(Val);
UpdVal();
}
Ty GetVal()
{
return CodePol::Decode(Val);
}
Ty GetRaw()
{
return CodePol::Decode(Raw);
}
void AddMod(INValMod& Mod)
{
Mods.push_back(&Mod);
// 修改器優先級排序
Mods.sort([](INValMod* A, INValMod* B) {return A->GetPriority() < B->GetPriority(); });
UpdVal();
}
void RemoveMod(INValMod& Mod)
{
Mods.remove(&Mod);
UpdVal();
}
void SetEve(NValUpdEve&& Eve)
{
UpdEve = std::forward<NValUpdEve>(Eve);
}
protected:
void UpdVal()
{
Val = Raw;
// 修改Value
for (auto& It : Mods)
{
It->Modify(Val);
}
OnUpdVal();
}
void OnUpdVal()
{
// 拋出訊息
UpdEve(Val);// Event內部有判空
}
};
/*修改器計算策略*/
enum class ENValModPolAndPri : int
{
Inc = 1,
More = 2,
And = 3,
Replace = 4,
};
template <ENValModPolAndPri>
struct NValModPol
{
static void Modify(int& Lhs, int& Rhs)
{
Lhs += Rhs;
}
};
/*數值修改器*/
class INValMod
{
public:
virtual void Modify(int& Ref) = 0;
virtual ENValModPolAndPri GetPriority() = 0;
void SetVal(const int& Val)
{
this->Val = Val;
}
protected:
int Val = 0;
};
template <ENValModPolAndPri ENV, typename MdfyPol = NValModPol<ENV> >
class NValMod : virtual public INValMod
{
public:
virtual void Modify(int& Ref) override {
MdfyPol::Modify(Ref, Val);
}
virtual ENValModPolAndPri GetPriority() override
{
return ENV;
}
};
Attr 屬性模塊
Attr屬性是建立在NVal上的上層建筑,個人思路把Attr數值分為兩部分:
- fix 固定值
- pct 百分比
最終的數值 value = https://www.cnblogs.com/hggzhang/p/fix * (1 + pct)
class Attr
{
public:
Attr(AttrID ID);
AttrID GetID() { return ID; }
void SetFix(int Val)
{
Fix.SetVal(Val);
}
void SetPct(float Val)
{
Pct.SetVal(Val);
}
void UpdVal();
void OnUpdVal();
int GetVal() { return Val.GetVal(); }
public:
void AddModifier(IAttrNValMod& InMod);
void RemoveModifier(IAttrNValMod& InMod);
void OnModifierValUpd()
{
UpdVal();
}
protected:
public:
void SetComp(AttrComp& Comp) { this->Comp = &Comp; }
AttrComp* GetComp() { return Comp; }
protected:
NVal<int> Fix;
NVal<float> Pct;
NVal<int> Val;
AttrID ID;
protected:
AttrComp* Comp;
};
// 監聽Fix,Pct數值變化更新Value,監聽Value數值變化廣播給AttrComponent
Attr::Attr(AttrID ID)
{
Val.SetEve(NSEvent::Event<int>([&](int Val) { if (Comp) { Comp->OnAttrUpdEve(ID, Val); }}));
Fix.SetEve(NSEvent::Event<int>([this](int Val) { this->UpdVal(); }));
Pct.SetEve(NSEvent::Event<int>([this](int Val) { this->UpdVal(); }));
}
void Attr::UpdVal()
{
int FixVal = Fix.GetVal();
float PctVal = 1 + Pct.GetVal();
int MixVal = (int)(FixVal * PctVal);
Val.SetVal(MixVal);
OnUpdVal();
}
void Attr::OnUpdVal()
{
if (Comp)
{
Comp->OnAttrUpdEve(ID, Val.GetVal());
}
}
void Attr::AddModifier(IAttrNValMod& InMod)
{
switch (InMod.GetAttrValType())
{
case EAttrVal::Fix:Fix.AddMod(InMod); break;
case EAttrVal::Pct:Pct.AddMod(InMod); break;
case EAttrVal::Val:Val.AddMod(InMod); break;
default:
break;
}
}
void Attr::RemoveModifier(IAttrNValMod& InMod)
{
switch (InMod.GetAttrValType())
{
case EAttrVal::Fix:Fix.RemoveMod(InMod); break;
case EAttrVal::Pct:Pct.RemoveMod(InMod); break;
case EAttrVal::Val:Val.RemoveMod(InMod); break;
default:
break;
}
}
AttrComp負責角色屬性子模塊對外部的介面:
- 獲得某屬性物件
- 屬性值變化向監聽者廣播訊息
- 向某屬性添加修改器
/*屬性組件*/
class AttrComp : public IComponent
{
public:
void AddAttrUpdEve(const AttrID& ID, NValUpdEve& Eve);
void RemoveAttrUpdEve(const AttrID& ID, NValUpdEve& Eve);
void OnAttrUpdEve(const AttrID& ID, const int& Val);
void AddAttrMod(IAttrNValMod& Mod);
void RemoveAttrMod(IAttrNValMod& Mod);
void GenAttr(AttrID ID);
Attr* GetAttr(AttrID ID);
protected:
BucketTable<AttrID, NValUpdEve> EveBktTable;
map<AttrID, Attr*> AttrDict;
};
void AttrComp::AddAttrUpdEve(const AttrID& ID, NValUpdEve& Eve)
{
EveBktTable.Add(ID, Eve);
}
void AttrComp::RemoveAttrUpdEve(const AttrID& ID, NValUpdEve& Eve)
{
EveBktTable.Remove(ID, Eve);
}
void AttrComp::OnAttrUpdEve(const AttrID& ID, const int& Val)
{
auto Bucket = EveBktTable.GetBucket(ID);
if (Bucket)
{
for (auto It : *Bucket)
{
(*It)(Val);
}
}
}
void AttrComp::AddAttrMod(IAttrNValMod& Mod)
{
auto ID = Mod.GetID();
auto Attr = GetAttr(ID);
if (Attr)
{
Attr->AddModifier(Mod);
}
}
void AttrComp::RemoveAttrMod(IAttrNValMod& Mod)
{
auto ID = Mod.GetID();
auto Attr = GetAttr(ID);
if (Attr)
{
Attr->RemoveModifier(Mod);
}
}
void AttrComp::GenAttr(AttrID ID)
{
if (AttrDict.find(ID) != AttrDict.end())
{
return;
}
AttrDict[ID] = new Attr(ID);
}
Attr* AttrComp::GetAttr(AttrID ID)
{
if (AttrDict.find(ID) == AttrDict.end())
{
return nullptr;
}
return AttrDict[ID];
}
/*輔助結構:桶表*/
template <typename IndexType, typename EleType>
class BucketTable
{
public:
virtual void Add(const IndexType& Index, EleType& Ele)
{
auto Test = GetBucket(Index);
if (!Test)
{
Buckets[Index] = new list<EleType*>();
}
Buckets[Index]->push_back(&Ele);
}
virtual void Remove(const IndexType& Index, EleType& Ele)
{
auto Test = GetBucket(Index);
if (Test)
{
Test->remove(&Ele);
}
}
list<EleType*>* GetBucket(const IndexType& Index)
{
if (Buckets.find(Index) == Buckets.end())
{
return nullptr;
}
return Buckets[Index];
}
auto Begin()
{
return Buckets.begin();
}
auto End()
{
return Buckets.end();
}
protected:
map<IndexType, list<EleType*>*> Buckets;
};
相應的,屬性修改器也是建立在數值修改器NValMod的上層建筑,考慮到流放之路有些詞綴會修改多個屬性,個人考慮屬性修改器修改多個屬性,
enum class EAttrVal
{
Fix = 1,
Pct = 2,
Val = 3,
};
class IAttrNValMod : virtual public INValMod
{
public:
AttrID GetID()
{
return ID;
}
EAttrVal GetAttrValType()
{
return EAttrVal;
}
void SetID(const AttrID& InID)
{
ID = InID;
}
void SetAttrValType(const EAttrVal& InEAttrVal)
{
EAttrVal = InEAttrVal;
}
protected:
AttrID ID;
EAttrVal EAttrVal;
};
// 屬性修改器
template <ENValModPolAndPri ENV>
class AttrNValMod : public NValMod<ENV>, public IAttrNValMod
{
};
class AttrMod : public IModifier
{
virtual void Apply(IEntity& InEntity) override;
virtual void UnApply(IEntity& InEntity) override;
protected:
list<IAttrNValMod*> Mods;
};
void AttrMod::Apply(IEntity& InEntity)
{
auto Comp = InEntity.GetComp<AttrComp>();
if (Comp)
{
for (auto& It : Mods)
{
Comp->AddAttrMod(*It);
}
}
}
void AttrMod::UnApply(IEntity& InEntity)
{
auto Comp = InEntity.GetComp<AttrComp>();
if (Comp)
{
for (auto& It : Mods)
{
Comp->RemoveAttrMod(*It);
}
}
}
修改器組件包含了所有型別的修改器,例如:屬性修改器,狀態修改器等,
class ModComp : public IComponent
{
public:
//
protected:
virtual void OnApply(IEntity& InEntity) override;
virtual void OnUnApply(IEntity& InEntity) override;
list<IModifier*> Mods;
};
void ModComp::OnApply(IEntity& InEntity)
{
for (auto& It : Mods)
{
It->Apply(InEntity);
}
}
void ModComp::OnUnApply(IEntity& InEntity)
{
for (auto& It : Mods)
{
It->UnApply(InEntity);
}
}
Game業務層
考慮一個經典游戲業務:角色裝備,
class Equip
{
public:
void OnEquip(IEntity& InEntity)
{
Comp.Apply(InEntity);
//...
}
private:
ModComp Comp;
};
class Role : IEntity
{
public:
void AddEquip(Equip& InEquip)
{
// ...
InEquip.OnEquip(*this);
}
};
備注
- 個人把最大HP,攻擊力等定義為屬性,HP,眩暈,沉默定義為狀態,
拓展思路
- 增加相當于最大HP10%的攻擊力,
- 拓展AttrNVal,在Apply時監聽最大Hp值,重新計算修改器的值,重新計算被修改的屬性的值,
- 像流放之路會把很多物件設定標簽,以詞綴為例:有的詞綴會讓所有【防御】標簽的屬性增加20%,
- 標簽的實作思路有很多,一種思路是給unsigned int 型別的TagID的分段,例如TagID是四位0000,100X表示屬性Tag,1001表示屬性的攻擊子Tag,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/501836.html
標籤:其他
