类暗黑破坏神属性系统思路
声明
此思路是个人的思路,不代表暗黑破坏神等游戏的实际实现思路。核心代码示例使用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 = 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。