ET篇:ETBook笔记(5.6 数值组件设计) 灰太狼 2022-02-17 09:58 345阅读 0赞 # **GitHub原文地址:[数值组件设计][Link 1]** # ### **黑体字为作者(熊猫大佬)原创,红色为个人理解** ### 类似魔兽世界,moba这种技能极其复杂,灵活性要求极高的技能系统,必须需要一套及其灵活的数值结构来搭配。数值结构设计好了,实现技能系统就会非常简单,否则就是一场灾难。比如魔兽世界,一个人物的数值属性非常之多,移动速度,力量,怒气,能量,集中值,魔法值,血量,最大血量,物理攻击,物理防御,法术攻击,法术防御,等等多达几十种之多。属性跟属性之间又相互影响,buff又会给属性增加绝对值,增加百分比,或者某种buff又会在算完所有的增加值之后再来给你翻个倍。 ## 普通的做法: ## 一般就是写个数值类: class Numeric { public int Hp; public int MaxHp; public int Speed; // 能量 public int Energy; public int MaxEnergy; // 魔法 public int Mp; public int MaxMp; ..... } 仔细一想,我一个盗贼使用的是能量,为什么要有一个Mp的值?我一个法师使用的是魔法为什么要有能量的字段?纠结这个搞毛,当作没看见不就行了吗?实在不行,我来个继承? // 法师数值 calss MageNumeric: Numeric { // 魔法 public int Mp; public int MaxMp; } // 盗贼数值 calss RougeNumeric: Numeric { // 能量 public int Energy; public int MaxEnergy; } 10个种族,每个种族7,8种英雄,光这些数值类继承关系,你就得懵逼了吧。面向对象是难以适应这种灵活的复杂的需求的。 再来看看Numeric类,每种数值可不能只设计一个字段,比如说,我有个buff会增加10点Speed,还有种buff增加50%的speed,那我至少还得加三个二级属性字段 class Numeric { // 速度最终值 public int Speed; // 速度初始值 public int SpeedInit; // 速度增加值 public int SpeedAdd; // 速度增加百分比值 public int SpeedPct; } SpeedAdd跟SpeedPct改变后,进行一次计算,就可以算出最终的速度值。buff只需要去修改SpeedAdd跟SpeedPct就行了。 Speed = (SpeedInit + SpeedAdd) * (100 + SpeedPct) / 100 每种属性都可能有好几种间接影响值,可以想想这个类是多么庞大,初略估计得有100多个字段。麻烦的是计算公式基本一样,但是就是无法统一成一个函数,例如MaxHp,也有buff影响 class Numeric { public int Speed; public int SpeedInit; public int SpeedAdd; public int SpeedPct; public int MaxHp; public int MaxHpInit; public int MaxHpAdd; public int MaxHpPct; } 也得写个Hp的计算公式 MaxHp=(MaxHpInit + MaxHpAdd) * (100 + MaxHpPct) / 100 几十种属性,就要写几十遍,并且每个二级属性改变都要正确调用对应的公式计算. 非常麻烦! 这样设计还有个很大的问题,buff配置表填对应的属性字段不是很好填,例如疾跑buff(增加速度50%),在buff表中怎么配置才能让程序简单的找到并操作SpeedPct字段呢?不好搞。 ## ET框架采用了Key Value形式保存数值属性 ## using System.Collections.Generic; namespace Model { public enum NumericType { Max = 10000, Speed = 1000, SpeedBase = Speed * 10 + 1, SpeedAdd = Speed * 10 + 2, SpeedPct = Speed * 10 + 3, SpeedFinalAdd = Speed * 10 + 4, SpeedFinalPct = Speed * 10 + 5, Hp = 1001, HpBase = Hp * 10 + 1, MaxHp = 1002, MaxHpBase = MaxHp * 10 + 1, MaxHpAdd = MaxHp * 10 + 2, MaxHpPct = MaxHp * 10 + 3, MaxHpFinalAdd = MaxHp * 10 + 4, MaxHpFinalPct = MaxHp * 10 + 5, } public class NumericComponent: Component { public readonly Dictionary<int, int> NumericDic = new Dictionary<int, int>(); public void Awake() { // 这里初始化base值 } public float GetAsFloat(NumericType numericType) { return (float)GetByKey((int)numericType) / 10000; } public int GetAsInt(NumericType numericType) { return GetByKey((int)numericType); } public void Set(NumericType nt, float value) { this[nt] = (int) (value * 10000); } public void Set(NumericType nt, int value) { this[nt] = value; } public int this[NumericType numericType] { get { return this.GetByKey((int) numericType); } set { int v = this.GetByKey((int) numericType); if (v == value) { return; } NumericDic[(int)numericType] = value; Update(numericType); } } private int GetByKey(int key) { int value = 0; this.NumericDic.TryGetValue(key, out value); return value; } public void Update(NumericType numericType) { if (numericType < NumericType.Max) { return; } int final = (int) numericType / 10; int bas = final * 10 + 1; int add = final * 10 + 2; int pct = final * 10 + 3; int finalAdd = final * 10 + 4; int finalPct = final * 10 + 5; // 一个数值可能会多种情况影响,比如速度,加个buff可能增加速度绝对值100,也有些buff增加10%速度,所以一个值可以由5个值进行控制其最终结果 // final = (((base + add) * (100 + pct) / 100) + finalAdd) * (100 + finalPct) / 100; this.NumericDic[final] = ((this.GetByKey(bas) + this.GetByKey(add)) * (100 + this.GetByKey(pct)) / 100 + this.GetByKey(finalAdd)) * (100 + this.GetByKey(finalPct)) / 100; Game.EventSystem.Run(EventIdType.NumbericChange, this.Entity.Id, numericType, this.NumericDic[final]); } } } 1.数值都用key value来保存,key是数值的类型,由NumericType来定义,value都是整数,float型也可以转成整数,例如乘以1000;key value保存属性会变得非常灵活,例如法师没有能量属性,那么初始化法师对象不加能量的key value就好了。盗贼没有法力值,没有法术伤害等等,初始化就不用加这些。 2.魔兽世界中,一个数值由5个值来影响,可以统一使用一条公式: final = (((base + add) * (100 + pct) / 100) + finalAdd) * (100 + finalPct) / 100; 比如说速度值speed,有个初始值speedbase,有个buff1增加10点绝对速度,那么buff1创建的时候会给speedadd加10,buff1删除的时候给speedadd减10,buff2增加20%的速度,那么buff2创建的时候给speedpct加20,buff2删除的时候给speedpct减20.甚至可能有buff3,会在最终值上再加100%,那么buff3将影响speedfinalpct。这5个值发生改变,统一使用Update函数就可以重新计算对应的属性了。buff配置中对应数值字段相当简单,buff配置中填上相应的NumericType,程序很轻松就能操作对应的数值。 3.属性的改变可以统一抛出事件给其它模块订阅,写一个属性变化监视器变得非常简单。例如成就模块需要开发一个成就生命值超过1000,会获得长寿大师的成就。那么开发成就模块的人将订阅HP的变化: /// 监视hp数值变化 [NumericWatcher(NumericType.Hp)] public class NumericWatcher_Hp : INumericWatcher { public void Run(long id, int value) { if (value > 1000) { //获得成就长寿大师成就 } } } 同理,记录一次金币变化大于10000的异常日志等等都可以这样做。 有了这个数值组件,一个moba技能系统可以说已经完成了一半。 ### **下面是个人笔记** ### 先说几个注意点 1. 枚举类型的值不是真正意义上的值,它是这个枚举所对应的ID,类似定义proto文件message\{\}体一样 2. 数值组件和数值变化监听组件都要用到一个id,这个id是当前数值组件所在实体(Entity)的id 3. 使用时需要给Game.Scene添加数值监听组件(NumericWatcherComponent: 一般是一个),并且给需要使用数值组件的实体(Entity:可以任意个)添加数值组件(NumericComponent ) 4. 对于直接的枚举类型的对比,都是比较ID的大小 5. 之所以final要/10是因为确保改变的是自己想要的类型 6. 不要直接对想取得的数值进行修改和赋值,不然无效 下面我用一个小例子来和大家一起学习吧 我们直接去客户端创建小骷髅的代码那里,M2C\_CreateUnitsHandler using ETModel; using PF; using Vector3 = UnityEngine.Vector3; namespace ETHotfix { [MessageHandler] public class M2C_CreateUnitsHandler: AMHandler<M2C_CreateUnits> { protected override void Run(ETModel.Session session, M2C_CreateUnits message) { UnitComponent unitComponent = ETModel.Game.Scene.GetComponent<UnitComponent>(); //为Game.Scene添加数值监听组件 ETModel.Game.Scene.AddComponent<NumericWatcherComponent>(); foreach (UnitInfo unitInfo in message.Units) { if (unitComponent.Get(unitInfo.UnitId) != null) { continue; } //根据不同ID,创建小骷髅 Unit unit = UnitFactory.Create(unitInfo.UnitId); //为小骷髅添加数值组件 NumericComponent numericComponent = unit.AddComponent<NumericComponent>(); //为小骷髅设置生命值,这将触发数值改变事件 numericComponent.Set(NumericType.HpBase, 1000); unit.Position = new Vector3(unitInfo.X, unitInfo.Y, unitInfo.Z); } } } } 这里为了做示例,修改了一下NumericComponent的Awake public void Awake() { //注意,这两个语句都将触发数值改变组件,只是没有写Max的处理函数,所以会没有反应 this[NumericType.Max] = 999999999; this[NumericType.HpBase] = 1; } 同样修改NumericWatcher\_Hp\_ShowUI namespace ETModel { /// <summary> /// 监视hp数值变化,改变血条值 /// </summary> [NumericWatcher(NumericType.Hp)] public class NumericWatcher_Hp_ShowUI : INumericWatcher { public void Run(long id, int value) { Log.Info("小骷髅ID为"+id+"血量变化了,变化之后的值为:"+value); } } } 运行游戏 ![20190419140410873.png][] ### 总结:一方面是理解了猫大的数值组件设计,另一方面,我们可以预见ET的组件化编程和异步语法将会极大方便与规范我们的开发,可以说,项目代码优美程度只取决于我们的想象力! ### [Link 1]: https://github.com/egametang/ET/blob/master/Book/5.6%E6%95%B0%E5%80%BC%E7%BB%84%E4%BB%B6%E8%AE%BE%E8%AE%A1.md [20190419140410873.png]: /images/20220217/b2e7777752ac4cc9809803fc11a5cb13.png
还没有评论,来说两句吧...