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


    声明

    此思路是个人的思路,不代表暗黑破坏神等游戏的实际实现思路。核心代码示例使用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  //注意头文件
    
    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();
    		// ... 省略一些检查
    		CompDict[Name] = &Comp;
    	}
    	
    	template <typename Ty>
    	void RemoveComp(Ty& Comp);
    
    	template <typename Ty>
    	Ty* GetComp();
    
    protected:
    	map CompDict;
    };
    
    

    Event: 消息机制基础

    利用C++lambda特性绕开函数指针和类成员指的可调用对象,详细功能见注释。

    /*
    * 可以实现类似:
    *class A
    *{
    *public:
    *	void F1()
    *	{
    *		Event Eve = [this](int x) {this->F2(x); };
    *		Eve(10);
    *	}
    *
    *	void F2(int x)
    *	{
    *		printf("x = %d", x);
    *	}
    *};
    */
    template Params>
    class Event
    {
    private:
    	class EventImplBase
    	{
    		friend class Event;
    	private:
    		virtual void Run(Params ... args) const = 0;
    	};
    
    	template 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 Ty>
    	Event(Ty&& Rhs) : Impl(new EventImpl<Ty>(std::forward<Ty>(Rhs)))
    	{
    	}
    
    	template 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(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;
    template 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 
    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 typename MdfyPol = NValModPol >
    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 Fix;
    	NVal Pct;
    	NVal 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 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 
    class AttrNValMod : public NValMod, public IAttrNValMod
    {
    };
    
    class AttrMod : public IModifier
    {
    	virtual void Apply(IEntity& InEntity) override;
    	virtual void UnApply(IEntity& InEntity) override;
    protected:
    	list Mods;
    };
    
    void AttrMod::Apply(IEntity& InEntity)
    {
    	auto Comp = InEntity.GetComp();
    	if (Comp)
    	{
    		for (auto& It : Mods)
    		{
    			Comp->AddAttrMod(*It);
    		}
    	}
    }
    
    void AttrMod::UnApply(IEntity& InEntity)
    {
    	auto Comp = InEntity.GetComp();
    	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。
  • 相关阅读:
    【数据库入门到精通】mysql的存储过程实战
    我们为什么需要调用InitCommonControls?
    浏览器如何更改定位位置-VMLogin指纹浏览器Geolocation经纬度设置
    网安须知|什么是护网行动?什么是红蓝对抗?
    三天吃透Java并发八股文!
    小程序容器怎样助力智能家居
    华为配置旁挂三层组网直接转发示例
    老板让你Excel统计数据无从下手?没事,ChatGPT来帮你!
    Go基础17-明确哪些函数可以作为deferred函数?
    【行业动态】福建服装品牌如何完成差异化战略?
  • 原文地址:https://www.cnblogs.com/hggzhang/p/16558845.html