news 2026/8/23 14:26:57

Buff 系统设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Buff 系统设计

上一篇讲行为组件的时候,减速组件干了件"甩锅"的事——它自己不管减速持续多久、什么时候消失,而是给目标挂了个 buff,然后就撒手不管了。

ctx.target.addBuff(slow);// 挂上去,剩下的交给buff系统

那接锅的 buff 系统到底怎么运作?这一篇就把这口锅接住。你会发现,中毒、灼烧、加攻、护盾、眩晕、狂暴……游戏里几乎所有"持续一段时间的状态",全靠这一套系统撑着。

先想清楚:buff 和普通效果有啥不一样

上一篇的伤害、治疗,都是一锤子买卖——打完这一下,事就结束了,组件执行完就退场。

但 buff 不一样,它的核心特征是**“持续”**:

  • 中毒:接下来 5 秒,每秒掉 10 血
  • 加攻:接下来 10 秒,攻击力 +20%
  • 眩晕:接下来 2 秒,不能动

这就带来一堆一锤子买卖不需要操心的问题:

  • 它得记住自己还剩多久,时间到了要自己消失
  • 它得在持续期间反复干活(比如中毒每秒都要掉血)
  • 它上身和消失时,可能要改变角色的属性(加攻上身时攻击力涨,消失时得还原)
  • 同一个 buff 反复叠加怎么办?中毒叠三层是什么效果?

所以 buff 系统的本质,是一套"管理持续状态"的机制。它要管这些状态的生老病死。

一个 buff 长什么样

先把 buff 这个数据结构定出来。它比普通效果多了"时间"和"生命周期"的概念:

