昨天给大家分享的是战斗系统中"游戏世界"的设计模式,今天给大家分享一个商业项目中改变游戏数值计算的最重要的一个手段: Buff。
1: Buff的需求分析
需求1: 游戏中改变某项数值的道具,装备等通常都是靠Buff来实现的。比如玩家获得一个加速鞋的道具,使得他的移动速度增加20%。玩家获得道具,给玩家开启一个"加速鞋道具对应的Buff"。随后玩家又获得一个战靴,该战靴也能让玩家的"移动速度增加10%",处理方式为给玩家开启一个"战靴对应的Buff"。这里的需求表明,我们在设计Buff的时候,不同的Buff可能会对同一个数值都有影响。这里我们称为"某个属性的多层Buff"。
需求2: 有些Buff有时效性,有些Buff是没有时效性的。比如复活以后,5秒的无敌状态,我们的实现逻辑为,计算伤害的时候,如果目标有"无敌Buff",那么我们就不计算伤害直接返回。同时我们要维护这个Buff的时效性,则要更新该Buff的失效。Buff有可能也有冷却时间,下次开启的时候,必须要冷却结束以后才能被再次开启。设计时,关于Buff的时效性,我们要考虑Buff开启有效时间,冷却时间。
需求3: 某些情况下Buff可能就是一个判断标记,有这个Buff,执行某些逻辑,我们只需要提供HasBuff的接口。如角色有立即复活的Buff,死亡后会检查是否有这个Buff,如果有走复活逻辑即可。
对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀
2: 针对以上述需求,Buff该如何设计
1: 一个实体,会带很多的Buff,所以每个实体要有专门的Buff组件来管理它上面所有的Buff,每个Buff为一个BuffNode, 伪代码与注释如下:
export enum BuffState{Invalid, // 非法的状态Ready, // 准备好的可以开启的Ready状态Started, // Buff已经开启Freezed, // Buff处于冷冻期间}export class BuffNode{public buffId: number; // 唯一标识该Buff的类型idpublic state: BuffState; // 该Buff当前的状态public freezeTime: number; // 该Buff的冷却时间public durationTime: number; // 该Buff的有效时间,-1为一直有效public passedTime: number; //维护过去的时间public constructor(buffId: number){this.buffId = buffId;this.state = BuffState.Ready;this.freezeTime = -1;this.durationTime = -1;this.passedTime = 0;}}
2: 提供一个BuffTimeLine对象,用来管理实体上的所有Buff;
export class BuffTimeLine {private buffNodeSet: any = null;public Init(): void {this.buffNodeSet = {};}public HasBuff(buffId: number): boolean{if (!this.buffNodeSet[buffId]) {return false;}var node: BuffNode = this.buffNodeSet[buffId];if (node.state != BuffState.Started){return false;}return true;}public AddBuffNode(buffId: number): BuffNode {if (this.buffNodeSet[buffId]) {return this.buffNodeSet[buffId];}var node: BuffNode = GM_BuffMgr.Instance.CreateBuffNode(buffId);if (node === null){console.error("Can not find BuffNode");return null;}this.buffNodeSet[buffId] = node;return node;}public RemoveBuff(buffId: number): void{if (this.buffNodeSet[buffId]){var node: BuffNode = this.buffNodeSet[buffId];node.state = BuffState.Invalid;this.buffNodeSet.Remove(buffId);}}public StartBuff(buffId: number): boolean {var node: BuffNode = this.AddBuffNode(buffId);// 如果是已经为Started状态,则重置技能时间if (node.state !== BuffState.Ready && node.state !== BuffState.Started) {return false;}node.state = BuffState.Started;node.passedTime = 0;// test// EventMgr.Instance.Emit(EventType.UI, UIGameEvent.UIBuffStarted);return true;}public CalcAllBuffsWithProp(propName: string, ret: FightCalcResult): void {for (var key in this.buffNodeSet) {var node: BuffNode = this.buffNodeSet[key];if (node.state != BuffState.Started) {continue;}GM_BuffMgr.Instance.CalcFightBuffWithProp(node.buffId, propName, ret);}}public OnUpdate(dt: number) {for (var key in this.buffNodeSet) {var node: BuffNode = this.buffNodeSet[key];if (node.state === BuffState.Started) {if(node.durationTime === -1) { // -1,表示Buff一直有效continue;}node.passedTime += dt;if (node.passedTime >= node.durationTime) {node.state = BuffState.Freezed;node.passedTime = 0;// test// EventMgr.Instance.Emit(EventType.UI, UIGameEvent.UIBuffFreezed, null);}}else if (node.state === BuffState.Freezed) {node.passedTime += dt;if (node.passedTime >= node.freezeTime) {node.state = BuffState.Ready;node.passedTime = 0;// test// EventMgr.Instance.Emit(EventType.UI, UIGameEvent.UIBuffReady);}}}}}
数据部分:
使用一个BuffId到BuffNode的一个映射表,保证一个BuffId只能对应一个BuffNode,同时可以非常方便的通过BuffId来找到BuffNode,也能非常方便的遍历实体所拥有的所有Buff。
接口部分:
AddBufNode: 给实体添加一个Buff,添加好后为Ready状态;
RemoveBufNode: 给实体添加一个移除Buff的接口,从实体移除Buff。
StartBuffNode: 开启某个Buff,开启后Buff的状态为Started;
OnUpdate: 每次更新迭代,维护所有BuffNode的时效性,包括有效时间与冷却时间。
HasBuff: 判断某个实体是否带了某个Buff;
CalcAllBuffsWithProp: 这个接口很关键,就是计算多个不同的Buff但影响同一个"数值属性"的数据的叠加。它遍历所有的Buff节点,找到对应的"数值属性"处理函数,计算叠加后的数值属性返回给用户。
3: 提供一个机制,不同类的Buff,对应不同的计算处理函数,这个机制称为BuffModel。我们可以根据Buff的需求不同,对Buff进行分类,同一个处理逻辑为同一个大类。分类时,我们按照BuffId来进行处理,

如上图BuffId 180004, 红色的1表示的是为1类Buff,执行逻辑为给主题叠加“数值属性"。蓝色80004表示特定的子BuffId。

如上图BuffId 200001,表示的是2类Buff,根据概率来处理的Buff。每一类Buff我们称为一个BuffModel, BuffModel实现了Buff的计算逻辑与策略,Buff框架根据BuffId的类型,找到匹配的BuffModel,再根据相关"属性"找到对应的处理函数,而BuffModel的编写留给具体的项目逻辑,我们只提供BuffModel的机制。BuffAModel的示例代码如下:
export class BuffAModel {@BuffProcesser("MvSpeed", 100000, -1)public static DefaultCalcPropMvSpeed(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("MvSpeed") < 0) { // 此Buff不能增强该属性return;}ret.propValue += ret.propValue * parseFloat(config.AttrValue);}@BuffProcesser("Defense", 100000, -1)public static DefaultCalcPropDefense(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("Defense") < 0) { // 此Buff不能增强该属性return;}ret.defense += ret.defense * parseFloat(config.AttrValue);}@BuffProcesser("AtkBonus", 100000, -1)public static DefaultCalcPropAtkBonus(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("AtkBonus") < 0) { // 此Buff不能增强该属性return;}ret.propValue += parseFloat(config.AttrValue);}@BuffProcesser("Range", 100000, -1)public static DefaultCalcPropRange(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("Range") < 0) { // 此Buff不能增强该属性return;}ret.propValue += (parseFloat(config.AttrValue)); // 攻击距离叠加}@BuffProcesser("CritChance", 100000, -1)public static DefaultCalcPropCritChance(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("CritChance") < 0) { // 此Buff不能增强该属性return;}ret.propValue += parseFloat(config.AttrValue);}@BuffProcesser("SummonBonus", 100000, -1)public static DefaultCalcPropSummonBonus(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("SummonBonus") < 0) { // 此Buff不能增强该属性return;}ret.propValue += parseInt(config.AttrValue);}@BuffProcesser("HPBonus", 100000, -1)public static DefaultCalcPropHPBonus(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("HPBonus") < 0) { // 此Buff不能增强该属性return;}ret.propValue += parseFloat(config.AttrValue);}@BuffProcesser("AtkSpeedBonus", 100000, -1)public static DefaultCalcPropAtkSpeedBonus(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("AtkSpeedBonus") < 0) { // 此Buff不能增强该属性return;}ret.propValue += parseFloat(config.AttrValue);}@BuffProcesser("CoolDownBonus", 100000, -1)public static DefaultCalcPropCoolDownBonus(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("CoolDownBonus") < 0) { // 此Buff不能增强该属性return;}ret.propValue += parseFloat(config.AttrValue);}@BuffProcesser("LifeSteal", 100000, -1)public static DefaultCalcPropLifeSteal(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("LifeSteal") < 0) { // 此Buff不能增强该属性return;}ret.propValue += parseFloat(config.AttrValue);}@BuffProcesser("ReviveTimeBonus", 100000, -1)public static DefaultCalcPropReviveTimeBonus(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("ReviveTimeBonus") < 0) { // 此Buff不能增强该属性return;}ret.propValue += parseFloat(config.AttrValue);}@BuffProcesser("Combo", 100000, -1)public static DefaultCalcPropReviveCombo(buffId: number, ret: FightCalcResult) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", buffId.toString());if (config === null) {return;}if(config.AttrName.indexOf("Combo") < 0) { // 此Buff不能增强该属性return;}ret.propValue += parseFloat(config.AttrValue);}@BuffProcesser("BuffTime", 100000, -1)public static DefaultBuffTime(node: BuffNode) {var config = ExcelMgr.Instance.QueryByID("GameBuffA", node.buffId.toString());if (config === null) {return;}// node.freezeTime = parseFloat(config.BuffFrezzeTime);node.freezeTime = 0; // 本Buff,没有任何Buff有冷却时间;// node.durationTime = parseFloat(config.BuffTime);node.durationTime = parseFloat(config.BuffDuration);if(node.durationTime <= 0) {node.durationTime = -1; // 外面判断为-1,所以保险起见,还是为-1;}}}
注意看这里: @BuffProcesser("AtkSpeedBonus", 100000, -1),通过装饰器(TypeScript) or Attribute(Java/C#等)来注册对应的"数值计算"函数。-1,表示的是大类默认的,如果有特殊的小类,我们就可以注册这个属性的特殊的小类。比如对Buff 180004 的 "AtkSpeedBonus"数值属性做特殊处理,我只要注册:
@BuffProcesser("AtkSpeedBonus", 100000, 80004),当计算80004的Buff的时候,就不会使用默认的,而使用该Buff特定的计算函数。这样在BuffModel层面,我们提供了某类Buff的默认处理规则与特殊处理规则两种机制,方便开发者使用。
END
最后,我们把Buff作为组件数据,带入到实体中,让实体能通过携带不同的Buff来改变对应的数值与处理逻辑:
export class SkillAndBuffComponent {public normalSkillOptSet: Array<SkillOptNode> = new Array<SkillOptNode>();public specialSkillOptSet: Array<SkillOptNode> = new Array<SkillOptNode>();public skillTimeLine: SkillTimeLine = new SkillTimeLine();public buffTimeLine: BuffTimeLine = new BuffTimeLine();public attackSpeedScale: number = 1; // 技能的攻击速度比例,外部算好,内部使用// 连接使用加速做,所以保存一下上一次连接的参数public comboCount: number = 0; // 连击数目;public comboSkillOptNode: SkillOptNode = null; // 连击时的选择的技能;public comboSpeedScale: number = 1;}export class BaseEntity {public worldId: number = -1; // 角色在世界里面的Id;// 各个与战斗相关的组件public uInfo: CharactorInfoComponent; // 角色的相关信息public uTransform: TransformComponent; // 位置旋转public uStatus: StatusComponent; // 玩家的状态数据相关;public uNav: NavComponent; // 玩家导航中需要保存的数据;public uSkillAndBuff: SkillAndBuffComponent; // 玩家释放技能与Buff相关数据;public uRvo: RVOComponent;public uAStarNav: AStarComponent; // 玩家A*寻路的时候需要保存的导航的数据;public uProps: PropsComponent; // 玩家单机游戏需要的战斗+移动数据,帧同步+单机需要;}
var attack = attacker.uInfo.charactorData.Atk;// 叠加Buff,计算该实体的所有Buff对Atk数值属性的加层var ret = new FightCalcResult();ret.propValue = attack;attacker.uSkillAndBuff.buffTimeLine.CalcAllBuffsWithProp("Atk", ret);attack = ret.propValue;// end
最后我们这套Buff机制经过了一批商业项目的验证,能非常灵活的解决商业项目中的需求,被大家认为是非常好的一个经典设计。配套课程+代码开源给到大家:
Unity 技能与多层Buff框架:
https://www.bycwedu.com/customize/category/103744369?back=1&promoter_id=0
Cocos 技能与多层Buff框架:
https://www.bycwedu.com/customize/category/1504861874?back=1&promoter_id=0
今晚(周六, 10月18日)继续直播《肉鸽类商业项目如何落地》欢迎大家来到直播间。学习更多的系统架构与设计的思想与经验。
===================================
,时长03:12
直播分析我们《肉鸽原型项目+方案+教程配套》是如何落地的。全程干货。
直播内容专题如下(每个小专题设置Q&A环节):
1: 立项: 为什么我们会选《肉鸽类》的游戏? 已结束
2: 初步策划案我们是如何来落地的? 已结束
3: 面对策划案的需求,如何做的技术选型与技术架构? 已结束
4: 如何解决原型项目的美术资源与为什么先从原型项目开始?已结束
5: 如何把别人项目的资源,迁移重构到自己的项目框架中? Q&A
6: 策划使用的地图编辑器我们是如何解决的?Q&A
7: 为什么核心战斗逻辑我们采用ECS架构来实现,有什么好处?Q&A
8: 面对5种英雄,8种军团兵种,25种敌人怪物,37种道具,49种技能,32种局内升级,15级的局外天赋升级,我们是如何做好技能与Buff的架构?Q&A
9:面对秒杀,暴击,复活,无敌,局外局内技能等对数值的影响,我们如何做伤害数值计算。Q&A
10:英雄操控军团的策略,军团与敌人的群体战斗,我们是如何设计与实现的?有限状态机,行为决策树?还是其它方案。Q&A
11: 为什么我们做原型的同时要配套教程?Q&A
12: 美术外包,我们是如何梳理自己的需求的?UI, 角色,场景,特效,ICON等。Q&A
850

被折叠的 条评论
为什么被折叠?



