十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Buff 系统设计

Buff 系统设计 上一篇讲行为组件的时候减速组件干了件甩锅的事——它自己不管减速持续多久、什么时候消失而是给目标挂了个 buff然后就撒手不管了。ctx.target.addBuff(slow);// 挂上去剩下的交给buff系统那接锅的 buff 系统到底怎么运作这一篇就把这口锅接住。你会发现中毒、灼烧、加攻、护盾、眩晕、狂暴……游戏里几乎所有持续一段时间的状态全靠这一套系统撑着。先想清楚buff 和普通效果有啥不一样上一篇的伤害、治疗都是一锤子买卖——打完这一下事就结束了组件执行完就退场。但 buff 不一样它的核心特征是**“持续”**中毒接下来 5 秒每秒掉 10 血加攻接下来 10 秒攻击力 20%眩晕接下来 2 秒不能动这就带来一堆一锤子买卖不需要操心的问题它得记住自己还剩多久时间到了要自己消失它得在持续期间反复干活比如中毒每秒都要掉血它上身和消失时可能要改变角色的属性加攻上身时攻击力涨消失时得还原同一个 buff 反复叠加怎么办中毒叠三层是什么效果所以 buff 系统的本质是一套管理持续状态的机制。它要管这些状态的生老病死。一个 buff 长什么样先把 buff 这个数据结构定出来。它比普通效果多了时间和生命周期的概念classBuff{Stringid;// 唯一标识比如 poisonStringname;// 显示名中毒Entitycaster;// 谁给我挂的结算伤害归属要用Entityowner;// 我挂在谁身上floatduration;// 总共持续多久floatremaining;// 还剩多久每帧递减intstack;// 当前叠了几层intmaxStack;// 最多能叠几层floattickInterval;// 每隔多久触发一次中毒每1秒floattickTimer;// 距离下次触发还有多久}对比一下上一篇的EffectConfig多出来的全是跟时间和叠加有关的字段。这就是 buff 的特殊之处。buff 也用组件化三个生命周期钩子还记得上一篇的思路吗——每种效果做成一个统一接口的组件。buff 也照搬这套只不过 buff 的接口要复杂一点因为它有生命周期。一个 buff 在它的一生中有三个关键时刻上身 → 持续期间反复触发 → 消失 onApply onTick onRemove所以 buff 组件的接口设计成三个方法interfaceBuffBehavior{voidonApply(Buffbuff);// 刚挂上时执行一次voidonTick(Buffbuff);// 持续期间每隔 tickInterval 执行一次voidonRemove(Buffbuff);// 消失时执行一次}不是每个 buff 三个方法都要用。看具体是哪种 buff中毒——只关心 tick每秒掉血上身和消失时啥都不干classPoisonBuffimplementsBuffBehavior{publicvoidonApply(Buffbuff){}// 不用管publicvoidonTick(Buffbuff){floatdamage10*buff.stack;// 每层10点叠加变强buff.owner.hp-damage;showDamageNumber(buff.owner,damage);}publicvoidonRemove(Buffbuff){}// 不用管}加攻——只关心上身和消失上身加攻击力消失还原中间不用反复干活classAttackUpBuffimplementsBuffBehavior{publicvoidonApply(Buffbuff){buff.owner.attack*1.2f;// 上身攻击力涨20%}publicvoidonTick(Buffbuff){}// 不用管publicvoidonRemove(Buffbuff){buff.owner.attack/1.2f;// 消失还原}}看出规律了一锤子的属性变化用 onApply/onRemove 成对处理周期性的效果用 onTick 处理。这两对钩子基本能覆盖所有 buff。核心让 buff 自己活起来buff 挂上去只是开始得有个东西驱动它——每一帧去更新所有 buff 的时间该触发触发该消失消失。这个活儿交给挂在每个角色身上的buff 容器来干。classBuffContainer{privateListBuffbuffsnewArrayList();privateBuffRegistryregistry;// buff类型 → 组件 的对照表// 每一帧都调用deltaTime 是这一帧过了多少秒voidupdate(floatdeltaTime){IteratorBuffitbuffs.iterator();while(it.hasNext()){Buffbuffit.next();BuffBehaviorbehaviorregistry.get(buff.id);// 1. 处理周期性触发buff.tickTimer-deltaTime;if(buff.tickTimer0){behavior.onTick(buff);// 时间到触发一次buff.tickTimerbuff.tickInterval;// 重置计时}// 2. 处理总时长倒计时buff.remaining-deltaTime;if(buff.remaining0){behavior.onRemove(buff);// 时间耗尽执行消失逻辑it.remove();// 从容器移除}}}}这段是整个系统的发动机。它每帧做两件事给周期计时器倒数到点就 tick给总时长倒数耗尽就 remove。中毒的每秒掉血持续5秒、加攻的持续10秒后还原全靠这几行自动跑起来。老大难问题buff 叠加怎么处理同一个 buff 再次挂上来该怎么办这是 buff 系统最容易出乱子的地方。现实里有好几种不同的规则得分清楚规则一叠层数。中毒再中一次叠成两层掉血翻倍。规则二刷新时间。已经中毒了再中一次不叠层但持续时间重新计满俗称续毒。规则三又叠层又刷新。最常见叠一层同时把时间刷新。规则四独立共存。两个中毒互不干扰各自倒计时少见但有些游戏这么设计。处理叠加的逻辑放在添加 buff的入口统一判断voidaddBuff(BuffnewBuff){BuffexistingfindBuff(newBuff.id);// 身上已经有同款吗if(existingnull){// 没有直接挂上buffs.add(newBuff);registry.get(newBuff.id).onApply(newBuff);return;}// 已经有了按叠加规则处理switch(newBuff.stackRule){caseSTACK:// 只叠层existing.stackMath.min(existing.stack1,existing.maxStack);break;caseREFRESH:// 只刷新时间existing.remainingexisting.duration;break;caseSTACK_REFRESH://又叠层又刷新 existing.stackMath.min(existing.stack1,existing.maxStack);existing.remainingexisting.duration;break;caseINDEPENDENT:// 独立共存buffs.add(newBuff);registry.get(newBuff.id).onApply(newBuff);break;}}叠加规则一定要写进配置让策划自己定。这个 buff 到底是叠层还是刷新是策划的设计决策不该由程序员拍脑袋。一个隐藏的大坑叠层时属性怎么算上面加攻的例子我写的是onApply里attack * 1.2。但如果这个 buff 能叠 3 层问题就来了——叠层的时候只是stack并没有再次调用onApply那多出来的两层攻击力加成怎么生效这是 buff 系统最阴险的坑属性类 buff 遇到叠层简单的上身加、消失减就不够用了。有个更干净的做法buff 不直接改属性而是角色属性每次都重新算一遍。// 角色的最终攻击力是实时算出来的不是被buff改来改去的floatgetFinalAttack(){floatattackbaseAttack;// 基础攻击// 遍历所有buff把加成累加上去for(Buffbuff:buffs){if(buff.id.equals(attack_up)){attack*(10.2f*buff.stack);// 按层数算}}returnattack;}这样一来buff 只管记录我有几层攻击力多少永远是现算的。叠层、掉层、消失属性自动就对了根本不用在 onApply/onRemove 里手动加加减减也就不会出现加了没减回去这种经典 bug。代价是每次读属性都要遍历 buff有性能开销。数据不常变、读取频繁的话可以加个缓存buff 变动时才重算。怎么权衡看项目。和技能系统怎么接上绕了一圈回到最初的问题——上一篇的减速组件是怎么把锅甩给 buff 系统的现在能看清全貌了// 技能行为组件里classSlowEffectimplementsSkillEffect{publicvoidapply(EffectContextctx){BuffslownewBuff();slow.idslow;slow.durationctx.config.duration;slow.remainingctx.config.duration;slow.valuectx.config.value;slow.stackRuleStackRule.REFRESH;// 减速一般是刷新不叠加ctx.target.buffContainer.addBuff(slow);// 甩锅给buff系统}}整条链路就串起来了配置里写 { type: slow, duration: 3 } ↓ 技能引擎读到找到 SlowEffect 组件 ↓ SlowEffect 造一个 buff扔进目标的 BuffContainer ↓ BuffContainer 每帧更新到点自动移除 ↓ 角色算移速时实时读取身上的减速buff技能系统负责产生buffbuff 系统负责管理buff各管一段接口清清爽爽。几个还要注意的点免疫和驱散。真实游戏里有净化能移除减益 buff有免疫能挡住某些 buff。所以 buff 最好带上分类标签增益/减益、可驱散/不可驱散、控制类/属性类方便这些机制批量筛选处理。控制类 buff 要能互相打断和抵抗。眩晕、冰冻、击飞这些控制 buff往往有递减抗性连续被控后续控制时间缩短还涉及互相覆盖的优先级。这块规则复杂得单独设计别硬塞进通用逻辑。buff 消失的原因不止时间到。还有被驱散、角色死亡、被更高优先级 buff 顶掉等等。这些情况都要正确触发 onRemove否则属性还原不了角色属性就乱套了比如加攻 buff 没触发 onRemove攻击力永久涨着。别让 buff 数量失控。有些场景下 buff 会疯狂叠加比如每帧都挂一个要设上限否则容器越来越大性能崩了。一句话收尾Buff 系统这一篇的核心buff 就是带时间的持续状态用 onApply / onTick / onRemove 三个钩子描述它的一生用一个每帧更新的容器驱动它自动运转。叠加规则交给配置属性计算尽量用实时重算而不是手动加减从根上避免加了忘减的坑。到这里技能系统的骨架就比较完整了数据逻辑分离打地基数据结构组织数据行为组件执行瞬时效果buff 系统管理持续状态。四块拼起来一套能扛住策划各种花活的技能框架就成型了。再往下就是把它们联动起来的事件系统——“击杀后回血”“受击时反弹”每third次攻击暴击这类触发式设计全靠事件机制串联。那是下一个话题了。
返回列表