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

声明

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

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

概述

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

思路

总体思路和类图

image

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

  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。