当前位置: 首页 >  在线学习 >  暗黑破坏神词缀实现思路2.0

暗黑破坏神词缀实现思路2.0

导读:代码示例.Github地址:暗黑破坏神词缀实现思路-示例代码.序言.暗黑类游戏非常经典,之前玩过很多,也尝试过写过实现的思路.最近又在之前的思路下有了新的想法。.我们先来分析下该类型游戏的特点和其词缀机制:.暗黑类游戏.我玩过的暗黑类游戏主要有:暗黑破坏神,火炬之光,流放之路。我

代码示例

Github地址:暗黑破坏神词缀实现思路-示例代码

序言

暗黑类游戏非常经典,之前玩过很多,也尝试过写过实现的思路
最近又在之前的思路下有了新的想法。

我们先来分析下该类型游戏的特点和其词缀机制:

暗黑类游戏

我玩过的暗黑类游戏主要有:暗黑破坏神,火炬之光,流放之路。我认为暗黑类游戏的最突出的特点,就是各种各样的词缀,让玩家刷刷刷,按照自己的策略刷出合适的词缀搭配和提升其数值,从而获得割草和挑战更高数值怪物的快感。

词缀

词缀按照我的理解就是修饰器,它可以修饰(或覆盖)原本的各种机制(属性,技能,状态…),下面我们举几个有趣的例子:

  • 属性类:
    • 你的防御力为0,你的攻击力上升原本防御力的1.5倍
    • 你的防御力上升攻击力的10%
    • 你的火焰抗性等于冰冷抗性
    • 你的50%火焰攻击力转换成闪电攻击力
  • 机制计算类:
    • 战斗机制:
      • 你不会被暴击
      • 你的伤害是幸运的(比如伤害是20-40,取值时靠近40的概率增加)
      • 你有50%概率避免中毒
      • 你受到的火焰伤害50%使用冰冷抗性抵抗
    • 技能/Buff机制:
      • 施加冰缓时,若已被冰缓则施加冰冻
      • 对标记的目标造成额外伤害
      • 你的攻击技能有5%概率追加释放【虚空之雨】
    • 地图机制:
      • 你在地图中受到【时空锁链】诅咒
      • 地图中包含一个额外宝箱
      • 你在地图中获得的金币翻倍
      • 地图中有【堕落的叛徒·乌崔德】
    • 其他机制:
      • 你不能装备武器,你的攻击力翻倍
      • 你获得主动技能【猫之势】
      • 你可以选择其他职业的一个技能
      • 你从装备中获取的属性提升50%,但你只能装备被【腐化】的装备

可以看到,词缀五花八门。有些词缀非属性类型的词缀比如(不会被暴击/50%避免中毒)也是可以通过属性或者状态来实现,但有些还需要其他机制处理(如标记追加伤害,需要在战斗模块进行处理)。

在暗黑类的众多词缀中,其中很多都是关联属性和状态的,而状态和属性在我的实现中比较像(后面会提到),所以这里详细说下我对属性模块和其修改器的实现思路 ,一些思想会应用于其他模块,并会简要的提出其他模块可能会不同的地方

我使用c++语言进行实现,其实思想都是一样的,使用lua/python等在编码效率等方面会更好些。

需求分析

从上述中,词缀影响到的机制非常的多。在实现时,可以选择更加灵活的语言(lua/python等)进行实现。配置方面,配表+脚本(一般使用配表,一些复杂的效果必要时调用脚本)是可行的,如果编写编辑器的话可能会更好一些(当然,程序侧的开发维护成本会增加,但如果游戏内容多的话,总体成本应当是下降的)。

结构示意图


Entity下挂载了一组Comp组件 ,包含属性、状态等。装备、Buff等挂载一组Affix词缀 ,词缀又包含了一组修改器Modifier (可能有属性、状态、甚至是外貌、动作等修改器),修改器在应用的时候作用到各个组件的业务 中(比如,属性修改器作用的属性组件的属性实例中,如增加攻击力)。若是使用观察者模式,则类似图中AttrBinder。外面把Binder注册进来,当属性变化时主动通知各个Binder属性变化

