一种基于DSL的数据驱动式大规模游戏战斗技能的实现方法

背景 我曾经为两款游戏开发战斗技能系统,使用的是传统OOP的方式,大概设计是有个基类BaseAbility,里面有一些基础的通用逻辑,然后不同的技能实现在不同的子类中。2020年我们立了一个新项目,策划告诉我这个项目以英雄战斗技能为主,可能多达500个技能。 过去两个项目中基于OOP的战斗系统已经让我陷入纠结和痛苦之中,因为一直在为类继承体系而纠结,虽然继承可以提高复用性,但也大大增加耦合性和复杂性,到了后期子类爆炸,逻辑在不同类中来回穿梭,属性计算也在庞大的类继承体系中迷失。 为了让战斗技能系统变的简单清晰,这个项目我准备用一种全新的基于DSL的数据驱动式的战斗技能系统,战斗框架采用ECS架构。数据驱动具体来讲是基于DSL来表述一个技能的全部逻辑,DSL在维基百科中的定义: 领域特定语言(英语:Domain-specific language,缩写:DSL),也称为特定域语言,是专门针对特定应用领域的计算机语言,和可以用在多种领域的通用语言(GPL)恰好相反。像HTML专门用在网页设计上,就属于领域特定语言。 战斗系统代码负责解析执行DSL中的逻辑,ECS是为了更好的实现逻辑和表现层分离,因为我们战斗系统是同一套代码同时运行在客户端和服务端,至于ECS带来的性能上的收益则是额外收获。 技能DSL设计 在参考了Dota2的技能描述语言后,我决定设计一套更简洁,功能更丰富的DSL来表达技能的配置系统。这套DSL必须足够简单,策划容易阅读和编写,不需要太多专业编程知识。经过研究,我发现yaml格式就可以很好的作为一种DSL的载体来定义技能。最终用这套系统实现了700多个技能,开发技能效率很高,上线后效果也不错。 后期基本是策划在做技能,程序偶尔辅助下。 先看个近战普攻技能示例: # MeleeBasicAttack.yaml # 近战普通攻击技能模板 # 技能每个等级的伤害数值 DamageTable: Ability: Type: PHYSICAL Levels: - 140% * $ATK + 600 - 280% * $ATK + 6000 - 320% * $ATK + 21000 - 400% * $ATK + 30000 # 技能主动作序列 MainActions: - PlayBasicAttackAnim: Sequence: - Attack1: 3 - Attack2: 1 - WaitCastPoint: Duration: $CAST_POINT - PlaySound: Name: $CAST_SOUND - PlayVfx: Target: $TARGET Prefab: Battle/Battlefield/VFX_General_AttackHit - AbilityApplyDamage: Target: $TARGET - PlaySound: Target: $TARGET RandomRange: - SFX_HIT_TARGET_1 - SFX_HIT_TARGET_2 - WaitRecoverTime: Duration: $RECOVER_TIME 第一段DamageTable用来定义技能的伤害,因为我们游戏技能可以升级所以写了4个等级的伤害,每一行都是一个伤害计算公式,公式里有常量和变量引用,上面技能DSL里$ATK用来引用英雄当前的物理攻击力,技能执行Runtime会动态替换里面的变量,上面技能1级伤害是140%的英雄物理攻击 + 600点伤害,后几级以此类推。DamageTable是一个Map结构,技能伤害的Key是Ability,技能可以有多种伤害,在DamageTable定义多个伤害配置。 ...

July 25, 2023 · 2 min · Jee