classBuff{Stringid;// 唯一标识,比如 "poison"Stringname;// 显示名,"中毒"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){floatdamage=10*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{privateList<Buff>buffs=newArrayList<>();privateBuffRegistryregistry;// buff类型 → 组件 的对照表// 每一帧都调用,deltaTime 是这一帧过了多少秒voidupdate(floatdeltaTime){Iterator<Buff>it=buffs.iterator();while(it.hasNext()){Buffbuff=it.next();BuffBehaviorbehavior=registry.get(buff.id);// 1. 处理周期性触发buff.tickTimer-=deltaTime;if(buff.tickTimer<=0){behavior.onTick(buff);// 时间到,触发一次buff.tickTimer+=buff.tickInterval;// 重置计时}// 2. 处理总时长倒计时buff.remaining-=deltaTime;if(buff.remaining<=0){behavior.onRemove(buff);// 时间耗尽,执行消失逻辑it.remove();// 从容器移除}}}}

这段是整个系统的发动机。它每帧做两件事:给周期计时器倒数,到点就 tick;给总时长倒数,耗尽就 remove。中毒的"每秒掉血持续5秒"、加攻的"持续10秒后还原",全靠这几行自动跑起来。

老大难问题:buff 叠加怎么处理

同一个 buff 再次挂上来,该怎么办?这是 buff 系统最容易出乱子的地方。现实里有好几种不同的规则,得分清楚:

规则一:叠层数。中毒再中一次,叠成两层,掉血翻倍。

规则二:刷新时间。已经中毒了,再中一次不叠层,但持续时间重新计满(俗称"续毒")。

规则三:又叠层又刷新。最常见,叠一层同时把时间刷新。

规则四:独立共存。两个中毒互不干扰,各自倒计时(少见,但有些游戏这么设计)。

处理叠加的逻辑,放在"添加 buff"的入口统一判断:

voidaddBuff(BuffnewBuff){Buffexisting=findBuff(newBuff.id);// 身上已经有同款吗?if(existing==null){// 没有,直接挂上buffs.add(newBuff);registry.get(newBuff.id).onApply(newBuff);return;}// 已经有了,按叠加规则处理switch(newBuff.stackRule){caseSTACK:// 只叠层existing.stack=Math.min(existing.stack+1,existing.maxStack);break;caseREFRESH:// 只刷新时间existing.remaining=existing.duration;break;caseSTACK_REFRESH://又叠层又刷新 existing.stack=Math.min(existing.stack+1,existing.maxStack);existing.remaining=existing.duration;break;caseINDEPENDENT:// 独立共存buffs.add(newBuff);registry.get(newBuff.id).onApply(newBuff);break;}}

叠加规则一定要写进配置,让策划自己定。这个 buff 到底是叠层还是刷新,是策划的设计决策,不该由程序员拍脑袋。

一个隐藏的大坑:叠层时属性怎么算

上面加攻的例子,我写的是onApplyattack *= 1.2。但如果这个 buff 能叠 3 层,问题就来了——叠层的时候只是stack++,并没有再次调用onApply,那多出来的两层攻击力加成怎么生效?

这是 buff 系统最阴险的坑:属性类 buff 遇到叠层,简单的"上身加、消失减"就不够用了。

有个更干净的做法:buff 不直接改属性,而是角色属性每次都"重新算一遍"。

// 角色的最终攻击力,是实时算出来的,不是被buff改来改去的floatgetFinalAttack(){floatattack=baseAttack;// 基础攻击// 遍历所有buff,把加成累加上去for(Buffbuff:buffs){if(buff.id.equals("attack_up")){attack*=(1+0.2f*buff.stack);// 按层数算}}returnattack;}

这样一来,buff 只管记录"我有几层",攻击力多少永远是现算的。叠层、掉层、消失,属性自动就对了,根本不用在 onApply/onRemove 里手动加加减减,也就不会出现"加了没减回去"这种经典 bug。

代价是每次读属性都要遍历 buff,有性能开销。数据不常变、读取频繁的话,可以加个缓存,buff 变动时才重算。怎么权衡看项目。

和技能系统怎么接上

绕了一圈,回到最初的问题——上一篇的减速组件是怎么把锅甩给 buff 系统的?现在能看清全貌了:

// 技能行为组件里classSlowEffectimplementsSkillEffect{publicvoidapply(EffectContextctx){Buffslow=newBuff();slow.id="slow";slow.duration=ctx.config.duration;slow.remaining=ctx.config.duration;slow.value=ctx.config.value;slow.stackRule=StackRule.REFRESH;// 减速一般是刷新,不叠加ctx.target.buffContainer.addBuff(slow);// 甩锅给buff系统}}

整条链路就串起来了:

配置里写 { "type": "slow", "duration": 3 } ↓ 技能引擎读到,找到 SlowEffect 组件 ↓ SlowEffect 造一个 buff,扔进目标的 BuffContainer ↓ BuffContainer 每帧更新,到点自动移除 ↓ 角色算移速时,实时读取身上的减速buff

技能系统负责"产生"buff,buff 系统负责"管理"buff,各管一段,接口清清爽爽。

几个还要注意的点

免疫和驱散。真实游戏里,有"净化"能移除减益 buff,有"免疫"能挡住某些 buff。所以 buff 最好带上分类标签(增益/减益、可驱散/不可驱散、控制类/属性类),方便这些机制批量筛选处理。

控制类 buff 要能互相打断和抵抗。眩晕、冰冻、击飞这些控制 buff,往往有"递减抗性"(连续被控,后续控制时间缩短),还涉及互相覆盖的优先级。这块规则复杂,得单独设计,别硬塞进通用逻辑。

buff 消失的原因不止"时间到"。还有被驱散、角色死亡、被更高优先级 buff 顶掉等等。这些情况都要正确触发 onRemove,否则属性还原不了,角色属性就乱套了(比如加攻 buff 没触发 onRemove,攻击力永久涨着)。

别让 buff 数量失控。有些场景下 buff 会疯狂叠加(比如每帧都挂一个),要设上限,否则容器越来越大,性能崩了。

一句话收尾

Buff 系统这一篇的核心:

buff 就是"带时间的持续状态",用 onApply / onTick / onRemove 三个钩子描述它的一生,用一个每帧更新的容器驱动它自动运转。叠加规则交给配置,属性计算尽量用"实时重算"而不是"手动加减",从根上避免加了忘减的坑。

到这里,技能系统的骨架就比较完整了:数据逻辑分离打地基,数据结构组织数据,行为组件执行瞬时效果,buff 系统管理持续状态。四块拼起来,一套能扛住策划各种花活的技能框架就成型了。

再往下,就是把它们联动起来的事件系统——“击杀后回血”“受击时反弹”"每third次攻击暴击"这类触发式设计,全靠事件机制串联。那是下一个话题了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/23 14:21:24

把PS3游戏跑流畅:RPCS3性能调优与配置完整指南

把PS3游戏跑流畅&#xff1a;RPCS3性能调优与配置完整指南 【免费下载链接】rpcs3 PlayStation 3 emulator and debugger 项目地址: https://gitcode.com/GitHub_Trending/rp/rpcs3 RPCS3 是目前完成度最高的 PS3 模拟器&#xff0c;可它并不是"装完就能玩"。…

作者头像 李华
网站建设 2026/8/23 14:19:33

1.4MB 跑通 107 种语言文本转语音,eSpeak NG 凭什么这么轻?

1.4MB 跑通 107 种语言文本转语音&#xff0c;eSpeak NG 凭什么这么轻&#xff1f; 【免费下载链接】espeak eSpeak NG is an open source speech synthesizer that supports 101 languages and accents. 项目地址: https://gitcode.com/gh_mirrors/es/espeak 给产品加上…

作者头像 李华
网站建设 2026/8/23 14:18:59

Redisson 与 Spring Boot 版本冲突:3 步定位并解决的排查指南

Redisson 与 Spring Boot 版本冲突&#xff1a;3 步定位并解决的排查指南 【免费下载链接】redisson Redisson: Valkey & Redis Java Client and Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Se…

作者头像 李华
网站建设 2026/8/23 14:17:19

PoB2 物品系统完整流程:从装备模拟到词缀优化的实战指南

PoB2 物品系统完整流程&#xff1a;从装备模拟到词缀优化的实战指南 【免费下载链接】PathOfBuilding-PoE2 项目地址: https://gitcode.com/GitHub_Trending/pa/PathOfBuilding-PoE2 Path of Building PoE2&#xff08;下称 PoB2&#xff09;是面向流放之路2 的桌面构建…

作者头像 李华
网站建设 2026/8/23 14:16:50

一条命令搞定中国省市区街道 sql/json 数据:从克隆到入库指南

一条命令搞定中国省市区街道 sql/json 数据&#xff1a;从克隆到入库指南 【免费下载链接】china_regions 最新最全省市区街道sql/json 文件 项目地址: https://gitcode.com/gh_mirrors/chi/china_regions china_regions 是一套覆盖中国省、市、区、街道四级行政区划的数…

作者头像 李华
网站建设 2026/8/23 14:15:25

StableLM-3B-4E1T能力有多强?7项Open LLM Leaderboard基准测试完整分析

StableLM-3B-4E1T能力有多强&#xff1f;7项Open LLM Leaderboard基准测试完整分析 【免费下载链接】stablelm-3b-4e1t 项目地址: https://ai.gitcode.com/hf_mirrors/ai-gitcode/stablelm-3b-4e1t StableLM-3B-4E1T 是 Stability AI 推出的 30 亿参数开源大语言模型&a…

作者头像 李华