Unity动作游戏技能系统全拆解:状态机、伤害判定与特效优化

发布时间:2026/9/28 15:25:49
Unity动作游戏技能系统全拆解:状态机、伤害判定与特效优化 最近我把之前用Unity做的动作Demo《赤莲破神枪》整理成了一整套可迁移、可复用的工程文件分享给群里和社区的朋友之后反馈比我想象中热烈。很多人问得最多的不是“你的模型哪来的”而是“这套技能的判定、特效和手感是怎么一步步搭出来的”。所以这篇直接把工程拆开来讲从机制到状态机、从伤害结算到指示器、从Shader到对象池把我在做这套技能系统时的完整思路和踩过的坑都记录下来。这套工程本质上是一个ARPG风格的技能演示角色起跳、滞空、化作赤色莲花然后锁定目标区域下刺“枪势”贯穿地面的同时沿途绽开数朵莲花造成多段伤害。适合正在做动作游戏、技能系统或割草类Demo的Unity开发者参考无论是想学状态机管理、对象池、特效优化还是单纯想看一套完整技能怎么从零落地都能从这里拿走不少东西。1. 技能整体设计与拆解思路动手之前先想清楚一件事赤莲破神枪这个技能看起来花里胡哨但它的核心交互其实可以拆成“位移、滞空、穿刺、多段爆炸、灼烧”五个元素。每个元素对应一套相对独立的表现和判定最后通过一个状态机串起来这样写代码时脑子不会乱后续换一个技能也只要换组合方式不用推翻重写。1.1 先还原技能表现再定义机制我的习惯是先对着参考视频把表现一帧一帧看一遍然后写一张“表现-机制”对照表。比如起跳阶段本质是位移和滞空空中化成莲花本质是模型切换和粒子覆盖下刺瞬间是穿刺判定落地后的多段莲花就是延迟触发的范围爆炸灼烧则是给目标挂一个持续掉血buff。这步特别重要很多人做技能一上来就写逻辑结果表现和判定对不上后期改起来极其痛苦。赤莲破神枪的实际表现和机制对照是下面这样表现阶段玩家看到的效果实际机制起跳角色后撤一步跃起枪尖拖出红焰位移短暂滞空无敌帧开启化莲角色被赤色莲花虚影包裹形态放大模型隐藏粒子模型接管受击范围缩小下刺角色持枪从天而降直线穿刺地面高速位移气浪判定中心高倍率伤害莲爆刺入点周围接连绽放数朵莲花延迟多段OverlapSphere爆炸伤害燃烬敌人身上残留红色灼烧粒子挂灼烧Buff每秒掉血这个表做完后面所有的代码和特效配置就都只是“填空”。1.2 工程结构怎么划分才不踩坑既然叫“工程文件分享”目录结构就一定要经得起别人打开。我的Assets目录大致是这么组织的Scripts/Core角色控制器、输入管理、公共数据Scripts/Skill技能状态机、技能数据、攻击判定Scripts/Effect对象池、伤害数字、特效管理Art/Character角色模型、材质、动画Art/VFX粒子预制体、Shader、贴图、后处理配置Data/ScriptableObject技能参数配置、数值表Animation动作片段和动画控制器这么分的好处有三个第一逻辑和表现完全分离策划调数值时不会碰坏代码第二敌人AI、伤害计算、特效播放各自独立单人维护也能快速定位问题第三别人拿到工程不用看全部代码先看Data和VFX就能猜个七八成功能。很多初学者喜欢把所有东西都丢在Assets根目录下文件一多就乱了。做工程分享或者正式项目宁可多花半小时分类也不要让后来者对着几百个文件发呆。2. 技能核心状态控制与多段伤害结算技能要做到“手感好”最关键的是每一步的时机和反馈。赤莲破神枪这里我用了一套自定义的技能状态机来管理整条流程而不是直接依赖Animator状态机。原因很简单Animator管的是动画表现技能逻辑涉及定时器、伤害判定、对象池调用混在一起会让状态机变得面目全非。2.1 技能状态机怎么设计技能状态我用一个枚举表达切换条件只由逻辑层触发public enum SkillPhase { None, CastStart, // 起跳前摇 Airborne, // 滞空/化莲 Thrust, // 下刺 Blooming, // 莲花连环爆 Done }核心是一个SkillStateMachine类维护当前阶段和切换函数。注意这里没有用Update里的switch暴力判断而是每个阶段注册进入和退出的回调配合协程或者Timer驱动阶段切换。public sealed class SkillStateMachine { private SkillPhase _current; private DictionarySkillPhase, Action _onEnter; private DictionarySkillPhase, Action _onExit; public void Switch(SkillPhase next) { _onExit[_current]?.Invoke(); _current next; _onEnter[_current]?.Invoke(); } }实际技能流程大概是这样输入触发后进入CastStart播放起跳动画给角色加垂直速度开启无敌帧。达到一定高度后进入Airborne隐藏主模型生成“赤莲姿态”的粒子模型播放镜头拉远。锁定目标点后进入Thrust角色高速向下位移同时计算直线穿刺判定。穿刺结束进入Blooming按时间序列生成2到4朵莲花爆炸区域每朵间隔约0.2到0.4秒。所有爆炸结算完进入Done回收特效和对象池恢复移动。这个设计的关键点是每个阶段都有自己的Enter和Exit回调阶段切换时做清理这样不会出现“上个阶段的粒子在下个阶段还在飘”的问题。2.2 多段伤害怎么算才不穿帮技能伤害判定是最容易翻车的部分赤莲破神枪的判定机制分三种穿刺判定一条射线或胶囊体下砸对路径上敌人造成一次高额物理伤害。莲花爆炸以目标点为中心多个不同半径的球形范围依次触发造成法术伤害。灼烧Buff被莲花爆炸击中的敌人挂上灼烧状态每秒造成火焰伤害持续3秒。为了避免多段伤害让同一个敌人被重复圈到我用了一个简单的记忆集合。每次技能释放时创建一个临时HashSet记录已经受击过的InstanceIDprivate HashSetint _hitCache new HashSetint(); void ApplyAreaDamage(Vector3 center, float radius, float damage) { Collider[] hits Physics.OverlapSphere(center, radius, targetLayerMask); for (int i 0; i hits.Length; i) { int id hits[i].gameObject.GetInstanceID(); if (_hitCache.Contains(id)) continue; _hitCache.Add(id); IDamageable target hits[i].GetComponentIDamageable(); target?.TakeDamage(damage, DamageType.Fire, true); } }这里的targetLayerMask我只勾选了“Enemy”和“Destructible”两层Avoid射到地板和场景。伤害公式我用的相对保守的配比穿刺是单体高倍率400%莲花爆炸是小范围多段60%每朵灼烧每秒20%加本身攻击力的百分比。暴击判定单独走一套避免数值溢出。每次技能释放前清空_hitCache否则上一次的受击记录会干扰下一次技能就容易出现敌人第二次中招不受伤的诡异情况。我一开始就是忘了清结果连续两次放技能只有第一次有伤害排查了半小时才反应过来。3. 技能攻击指示器与手感打磨Unity相关热词里有个“skill attack indicators 技能攻击指示器”赤莲破神枪刚好把两类指示器都做进去了直线穿刺指示器和范围爆炸指示器。指示器做得好不好直接决定技能看起来像“预判”还是“瞎按”。3.1 直线穿刺指示器怎么做下刺阶段玩家需要看到自己将要穿刺的路径我的方案是用一个动态的拖尾圆柱体加地面投影线。地面投影用LineRenderer画一条从角色正下方沿目标方向延伸的线段材质用半透明的红色渐变。关键实现点指示器的长度和方向要实时根据鼠标指向或手柄右摇杆方向更新。这里有个容易错的地方——屏幕坐标转世界坐标时一定要用可交互地面的平面做射线检测否则通过相机发出的射线会打到远处的墙面或天空盒上导致指示器歪掉。Ray camRay Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(camRay, out RaycastHit groundHit, 100f, groundLayerMask)) { targetPoint groundHit.point; }我的工程里groundLayerMask专门有一层叫“Ground”其他像墙体、障碍物都放在“Scenario”层指示器判定和实际落点判定都用Ground层能保证两边完全一致。穿刺前摇阶段再叠加一个角色朝向的调整让角色用Lerp旋转平滑转向指示器末端而不是瞬间转向。瞬间转向会显得僵硬Lerp可以用0.1到0.2秒的过渡时间让镜头和动画都能跟上。3.2 范围爆炸莲花区域提示落地后的莲花爆炸我用的是圆环填充圆的双层结构。外层圆环负责提示最大范围内层半透明填充告诉玩家中心区域伤害更高。这个效果我直接在Shader里用一个圆形遮罩加柔边做比贴一张带透明通道的贴图更省内存。做法是在ShaderGraph里做这样一个网络用UV坐标到圆心距离做Radial Gradient再乘以整体Alpha。圆环就用Step函数做出一个窄带。两个效果合并输出到Base Color的Alpha通道。HDR颜色可以给到1.5以上方便后期Bloom捕捉。指示器还有个细节爆炸圈应该跟随地形高度不能直接贴在平面贴图上。我把指示器做一个正上方的Raycast检测地面高度后把指示器的y轴贴到地面点上这样在坡道和台阶上也不会穿模。3.3 三段式打击节奏和镜头反馈手感这东西很大程度上是“节奏”和“反馈”堆出来的。赤莲破神枪我把技能分成了三段来打节奏起跳前摇0.15秒快速不拖沓化莲滞空0.6秒给玩家观察落地点的窗口下刺后命中瞬间做0.05秒的HitStop时间缩放然后紧接第一朵莲花爆炸HitStop的实现特别朴实就是把Time.timeScale临时设成0.05然后用协程在WaitForSecondsRealtime里等0.05秒再恢复。但注意用WaitForSecondsRealtime是因为timeScale变化后WaitForSeconds会被跳过这是个老坑了。镜头反馈我做了三样下刺落地时给摄像机加一个向下的冲击抖动幅度0.3到0.5衰减0.4秒莲花爆炸时加轻微的旋涡模糊效果用后处理里的Radial Blur强度0.15角色化莲时镜头拉远10%增强层次感。这些都不需要额外插件Unity自带的Cinemachine加URP后处理Volume就能实现。4. 特效与着色器实现细节特效是这个技能的第一印象。我用的渲染管线是URP粒子系统用Shuriken着色器部分用ShaderGraph和手写Shader混合。这部分我拆成三个点来讲赤莲姿态的搭建、枪势拖尾Shader、以及反向遮罩和Bloom的搭配。4.1 赤莲姿态如何拼接角色在空中“化莲”这一下最简单的做法不是真的让模型变成一朵花而是用粒子拼一朵出来。我在角色隐藏的同时实例化一个预制体“LotusForm”里面包含一个半透明的花瓣模型用Cone改的法线翻面后从外观看像个花瓣开口配合一圈Flame粒子做燃烧效果。花瓣的材质用的是双面渲染。这里就涉及热词里的“Unity双面材质Shader”问题URP下默认Lit材质不支持双面需要在ShaderGraph里打开Surface选项的Two Sided或者在代码里渲染的时候设置Cull Off。如果忘了开双面从内部看花瓣就是透明的空壳效果直接破功。火焰粒子我用了三种叠加外层大火焰使用Additive混合从花瓣底部向上喷射内层火芯Alpha Blend黄色到白色的渐变模拟高温火星拖尾几颗小粒子TrailMaterial用白色发光材质速度向上偏外粒子数量我控制在一朵莲花总共不超过200个粒子因为下刺后会连续生成好几朵。这里偷个懒不开Emission的Rate over Distance只开Rate over Time避免角色位移时产生意料之外的粒子发射量。4.2 用ShaderGraph做枪势拖尾下刺瞬间最抢眼的是一道从枪尖拉出的竖直光柱。这个我用TrailRenderer加一个自发光Shader实现。Shader的思路是用UV的V方向做渐变从枪尾到枪尖越来越亮再用UV的U方向做窄带遮罩让光柱看起来是“一条线”而不是“一块板”。ShaderGraph里的节点路径大概是这样Texture2D用一张1x256的渐变色Ramp图UV节点连到Plane的UV拆分出V轴Power节点调整渐变的锐度参数设在3左右用Saturate后的值作为Alpha和Emission强度最后的Unlit Master节点选Additive混合模式这样拖尾在暗背景下特别明显叠上Bloom后就是一道光柱。注意这里不要用Lit材质因为是纯视觉特效不受光照反而更稳。4.3 反向遮罩和辉光搭配热词里有“unity 反向遮罩组件”我在莲花爆炸那个瞬间实际用到了类似思路。爆炸中心会先出现一个暗色冲击波然后把亮色的火焰炸开来。这个“暗色冲击波”就是反向遮罩的典型用法——用一个Shader在Alpha通道里做环形遮罩中心透明度为0边缘为0.8效果是大圈边缘亮中心反而看到背后的场景。搭配Bloom时要注意Bloom的Threshold不能调太低URP里默认的Threshold是0.9我工程里设的是0.85。太低了会把所有发光物体都糊成一团太高了又看不到粒子核心。另一个坑是HDR颜色一定要挂对粒子材质里的Emission强度至少要给到1以上Bloom才会对粒子的高亮区域生效。反遮罩贴图我也踩过坑刚开始用的贴图是黑色背景白色光斑加在粒子系统上发现叠加后发灰。后来把贴图改成白色背景黑色遮罩混合模式用Multiply效果立刻干净了。这个细节有兴趣的可以直接在我的工程文件里看最终版的材质设置。5. 性能优化与常见坑分享工程出去之后很多人问我特效这么多手机上跑得动吗答案是能但要讲方法。赤莲破神枪的最终Demo在骁龙870的机器上可以稳定60帧1080P粒子峰值在800左右GC Alloc控制在每帧2KB以内。做到这一点靠的是对象池、批处理和材质实例化三个手段。5.1 对象池必须做而且要做两层技能里的伤害数字、莲花粒子、冲击波贴花都是频繁生成和销毁的对象。如果每次都用Instantiate和DestroyGC压力会立刻把帧数拉下来。我的对象池写法很朴素但够用public class SimplePoolT where T : Component { private readonly StackT _pool new StackT(); private readonly Transform _parent; public SimplePool(Transform parent) { _parent parent; } public T Rent() { return _pool.Count 0 ? _pool.Pop() : new GameObject(typeof(T).Name).AddComponentT(); } public void Return(T item) { item.gameObject.SetActive(false); item.transform.SetParent(_parent); _pool.Push(item); } }这里有个容易被忽略的细节对象池不仅要管特效的GameObject还要管每个粒子系统里的子发射器。也就是说粒子系统的预制体里如果还有TrailRenderer或者Light组件也要一并回收。我在花莲爆炸预制体里放了4个子粒子系统最开始只回收根节点结果子粒子在回收后还在播放导致视野里到处飘火星。后来在回收函数里写了一个递归禁用所有子粒子系统的逻辑问题才解决。5.2 阴影、材质实例化和Renderer Feature下刺动作角色高速移动如果阴影用实时阴影阴影贴图每帧都要重新渲染消耗很大。我的方案是下刺阶段把角色阴影切换成假阴影一张圆形黑色贴花贴在地面穿过头顶时贴花跟着地面投影走既稳定又省性能。正常情况下非技能状态才开实时阴影阴影距离控制在40米。材质实例化方面不直接改预制体上的材质而是用MaterialPropertyBlock控制颜色和发光强度。同一朵莲花在不同位置爆开颜色可能有轻微偏移但不会产生新的材质实例不然二十朵莲花就是二十个BatchDraw Call直接翻车。URP的Renderer Feature我也用了一个来画技能指示器这样指示器渲染跟场景渲染路径是分开的指令式绘制比额外摄像机渲染要便宜。贴士是只要不是必须用单独摄像机的情况优先用Renderer Feature后者会多一次全屏渲染。5.3 用Profiler盯紧两个峰值每做完一个技能模块我就用Profiler跑一遍主要看两个指标一个是GC Alloc要求每帧不超过10KB另一个是SetPass Call和Draw Call技能释放瞬间的峰值可以容忍到80但平时不能超过50。如果发现粒子特效导致SetPass Call翻倍多半是材质没有合批。URP里的粒子材质如果用Standard Surface几乎不可能合批。我都把火焰和拖尾材质改成Unlit Shader并关闭阴影投射Draw Call才掉下来。另一个GC峰值来自装箱。比如把int存进Dictionary、给协程传参里面只要有任何object类型变量就会产生Boxing Alloc。这问题特别隐蔽开着Profiler看一帧GC Alloc有几十B查了半天才发现是几处ToString和参数传递。修的时候把数字渲染专用一个StringBuilder就压下来了。6. 调试实录与常见问题速查这部分是我在实际调试中反复遇到的状况整理成速查表每个问题都附上排查思路和最终解法可以直接照着排。6.1 技能指示器落点偏移症状鼠标指向地面时指示器末端和目标点明显错位。原因我在做地面射线检测时光把LayerMask设成Everything射线打到了角色自身或者场景里的可交互物件上导致目标点被压在角色脚底。解法Ground层单独管理指示器和穿刺判定都只检测Ground层。不要用Everything哪怕是在原型阶段也不要图省事。6.2 多段伤害延迟触发的重复判断症状偶尔一个敌人同一朵莲花爆两次伤害。原因Bloom阶段用了多个独立的Timer每个Timer都调用一次ApplyAreaDamage而_hitCache在第一次爆炸后已经不清空了。第二个Timer检查时发现SameInstanceID已经存在但第三个Timer又在同一区域触发了新的爆炸把前一个区域的敌人算进来了。解法每次技能启动时清空_hitCache并把整个技能的伤害计算放在一个统一的结算队列里每个爆炸区域只入队一次由技能状态机统一驱动队列执行而不是各建各的Timer。6.3 Animator和SkillStateMachine时序冲突症状下刺动画播完但伤害已经触发或者动画卡在起跳时伤害触发了。原因Animator用的是普通状态机SkillStateMachine用的是协程两边的时间线独立没做同步。解法把所有影响手感的节点统一用Animation Event回调。在角色动画的关键帧上加事件比如“OnThrustHit”“OnBloomStart”这些事件再转发给我的SkillStateMachine。这样表现和逻辑天然对齐调参时只要拖动画事件的位置就行不用跑代码里改时间常量。6.4 常见问题速查表问题常见原因解决办法粒子发灰或颜色不对混合模式选错Additive用发光贴图Multiply用遮罩贴图花瓣从内部看透明材质没有双面渲染ShaderGraph打开TwoSided代码设Cull OffBloom糊成一团Threshold太低或强度太高Threshold调0.85左右Emission强度给1.5到2技能伤害偶尔不触发LayerMask没把Ground剔除单独建Ground层射线和范围检测都不检测Ground动画慢半拍协程受Time.timeScale影响用WaitForSecondsRealtime或你自定义Timer特效频繁实例化卡顿没有对象池特效和伤害数字全部走对象池根节点和子粒子一并回收指示器歪斜射线打到非地面层用专用Ground层做射线检测并加高度贴地逻辑这套技能做下来我最大的体会是一个看起来很炫的技能本质上就是“表现拆解 状态控制 判定时机 性能控制”四件事的排列组合。赤莲破神枪把这些模块整理好之后我换其他技能只需要重新组合状态机的阶段顺序和特效回调整个骨架完全不用动。如果你拿到的工程文件里有任何地方跟文章里描述不一致优先看ScriptableObject里那份“Hero_Skill_ChilPiao”的配置所有技能数值和阶段时间都集中在那边改完数值再去看代码会清楚很多。最后再分享一个小技巧调试技能手感时可以在输入层加一个“DebugForceSkill”的开关绑定一个键每按一次自动跳过前摇直接进入下刺阶段。我整个调参过程全靠这个键反复刷技能比手动跑完整流程效率高出一个数量级。这个开关我也留在工程里了默认是关闭的打开就能用。