EC模块

在角色相关的系统中,EC模式(Entity- Component)是比较常见且好用的,它把(这里是角色,但是Entity不仅限是角色)Entity的各个业务拆分开来,降低代码的复杂度和耦合性。

这里有一个使用什么作为存component的key的问题,我考虑了三种方式:

  1. 使用枚举,如EComp::Attr
  2. 使用字符串, 如 “Attr”
  3. 使用RTTI(运行时类型信息 Run-time Type Information)生成的类的名字信息的字符串 typeid(Ty).name()

使用RTTI类名字符串

/*取类名String*/
#include <typeinfo> //注意头文件

struct ClassName
{
	template <typename Ty>
	static string Get()
	{
		static string name = typeid(Ty).name();
		return name;
	}
};

/*获取组件*/
template<class T>
std::shared_ptr<T> Entity::GetComp<T>()
{
	string name = ClassName::Get<T>();

	return std::dynamic_pointer_cast<T>(comp_map[name])
}

三种方式对比分析
2比较方便,代码量较少,但1更加规范尤其是多人合作项目推荐使用方式1。
方式3同2一样方便(在c++上其实比2更加方便),不像2那样容易出错(有代码检查和提示),但是不像枚举那样罗列了所有组件类型,且RTTI依赖编译器,不确定是否有些情况会有问题。
我总结了下原则:
在跨系统模块中,或者是动态生成的东西,使用字符串作为参数更加灵活和方便,其他情况使用枚举保证方便维护和合作

属性模块

如结构图示:

  1. 有一个属性组件AttrComp挂载在Entity上,管理了一堆属性Attr
  2. Attr可以接收Binder绑定器和Modifier修改器。当Modifier进来会重新收集所有Modifier的数据并计算,并通知Binder。需要说明的是:
    • 在我的设计中Attr没有所谓的默认值,如果角色天生带有一些基础属性,则由角色/职业相关组件添加Modifier进来
    • Binder的思想是观察者模式 ,Binder是在观察者的回调函数上进一步的封装,以减少重复的逻辑。比如多个面板有属性数值显示,就可以把获取属性数值,赋值给UI控件封装成一个Binder在多个面板上复用,只需传入控件和属性类型。也可以传入lambda表达式作为一般的回调使用,如这里的AttrBinderLambda。注意Binder在刚绑定时也会触发回调
  3. Affix词缀包含了多个Modifier,在Apply函数中应用到Entitt的各个模块中,如属性应用到AttrComp指定类型的属性Attr

应用实例
AttrData示例:

struct AttrData
{
	int fix = 0;
	int more = 0;
	int total = 0;
	int pct = 0;
	int override = 0;
	bool bOverride = false;
	int final = 0;
};

int raw = fix * (1 + more) * (1 + total) + (1 + pct);
int final = bOverride ? override : raw;

词缀效果应用:

  1. 你的攻击力:增加10(fix)/ 增加150%(more)/ 总增50%(total)

  2. 你的攻击力为0,你的防御力为上升原本攻击力的150%
    这里2应用BinderModifier的实现:

    int AttrUtil::GetRawOverride(const AttrData& data) { int tmp = GetRawPct(data); tmp *= (1 + data.pct / 100.f); return tmp; }

    int AttrUtil::GetRawPct(const AttrData& data) { int tmp = 0; tmp += data.fix; tmp *= (1 + data.more / 100.f); tmp *= (1 + data.total / 100.f); return tmp; }

    void AttrModifyIncByAttr::Modify(AttrData& data) { data.fix += v; }

    void AttrModifyIncByAttr::Init() { auto func = this { if (target == from) return; int tmp = (AttrUtil::GetRawOverride(data)) * (pct / 100.f); SetVal(tmp); };

    bind = std::make_shared<AttrBinderLambda>(func);
    

    }

    void AttrModifyIncByAttr::Apply(const SP(Entity)& in_ent) { if (in_ent) { auto comp = in_ent->GetComp(EComp::Attr); if (comp) { comp->AddBinder(from, bind); } } else { if (auto lock = ent.lock()) { auto comp = lock->GetComp(EComp::Attr); if (comp) { comp->RemBinder(from, bind); } } } AttrModify::Apply(in_ent); }

    void AttrModify::SetVal(int in) { if (v == in) return; v = in; Upd(); }

    void AttrModify::Upd() { if (auto lock = ent.lock()) { auto comp = lock->GetComp(GetCompTy()); if (comp) { comp->UpdMod(target); } } }

