ARTICLE DETAIL

资讯详情

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

ECS随笔4:一个技能释放链路

ECS随笔4:一个技能释放链路 文章目录具体例子技能配置抽象在 ECS 里这两个技能可以直接表达成数据推荐组件单位组件能力所需的数据组件技能请求组件施法数据组件子弹组件范围技能组件命中和伤害事件系统执行顺序具体例子ECS流程1、英雄实体拥有技能初始化2、SkillSelectionSystem 挑技能3、CastRequestSystem 建立施法状态4、CastingSystem 到点后允许生成技能实体5、SkillSpawnSystem 创建子弹或范围实体6、ProjectileMoveSystem 移动弹珠7、ProjectileEndSystem 触发 30020028、AreaEffectSystem 命中范围目标9、HitResolveSystem 生成伤害和 buff 事件10、DamageSystem 扣血ECS 链路总览为什么 ECS 版更利于维护技能行为从“继承树”变成“组件组合”每个系统的阅读入口固定调试更直接配置和逻辑更容易对应具体例子现在 1 的链路大致是SkillMgr.logicTick-_processSkillCompRequests-checkSkillComp-SkillComp.getCastSkillId-SkillMgr.useSkill-_processSkillTargets-_createSkillActionInternal-EntityMgr.createSkill-TraceBulletAction2-TraceMoveComp5-SkillEntity.logicTick/checkHit/doHit-BattleMgr.onHit-end_skill 触发1这套写法能跑也有一定组件化痕迹但对人类阅读不友好主要问题是一个技能的生命周期被拆散在 SkillComp、SkillMgr、EntityMgr、SkillEntity、具体 Action、MoveComp、Collider、BattleMgr 之间。“技能本体是什么”不够直观。1 实际是一个抛物线弹珠落地后触发 2 范围伤害但读代码时要跨很多类才能拼出来。SkillEntity 同时承担技能状态、移动、碰撞、特效事件、命中、派生技能触发等职责职责偏重。行为藏在继承和方法调用里调试时需要知道“现在到底进了哪个子类”。如果是长期维护、技能越来越多、配置越来越复杂改成更纯的 ECS 会更利于维护。更准确地说不是把所有代码机械改名成 ECS而是把技能释放拆成一条可读的系统流水线。这样每个系统只做一件事。一个技能当前在哪一步直接看实体身上的组件即可。SkillSelectionSystem-CastStartSystem-ProjectileSpawnSystem/AreaSpawnSystem-ProjectileMoveSystem-CollisionSystem-HitResolveSystem-DamageSystem-SkillTriggerSystem-CleanupSystem技能配置抽象以3000为例英雄技能consthero3000{id:3000,name:英雄,skills:[1,2],};普攻 1 的关键特征type 10普通技能。atk_type 7抛物线投射。select_mode 2最近目标。cast_range [1, x]圆形 x 范围。target_type [2, 4, 6, 8]敌方塔、敌方英雄、敌方怪物、敌方召唤物。damge_if 1自身不直接结算伤害。end_skill [{2, 10000}]结束时 100% 触发2。派生技能 2 的关键特征atk_type 11范围攻击。select_mode 0不重新索敌。atk_range [1, y]圆形 y 范围。damage 10000100% 攻击力。hit_buff [{buff, 10000}]命中后 100% 添加 buff。在 ECS 里这两个技能可以直接表达成数据enumSkillKind{ParabolaProjectileParabolaProjectile,AreaHitAreaHit,}interfaceSkillConfig{id:number;heroId:number;priorityType:number;kind:SkillKind;cooldownMs:number;castTimeMs:number;targetRule:TargetRule;projectile?:ProjectileConfig;area?:AreaConfig;damage?:DamageConfig;triggers?:SkillTriggerConfig[];hitBuffs?:BuffTriggerConfig[];}constskill1:SkillConfig{id:1,heroId:3000,priorityType:10,kind:SkillKind.ParabolaProjectile,cooldownMs:4000,castTimeMs:1100,targetRule:{selectMode:Closest,rangeShape:Circle,range:350,targetTypes:[EnemyTower,EnemyHero,EnemyMinion,EnemySummon],},projectile:{moveKind:Parabola,radius:25,maxHeight:200,minHeight:10,maxDistance:600,randomHeight:0,minDurationMs:100,speedByDistance:{maxSpeed:600,minSpeed:300,maxSpeedDistance:600,randomSpeed:0,},},triggers:[{timing:OnEnd,skillId:2,probability:10000,inheritSnapshot:true,castAt:ProjectileEndPosition,},],};constskill2:SkillConfig{id:2,heroId:3000,priorityType:10,kind:SkillKind.AreaHit,cooldownMs:0,castTimeMs:0,targetRule:{selectMode:None,targetTypes:[EnemyTower,EnemyHero,EnemyMinion,EnemySummon],},area:{shape:Circle,radius:50,durationMs:50,tickIntervalMs:50,maxTargetsPerTick:1,},damage:{damageType:Magic,coefficient:1.0,},hitBuffs:[{buffId:buff,probability:10000,},],};这样读配置时就能直接看出1 是抛物线弹珠。2 结束后在弹珠落点释放 2。2 是一个 50 半径的范围命中并造成伤害和 buff。推荐组件单位组件能力所需的数据组件typeEntityIdnumber;interfaceIdentityComponent{type:Hero|Minion|Tower|Summon|Projectile|Area;cfgId:number;camp:number;owner?:EntityId;}interfaceTransformComponent{x:number;y:number;faceAngle:number;}interfaceAttributeComponent{attack:number;crit:number;attackSpeedInc:number;skillCdInc:number;}interfaceSkillBookComponent{skillIds:number[];}interfaceCooldownComponent{skillReadyAt:Recordnumber,number;}interfaceCastingComponent{skillId:number;targetIds:EntityId[];startTime:number;fireTime:number;actionName:string;}技能请求组件施法数据组件CastRequestComponent 是“我要释放一个技能”的数据。系统只要看到它就知道下一步要处理施法。interfaceCastRequestComponent{caster:EntityId;skillId:number;targetIds:EntityId[];castPosition?:{x:number;y:number};startAngle?:number;source?:SkillSource;}interfaceSkillSource{type:Cooldown|OnEnd|OnHit|Manual|Buff;sourceSkillId?:number;sourceEntity?:EntityId;}interfaceAttackerSnapshot{caster:EntityId;camp:number;cfgId:number;attack:number;crit:number;damageIncreases:Recordstring,number;}子弹组件interfaceProjectileComponent{skillId:number;caster:EntityId;mainTarget?:EntityId;targetTypes:string[];radius:number;snapshot:AttackerSnapshot;endTriggers:SkillTriggerConfig[];}interfaceParabolaMoveComponent{startX:number;startY:number;endX:number;endY:number;elapsedMs:number;durationMs:number;height:number;}interfaceLifetimeComponent{endAt:number;}范围技能组件interfaceAreaEffectComponent{skillId:number;caster:EntityId;shape:Circle|Rectangle|Fan;radius:number;targetTypes:string[];maxTargetsPerTick:number;tickIntervalMs:number;nextTickAt:number;snapshot:AttackerSnapshot;}命中和伤害事件事件也可以是 ECS 里的短生命周期实体。interfaceHitEventComponent{attacker:EntityId;target:EntityId;skillId:number;snapshot:AttackerSnapshot;}interfaceDamageEventComponent{attacker:EntityId;target:EntityId;skillId:number;baseAttack:number;coefficient:number;damageType:Physical|Magic;}interfaceBuffApplyEventComponent{applier:EntityId;target:EntityId;buffId:number;skillId:number;}系统执行顺序技能链路建议拆成这些系统每个系统职责固定SkillSelectionSystem从单位技能列表里挑一个当前可释放技能。CastRequestSystem把可释放技能变成 CastRequestComponent。CastingSystem处理前摇和动作时间。SkillSpawnSystem根据技能配置生成子弹实体或范围实体。ProjectileMoveSystem只负责移动子弹。ProjectileEndSystem子弹结束时处理 OnEnd 触发。AreaEffectSystem范围技能定时找目标产生命中事件。HitResolveSystem命中率、闪避、命中后触发。DamageSystem根据攻击快照和技能系数扣血。BuffApplySystem处理命中 buff。RenderEventSystem把逻辑事件转给表现层。CleanupSystem清理一次性请求、事件、过期实体。具体例子ECS流程1、英雄实体拥有技能初始化constxxworld.createEntity();world.add(xx,IdentityComponent,{type:Hero,cfgId:3002,camp:Camp.Self,});world.add(xx,TransformComponent,{x:420,y:760,faceAngle:-90,});//...world.add(xx,SkillBookComponent,{skillIds:[3002001,3002005],});2、SkillSelectionSystem 挑技能这个系统只做一件事遍历有 SkillBookComponent 和 CooldownComponent 的单位找当前能释放的技能。系统读取纯数据组件后产生一个 CastRequestComponentclassSkillSelectionSystem{update(world:World,now:number):void{//......constrequestworld.createEntity();world.add(request,CastRequestComponent,{caster:entity,skillId,targetIds:targets.map(tt.entity),source:{type:Cooldown},});}}3、CastRequestSystem 建立施法状态这里把“施法前摇”和“创建技能实体请求”分开。表现层如果要播 attack_01只需要监听 CastingComponent 或逻辑事件。classCastRequestSystem{update(world:World,now:number):void{//......constspawnRequestworld.createEntity();world.add(spawnRequest,SkillSpawnRequestComponent,{caster:request.caster,skillId:request.skill,targetIds:request.targetIds,castPosition,startAngle:request.startAngle,snapshot,source:request.source,});world.destroy(requestEntity);}}4、CastingSystem 到点后允许生成技能实体如果某些衍生技能没有前摇它的 castTimeMs 0可以同帧 ready。interfaceSkillSpawnRequestComponent{caster:EntityId;skillId:number;targetIds:EntityId[];castPosition:{x:number;y:number};startAngle?:number;snapshot:AttackerSnapshot;source?:SkillSource;ready?:boolean;}classCastingSystem{update(world:World,now:number):void{}}5、SkillSpawnSystem 创建子弹或范围实体classSkillSpawnSystem{update(world:World,now:number):void{}privatespawnParabolaProjectile(world:World,now:number,spawn:SkillSpawnRequestComponent,config:SkillConfig):void{}privatespawnArea(world:World,now:number,spawn:SkillSpawnRequestComponent,config:SkillConfig):void{}}6、ProjectileMoveSystem 移动弹珠classProjectileMoveSystem{update(world:World,deltaMs:number):void{//更新数据驱动改变}}7、ProjectileEndSystem 触发 3002002ECS 里触发链很直观ProjectileEndEvent 生成新的 CastRequest。interfaceProjectileEndEventComponent{projectile:EntityId;x:number;y:number;}classProjectileEndSystem{update(world:World):void{//增加CastRequestComponent}}8、AreaEffectSystem 命中范围目标3002002 会由 SkillSpawnSystem 创建一个 AreaEffect 实体。接下来它自己按 tick 找范围内目标。classAreaEffectSystem{update(world:World,now:number):void{}}9、HitResolveSystem 生成伤害和 buff 事件classHitResolveSystem{update(world:World):void{//addDamageEventComponent、BuffApplyEventComponent}}10、DamageSystem 扣血classDamageSystem{update(world:World):void{for(consteventEntityofworld.query(DamageEventComponent)){//消费DamageEventComponent生成RenderEventComponent}}}ECS 链路总览flowchart TD A[SkillSelectionSystembr/英雄选中 3002001] -- B[CastRequestComponentbr/caster英雄 skillId3002001] B -- C[CastingSystembr/等待 castTime 1100ms] C -- D[SkillSpawnSystembr/创建 Projectile Entity] D -- E[ProjectileMoveSystembr/抛物线移动] E -- F[ProjectileEndEventbr/弹珠到达目标点] F -- G[ProjectileEndSystembr/触发 OnEnd 技能 3002002] G -- H[CastRequestComponentbr/skillId3002002 castPosition落点] H -- I[SkillSpawnSystembr/创建 Area Entity] I -- J[AreaEffectSystembr/找 50 半径内目标] J -- K[HitEventComponent] K -- L[HitResolveSystembr/命中率 / buff / 伤害事件] L -- M[DamageSystembr/扣血] L -- N[BuffApplySystembr/添加 300200101]为什么 ECS 版更利于维护技能行为从“继承树”变成“组件组合”新增技能时优先组合组件而不是新增子类。system流水线比继承类嵌套组合使用调用更适合人维护阅读。XxxextendsSkillEntity//继承//组合抛物线子弹Projectile子弹ParabolaMove移动直线子弹ProjectileLinearMove每个系统的阅读入口固定想看“怎么选技能”只看 SkillSelectionSystem。想看“怎么移动抛物线”只看 ProjectileMoveSystem。想看“怎么触发 end_skill”只看 ProjectileEndSystem 或 SkillTriggerSystem。想看“怎么结算伤害”只看 DamageSystem。不需要从 SkillMgr 一路追到 EntityMgr再追到某个具体子类。调试更直接ECS 调试时可以直接看实体组件entity 501 是 Projectile有 ParabolaMoveskillIdxxx1。entity 501 产生了 ProjectileEndEvent。ProjectileEndSystem 创建了 CastRequest skillIdxxx2。entity 610 是 AreaEffectradius50。AreaEffectSystem 产生了 HitEvent。DamageSystem 扣血。配置和逻辑更容易对应当前 atk_type7 要知道它会进Xxx2 和 Xxx5。ECS 可以把配置预编译成更语义化的 SkillKind.ParabolaProjectile策划配置和程序逻辑更容易对齐。
返回列表