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