ARTICLE DETAIL

资讯详情

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

技能数据结构设计

技能数据结构设计 上一篇讲了数据和逻辑要分离把技能数值抽成配置。但真到动手设计配置结构的时候你会发现坑比想象的多。字段怎么定、效果怎么组织、目标怎么选……随便一个没想清楚后面就得推倒重来。这篇就把技能的数据结构一步步搭起来。从最天真的版本开始新手第一版通常长这样{id:fireball,name:火球术,damage:100,cooldown:3}能用但撑不了几天。策划马上会提各种需求把这个结构撑爆“火球术要有施法距离限制” → 加个range“要消耗 30 点法力” → 加个manaCost“命中后还要点燃每秒掉 5 血持续 3 秒” → 这咋加“有个技能是治疗队友不是打敌人” →damage变负数太别扭了你一路加字段结构越来越乱最后变成一个几十个字段的大杂烩一半技能用不上一半字段。这说明一开始就没分清楚哪些是所有技能都有的哪些是特定技能才有的。第一刀分出基础信息和效果任何技能都能拆成两部分。一部分是元信息——这个技能是什么比如名字、图标、冷却、消耗、施法距离。这些几乎每个技能都有是固定的框架。另一部分是效果——这个技能干什么比如造成伤害、治疗、加buff、击退。这部分千变万化是真正的核心。按这个思路重新设计{id:fireball,name:火球术,icon:fireball.png,cooldown:3,manaCost:30,range:8,effects:[{type:damage,value:100,element:fire}]}关键变化在于伤害不再是一个顶层字段而是塞进了effects数组里成为一个效果。这一步看着简单威力巨大。因为效果是数组一个技能就能挂多个效果{id:poison_blade,name:淬毒之刃,cooldown:5,effects:[{type:damage,value:50,element:physical},{type:dot,value:10,duration:3,element:poison},{type:slow,value:0.3,duration:2}]}一刀砍下去先扣 50 血再中毒每秒掉 10 持续 3 秒还减速 30%。全靠往数组里加效果一行代码不用写前提是这几种效果引擎都支持。第二刀效果要能描述打谁上面的结构还漏了关键一环——这些效果作用在谁身上火球术的伤害打敌人但同一个技能可能伤害敌人的同时治疗自己。所以每个效果都得说清楚自己的目标。给效果加上target字段{id:life_drain,name:生命汲取,cooldown:8,effects:[{type:damage,value:80,target:enemy},{type:heal,value:40,target:self}]}打敌人 80同时回自己 40。目标不同写清楚就行。目标类型常见的有这么几种提前定义好self 自己 enemy 单个敌人 allEnemies 范围内所有敌人 ally 单个队友 allAllies 所有队友第三刀效果要能描述什么时候触发再复杂一点的技能效果不是立刻全部生效的有先后、有条件。比如蓄力斩施法瞬间不掉血蓄力 2 秒后才爆发伤害。再比如处决只有当目标血量低于 30% 时才追加一段真实伤害。这就需要给效果加上触发时机和触发条件{id:execute,name:斩杀,cooldown:10,effects:[{type:damage,value:100,target:enemy,trigger:onCast},{type:damage,value:300,target:enemy,trigger:onHit,condition:{targetHpPercent:0.3}}]}第一段伤害固定 100施法就生效。第二段 300 的斩杀伤害只有命中时目标血量低于 30% 才触发。trigger定义在哪个时间点执行常见的onCast 施法瞬间 onHit 命中目标时 onTick 持续期间每秒 onEnd 效果结束时condition定义满足什么才执行是可选的不填就是无条件执行。拼起来看一个完整的技能结构长啥样把上面几刀合起来一个技能的完整数据结构大概是这样{id:flame_strike,name:烈焰打击,icon:flame_strike.png,description:对目标造成火焰伤害并点燃若目标已中毒则额外爆炸,cooldown:6,manaCost:40,range:10,castTime:0.5,effects:[{type:damage,value:120,element:fire,target:enemy,trigger:onCast},{type:dot,value:15,duration:4,element:fire,target:enemy,trigger:onHit},{type:damage,value:200,element:fire,target:enemy,trigger:onHit,condition:{targetHasBuff:poison}}]}结构清晰得一目了然上半部分是技能的框架信息下半部分是三个效果各自写明了作用对象、触发时机和触发条件。对应到代码里用类来接classSkillConfig{Stringid;Stringname;Stringicon;Stringdescription;floatcooldown;intmanaCost;floatrange;floatcastTime;ListEffectConfigeffects;// 核心效果列表}classEffectConfig{Stringtype;// damage / heal / dot / slow ...floatvalue;Stringelement;// 元素属性可选intduration;// 持续时间可选Stringtarget;// self / enemy / allEnemies ...Stringtrigger;// onCast / onHit / onTick ...MapString,Objectcondition;// 触发条件可选}一个容易翻车的点别把所有字段塞进一个类你可能注意到EffectConfig里有duration持续时间这种字段但纯伤害效果根本用不上它。字段一多就会出现伤害效果里躺着一堆治疗才用的字段这种尴尬。小项目无所谓字段多点就多点。但如果效果类型很多、差异很大可以考虑按类型拆分用多态abstractclassEffectConfig{Stringtarget;Stringtrigger;MapString,Objectcondition;}classDamageEffectextendsEffectConfig{floatvalue;Stringelement;}classDotEffectextendsEffectConfig{// 持续伤害floatvalue;intduration;Stringelement;}classHealEffectextendsEffectConfig{floatvalue;}这样每种效果只带自己需要的字段干净。代价是解析配置时要根据type判断该反序列化成哪个类稍微麻烦点。怎么选看你的效果种类多不多、字段差异大不大。再进一阶技能之间的复用做到后面你会发现很多技能长得像。比如一堆火系技能都是伤害 点燃只是数值不同。每个都完整写一遍还是有点冗余。这时可以引入模板或继承的思路让技能能复用一个基础配置{id:big_fireball,name:大火球,extends:fireball,// 继承火球术的所有配置overrides:{cooldown:5,effects[0].value:250// 只改伤害值}}不过这个要不要上得掂量。它确实减少了重复但也让配置变活了——你光看一个技能配置不去翻它继承的爹就不知道它最终长啥样排查问题变累。除非重复真的多到受不了否则老老实实每个技能写全反而更好维护。设计时记住这几条先分层再填字段。永远先把元信息和效果分开效果一定要设计成数组。这是整个结构的地基地基对了后面才好扩展。效果是核心把它做通用。一个效果至少要能回答三个问题干什么type value、对谁干target、什么时候干trigger/condition。把这几个维度定清楚绝大多数技能都能用配置拼出来。别一步到位设计太复杂。如果你的技能就是造成伤害这么简单那trigger、condition这些先别加等真有需求了再补。过度设计的结构维护起来一样痛苦。留好扩展口子但别提前实现。结构上给未来的效果类型留位置比如type用字符串而不是写死的枚举但没到那一步就别硬塞功能进去。一句话收尾技能数据结构设计核心就一句把技能拆成框架信息和效果列表让每个效果都能说清楚自己干什么、对谁干、什么时候干。框架保证所有技能长得规整效果数组保证任意技能都能自由组合拼装。结构设计好了前面那套数据逻辑解耦才能真正落地——策划在配置表里拼效果程序员在引擎里实现效果各司其职谁也不耽误谁。
返回列表