
ET框架Buff系统实战指南事件驱动与数值组件搭建低耦合技能系统【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET给战斗逻辑新增一个技能时开发者往往要连改四处扣血函数、血条UI、伤害飘字、成就统计全部在同一个函数里手工串联。改动落地后调一次伤害数值整条数值链跟着连环报错。本文基于开源项目ETUnity客户端C#服务端框架完整讲解ET框架Buff系统的设计思路用事件驱动解耦响应方用键值对数值组件统一属性计算再按Buff生命周期把它组装成可落地的技能模块。 先看效果一个中毒DeBuff的执行流程先定一个具体目标中毒效果命中后每2秒扣除12点生命持续8秒期间目标头顶图标常亮扣血时飘字血量归零触发死亡流程。执行流程拆开就是六步技能命中后服务端在目标身上挂一个中毒Buff实体实体里记录配置ID、施加者、结算间隔2秒和到期时间8秒后Buff激活时立即结算一次随后注册周期计时器到点就再结算一次每次结算只做一件事把HP键的新值写进目标的数值组件数值组件写入后统一重算并向场景抛出一个数值变化事件血条、飘字、死亡判定各自订阅这个事件谁关心谁处理扣血代码对它们一无所知到期时间到了Buff被销毁还原数值、注销计时器所有状态一次性清干净。注意第4步是整个流程的枢纽数值组件既是存储也是事件出口。后面两个核心机制都围绕它展开。 设计动机两组取舍写技能系统之前先想清楚两个老问题。第一数值变了之后一堆消费方UI、飘字、成就、日志怎么被通知第二几十种属性怎么存、怎么算。各有一组对照维度直接调用事件订阅通知方式扣血函数里逐个手写调用改完数值只抛一次事件消费方自己挂上新增消费方回到扣血函数里再插一行新增一个订阅类不碰原有逻辑依赖方向战斗逻辑依赖所有展示模块战斗逻辑只依赖事件名出错影响某个消费方抛异常整条扣血链中断各模块独立处理互不传染维度固定字段键值数值新增一种属性数值类加字段再手写一条计算公式加一个枚举项复用统一公式角色差异盗贼没有法力字段仍在只能当作没看见初始化不写这个键真实不存在配置表对接表格得把字段名映射到代码表里直接填NumericType枚举名Buff改属性每种属性各写一套修改逻辑统一写Add/Pct等因子键结论不复杂事件订阅解决谁需要知道键值数值解决怎么算。两者合起来Buff系统才不需要为每个技能写定制代码。⚙️ 核心机制事件订阅模式改值方不知道谁在看一句话原理改数值的一方只负责写值和抛事件所有响应逻辑以订阅类的形式挂在事件上两边互不引用。反例是把所有响应方写死在扣血函数里// 传统写法扣血函数手工串联所有消费方 public void ApplyPoisonDamage(Unit target) { long oldValue target.NumericComponent.Get(NumericType.HP); long newValue oldValue - 12; target.NumericComponent.Set(NumericType.HP, newValue); target.HpBarView.Refresh(oldValue, newValue); // 头顶血条 FloatingTextView.ShowAt(target, -12); // 伤害飘字 AchievementService.OnHpChanged(target); // 成就统计 }想加一个血量低于20%报警的逻辑就得再回到这个函数里插一行。函数越长越是牵一发动全身。正确写法是把写值和响应拆成两半public void ApplyPoisonDamage(Unit target) { long oldValue target.NumericComponent.Get(NumericType.HP); long newValue Math.Max(0, oldValue - 12); target.NumericComponent.Set(NumericType.HP, newValue); // 写完值只发事件通知里有实体引用、属性类型、新旧值 EventSystem.Instance.Publish(target.Scene(), new NumbericChange { Unit target, NumericType NumericType.HP, Old oldValue, New newValue }); }消费方各自独立成一个类// 血条模块订阅数值变化事件 [Event(SceneType.All)] public class NumbericChange_HpBar: AEventScene, NumbericChange { protected override void Run(Scene scene, NumbericChange change) { if (change.NumericType NumericType.HP) { change.Unit.GetComponentHpBarView() .Refresh(change.Old, change.New); } } }收益在于扣血函数从此稳定不变新增任何响应逻辑都只是多写一个订阅类某个订阅方出bug也只影响它自己。ET框架的整个运行逻辑就是由这些事件驱动的更多事件类型生命周期事件、消息事件等可参考事件机制EventSystem。分层数值计算一条公式管掉所有属性一句话原理每种属性最多拆成五个因子——基础值、增加值、百分比、最终增加、最终百分比——无论多少种属性共用同一条重算公式。反例是每种属性各写一条公式// 固定字段做法属性一多公式就失控 public int Speed (SpeedBase SpeedAdd) * (100 SpeedPct) / 100; public int MaxHp (MaxHpBase MaxHpAdd) * (100 MaxHpPct) / 100; public int MaxMp (MaxMpBase MaxMpAdd) * (100 MaxMpPct) / 100; // 再加一种属性就要再抄一条公式调整时要逐条排查修改正确写法是用键值字典存所有因子key由NumericType枚举定义// 每个因子一个key主键加后缀区分角色 // Speed1000, SpeedBase10001, SpeedAdd10002 ... public DictionaryNumericType, long NumericDic new(); // 任一因子变化时统一走重算 void Recalculate(NumericType type) { int finalKey (int)type / 10; int baseKey finalKey * 10 1; int addKey finalKey * 10 2; int pctKey finalKey * 10 3; int finalAddKey finalKey * 10 4; int finalPctKey finalKey * 10 5; long result ((Get(baseKey) Get(addKey)) * (100 Get(pctKey)) / 100 Get(finalAddKey)) * (100 Get(finalPctKey)) / 100; NumericDic[(NumericType)finalKey] result; // 重算完成抛出数值变化事件见上节订阅模式 }收益有三新增属性只需加枚举项公式一行不改盗贼没有法力值初始化时不写那组键即可不存在死字段配置表里填NumericType枚举名Buff就能直接定位到要改的因子。完整设计背景见数值组件设计可运行实现见numeric包源码。 落地路径Buff生命周期三步走机制讲完按由小到大搭起来。第一步定义容器与Buff实体。每个Unit身上挂一个BuffComponent作为它所有Buff的容器Buff本身是挂在容器下的子实体只存元信息具体数值行为交给配置表。[ComponentOf(typeof(Unit))] public class BuffComponent: Entity { // 同一类Buff可能同时存在多个实例用集合存放引用 public MultiMapSetint, EntityRefBuff flagBuffs new(); public MultiMapSetType, EntityRefBuff effectBuffs new(); public MultiMapSetint, EntityRefBuff configIdBuffs new(); } [ChildOf(typeof(BuffComponent))] public class Buff: Entity, IAwakeint, IDestroy { public int ConfigId { get; set; } // 配置表ID数值与效果节点都从这里取 public long Caster { get; set; } // 施加者ID public long CreateTime { get; set; } public int TickTime { get; set; } // 结算间隔毫秒0表示无周期效果 public long ExpireTime { get; set; } // 到期时间戳 public int Stack { get; set; } // 当前叠加层数 }第二步写数值修改逻辑。Buff不直接改字段而是写数值组件的因子键。以迅捷Buff为例激活时给速度百分比因子加15销毁时减15// 激活写百分比因子 public void ApplyHaste(Unit owner) { owner.NumericComponent.Set(NumericType.SpeedPct, owner.NumericComponent.Get(NumericType.SpeedPct) 15); } // 销毁等量回退动作必须成对出现 public void RevertHaste(Unit owner) { owner.NumericComponent.Set(NumericType.SpeedPct, owner.NumericComponent.Get(NumericType.SpeedPct) - 15); }写值会自动触发重算和事件所以这一步天然接好了前面两个核心机制不需要额外写任何通知代码。第三步接通生命周期。激活时应用初始效果并注册周期计时器到期销毁时还原数值。周期结算和到期处理在spell包中都有现成骨架// 激活后先结算一次再挂周期计时器 public static void StartTick(Buff buff) { if (buff.TickTime 0) { return; } buff.RunTickOnce(); // 首次结算中毒在这里写入HP新值 TimerComponent timer buff.Root().TimerComponent; buff.TickTimerId timer.NewRepeatedTimer( buff.TickTime, TimerInvokeType.BuffTickTimer, buff); } // 伤害结算只改数值响应交给事件 public static void Damage(Unit attacker, Unit target, int value) { NumericComponent numeric target.NumericComponent; long hp numeric.Get(NumericType.HP); if (hp value) { value (int)hp; // 扣到0为止不出现负血 } long v hp - value; numeric.Set(NumericType.HP, v); if (v 0) { EventSystem.Instance.Publish(target.Scene(), new UnitDie { Unit attacker, Target target }); } }销毁侧只需在Buff的Destroy扩展里调用TimerComponent.Remove注销计时器然后执行第二步的回退函数状态就完整还原了。完整实现可对照spell包源码它甚至把Buff效果做成了行为树节点和可视化编辑器。 进阶话题Buff叠加实现要点同一配置ID的Buff再次施加时中毒持续期内再次中毒不要新建实体而是找到已有实例把Stack层数加一再按新层数重写因子。例如攻击强化每层加8点攻击、上限3层层数变化时先按旧层数减去旧因子再加上新层数对应的值。要点是原地改、不新增——实体数量稳定数值重算不关心叠加了多少次UI刷新也只认最终值。到期清理要点两层保险创建时注册一次性到期计时器ExpireTime到了就销毁Buff销毁动作里做数值回退并注销所有计时器。最容易出问题的地方是应用和回退必须严格成对少写一条回退属性就会永久泄漏而且往往要等玩家下线才暴露。网络同步要点服务端持有数值唯一权威Buff的添加、移除、结算都在服务端执行再通过消息推给客户端客户端的处理器按消息重建同一套Buff结构只用来驱动头顶图标、特效这类表现层。BuffComponent本身已支持序列化和转移切图、断线重连时整套Buff可以直接整体同步不用逐条补状态。收尾与动手这套体系的价值在于把数值变化收敛成了一个事件出口和一条统一公式想看的自己订阅改值的无需关心谁在看。现在就可以动手照上面三步建一个中毒Buff接上每2秒12点的周期结算再订阅一次HP变化事件验证UI响应你就有了一套可复用的Buff原型。更多入口见项目README。【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考