可以看到:这里在初始化时,创建了一个Binder,在回调时根据攻击力(from)计算修饰的值 ,SetVal时必要时会通知防御力属性(target)更新属性。
即:攻击力变化->修饰值变化->防御力变化。
诸如其他的属性词缀如一半的闪避值转化成攻击力,同理。
(注意这里防止转化之间的嵌套 ,比如攻击上升防御的一半,防御又上升攻击的一半,需要根据需求防止循环)

这里的设计主要是考虑复杂的需求和灵活 :比如以后有什么获取所有装备提供的攻击力等需求可以快速的拓展。当然如果属性系统没有那么多花样,这里虽然能满足需求,但是在代码复杂度和效率上可能会差一些。

其他系统

多数的情况下,修改器都是更新数据(如属性、状态、标志位等),联动到更新这些数据对应的业务,也有一些是在后续的逻辑中查询这些数据(如战斗系统查询追伤标记位(有可能是某个buf)追加伤害)

状态系统
在我的设计中,状态系统管理的多数是Bool 值,如:

  • 是否可以行动?
  • 是否可以释放技能?
  • 是否能够移动?

这些值往往使用乘法运算规则,如原本是可以行动,有个眩晕和封印技能同时添加状态修改器 ,即val = 1 * 0 * 0 = 0,值为0不能行动。

当然也有一些其他情况(如标记层数、中毒等)使用数字 (Number)

战斗系统
在我的设计中,战斗系统和状态、属性系统是紧密关联的。
战斗系统会频繁的查改属性和状态 。战斗系统主要负责战斗的流程处理和结算,并调用其他系统进行状态变更和表现处理。如调用伤害计算公式结算伤害,并修改属性系统HP值。

内容
  • 计算机网络概念汇总
    计算机网络概念汇总
    2023-12-05
    1. 模型结构.五层模型 :.应用层、传输层、网络层、数据链路层、物理层。.应用层 :为应用程序提供交互服务。在互联网中
  • UE开发使用Rider时缓存干爆C盘的解决方案
    UE开发使用Rider时缓存干爆
    2023-12-03
    我们在使用Rider开发UE时,Ride会为每一个项目创建一个解决方案缓存,如果开几个新项目写测试demo,我们的C盘会
  • Unity3D 横板跳跃游戏半成品demo源代码
    Unity3D 横板跳跃游戏半成
    2023-12-08
    项目介绍:.基于B站的 Unity3D新手教程 进行学习制作,但视频中的做法有很多BUG,此demo是经过优化,几乎修复
  • 数据分析师如何用SQL解决业务问题?
    数据分析师如何用SQL解决业务问
    2023-12-03
    本文来自问答。.提问:数据分析人员需要掌握sql到什么程度?.请问做一名数据分析人员,在sql方面需要掌握到什么程度呢?
  • Unity 中的存档系统(本地存档)
    Unity 中的存档系统(本地存
    2023-12-09
    思想.在游戏过程中,玩家的背包、登录、人物系统都与数据息息相关,无论是一开始就设定好的默认数据,还是可以动态存取的数据,
  • unity 实现自定义class深度拷贝 deep copy 深度复制 引用类型复制
    unity 实现自定义class
    2023-12-09
    气死我了,搜半天没有,全让序列化再反序列化,又不方便又不美观。结果自己试着一写就通,两行完事。.首先先安装Newtons