类暗黑破坏神属性系统思路

声明

此思路是个人的思路,不代表暗黑破坏神等游戏的实际实现思路。核心代码示例使用C++语言,注意代码示例可能是代码片段,需要读者有游戏业务基础概念和C++语言基础。

游戏业务思路往往是比较开放的,其实不仅是游戏业务思路,个人接触到的游戏引擎Unity,Unreal,Cocos,还有以前公司的自研引擎对游戏世界的对象理解,gamePlay框架等很多方面的解决方案都存在差异。所以游戏行业不相信通用的解决方案,简单来说,以最小的维护成本满足需求才是王道。

概述

暗黑破坏神,流放之路,火炬之光等经典RPG游戏有令人眼花缭乱的角色属性词缀和相应的机制,搭配修改角色属性的装备,技能,Buff等形成很多有趣的流派。此文提供一种类似游戏的角色相关模块的实现思路,以角色属性子模块实现为引,也会涉及到其他角色相关系统。

思路

总体思路和类图

类暗黑破坏神属性系统思路

如图所示,角色模块大概会涉及到以下几个模块:

  1. Event:提供消息机制,实现消息传递。
  2. NVal:NumericValue数值对象,可以被修改器修改,计算所有修改器得到一个值,并向监听者广播数值更新消息。
  3. Entity:角色模块化,例如:属性,状态,Buff,技能等。
  4. 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 数值对象

考虑以下几种机制:

  1. 玩家掉血同步到UI显示。
  2. 玩家陷入眩晕状态时打断施法吟唱。

抽象:值变化时,抛出消息。


  1. 攻击力+10点/攻击力+30%/【X】技能持续期间,攻击力强制更改为100点。
  2. 【X】Buff眩晕1秒/【Y】Buff眩晕2秒。

抽象:值可被修改器修改,且修改器有优先级。比如上例中攻击力强制更改为100点的优先级低于攻击力+10点/攻击力+30%。值得一提的是:中了两个眩晕buff,其实是添加两个眩晕状态修改器,伪代码为: 值 = 原值(False)& 修改器1 & 修改器2。


NVal需要以下特性:

  1. 值变化时,抛出消息。
  2. 被修改器修改,有修改顺序。
  3. 值 可以是 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负责角色属性子模块对外部的接口:

  1. 获得某属性对象
  2. 属性值变化向监听者广播消息
  3. 向某属性添加修改器
/*属性组件*/ 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。
发表评论

评论已关闭。

相关文章