尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity FPS 动画性能优化实战:从代码层面把 CPU 省下来

Unity FPS 动画性能优化实战:从代码层面把 CPU 省下来 一、先给动画代码加上测量标记优化前得先能看见。Unity 的 Profiler 默认能看到Animators.Update这种大类但如果你想知道自己写的动画脚本到底花了多少时间得手动打标记。用ProfilerMarker是最省事的做法usingUnity.Profiling;usingUnityEngine;publicclassWeaponSway:MonoBehaviour{// 定义一个标记名字随便起Profiler 里能搜到就行staticreadonlyProfilerMarkers_SwayMarkernewProfilerMarker(WeaponSway.Update);publicfloatswayAmount0.02f;publicfloatsmooth6f;Vector3initialPos;voidStart(){initialPostransform.localPosition;}voidLateUpdate(){// 用 using 包起来这段代码的耗时就会单独统计using(s_SwayMarker.Auto()){floatmouseXInput.GetAxis(Mouse X)*swayAmount;floatmouseYInput.GetAxis(Mouse Y)*swayAmount;Vector3targetnewVector3(-mouseX,-mouseY,0);transform.localPositionVector3.Lerp(transform.localPosition,initialPostarget,Time.deltaTime*smooth);}}}打开 Profiler跑起来之后在 CPU 模块里搜 “WeaponSway”就能看到这段代码单独占了多少时间。我当初就是这么发现我的后坐力脚本比 Animator 还费。小提示ProfilerMarker在正式打包时几乎没有开销可以放心留在代码里。二、管好不需要动的角色最基础的一步不动的角色Animator 就别开着空转。比如场景里的待机 NPC、还没被激活的敌人可以做一个简单的管理让它们在需要的时候才启用 AnimatorusingUnityEngine;publicclassAnimatorToggle:MonoBehaviour{Animatoranimator;boolisActivetrue;voidAwake(){animatorGetComponentAnimator();}// 敌人被唤醒比如玩家靠近、被警戒时调用publicvoidActivate(){if(isActive)return;isActivetrue;animator.enabledtrue;}// 敌人进入休眠走远了、死了时调用publicvoidDeactivate(){if(!isActive)return;isActivefalse;animator.enabledfalse;}}这个逻辑很朴素但很有效。关键是你得有个触发时机——一般结合 AI 的状态来调用。比如敌人在巡逻范围外时休眠玩家进入警戒距离再唤醒。三、用 Culling Mode 让屏幕外的动画自动停上面那个是手动管理其实 Unity 自己就有个更省事的机制Culling Mode。角色被摄像机剔除跑到屏幕外时Animator 可以自动停止计算。这个不用写太多代码一行就够usingUnityEngine;publicclassSetupAnimatorCulling:MonoBehaviour{voidAwake(){varanimatorGetComponentAnimator();// CullCompletely不可见时完全停止动画计算最省// CullUpdateTransforms不可见时不写回 Transform但仍评估状态机animator.cullingModeAnimatorCullingMode.CullCompletely;}}两种模式怎么选CullCompletely最省性能但角色重新进入视野时动画是从停下的那一刻接着播的可能会有一点突兀。适合远处的杂兵。CullUpdateTransforms状态机还在跑只是不更新骨骼位置。重新可见时衔接更自然但省得没那么彻底。适合可能随时被看到的角色。注意一个坑Culling 依赖 SkinnedMeshRenderer 的包围盒。如果你的角色有动画会让模型超出默认包围盒比如挥舞大武器可能会出现明明看得见却被剔除了的诡异情况。这时候需要调一下 Renderer 的 bounds或者干脆用 CullUpdateTransforms。四、按距离做动画 LOD重点这是性价比最高的一块。核心思路就一句话离玩家越远的角色动画更新得越糙玩家反正也看不清。Unity 没有内置的动画帧率降级得自己写。我的做法是给敌人挂一个动画更新调度器根据距离决定隔几帧更新一次usingUnityEngine;publicclassAnimationLOD:MonoBehaviour{Animatoranimator;Transformplayer;// 不同距离档位下每隔几帧更新一次动画// 1 每帧更新2 隔一帧3 隔两帧……[Header(距离阈值)]publicfloatnearDistance15f;// 15米内全速更新publicfloatmidDistance35f;// 35米内隔帧更新// 超过 midDistance更省的更新intframeCounter;intcurrentInterval1;voidAwake(){animatorGetComponentAnimator();// 关键把 Animator 的更新模式设成手动控制// 我们要自己决定什么时候让它算}voidStart(){// 找到玩家实际项目里建议用更靠谱的方式获取varplayerObjGameObject.FindWithTag(Player);if(playerObj!null)playerplayerObj.transform;}voidUpdate(){if(playernull)return;// 先根据距离确定当前该用哪个更新间隔floatdistVector3.Distance(transform.position,player.position);currentIntervalGetIntervalByDistance(dist);// 按间隔跳帧frameCounter;if(frameCountercurrentInterval){// 这一帧不更新动画直接禁用让它别算animator.enabledfalse;return;}frameCounter0;animator.enabledtrue;}intGetIntervalByDistance(floatdist){if(distnearDistance)return1;// 近处每帧算if(distmidDistance)return2;// 中等距离隔一帧return4;// 远处隔三帧}}这个思路能用但上面这种直接禁用 Animator的做法有点糙跳帧的时候动画会有轻微的卡顿感。如果对画面要求高更好的做法是用Animator.Update() 手动步进把跳过的时间一次性补上usingUnityEngine;publicclassAnimationLODManual:MonoBehaviour{Animatoranimator;Transformplayer;publicfloatnearDistance15f;publicfloatmidDistance35f;intframeCounter;intcurrentInterval1;floataccumulatedTime;// 累积跳过的时间voidAwake(){animatorGetComponentAnimator();// 关掉 Animator 的自动更新改成我们手动喂时间animator.updateModeAnimatorUpdateMode.Normal;animator.enabledfalse;// 禁用自动 Update靠脚本驱动}voidStart(){varplayerObjGameObject.FindWithTag(Player);if(playerObj!null)playerplayerObj.transform;}voidUpdate(){if(playernull)return;// 累积每帧的真实时间accumulatedTimeTime.deltaTime;floatdistVector3.Distance(transform.position,player.position);currentIntervalGetIntervalByDistance(dist);frameCounter;if(frameCountercurrentInterval)return;frameCounter0;// 手动推进动画把累积的时间一次性补进去// 这样即使跳帧动画整体进度也是对的animator.Update(accumulatedTime);accumulatedTime0f;}intGetIntervalByDistance(floatdist){if(distnearDistance)return1;if(distmidDistance)return2;return4;}}这里的关键是animator.Update(deltaTime)——手动把时间喂给 Animator让它一次算够。这样远处角色虽然实际只算了几帧但动画进度不会错乱。实际项目里别每个敌人都自己去FindWithTag找玩家、算距离那样脚本一多也是开销。更好的做法是搞一个全局管理器统一分发距离信息或者用平方距离sqrMagnitude避免开根号。下面会讲。五、用管理器统一调度别让每个脚本各干各的上面每个敌人自己Update里算距离敌人一多光是这些 Update 调用本身就有开销Unity 的 Update 反射调用不便宜。更好的做法是搞一个中央管理器统一遍历所有敌人usingSystem.Collections.Generic;usingUnityEngine;// 挂在场景里唯一一个空物体上publicclassAnimationLODManager:MonoBehaviour{publicstaticAnimationLODManagerInstance{get;privateset;}// 用平方距离省掉开根号floatnearSqr15f*15f;floatmidSqr35f*35f;Transformplayer;readonlyListManagedAnimatoragentsnewListManagedAnimator();voidAwake(){Instancethis;varpGameObject.FindWithTag(Player);if(p!null)playerp.transform;}// 敌人生成时注册进来publicvoidRegister(ManagedAnimatoragent){agents.Add(agent);}publicvoidUnregister(ManagedAnimatoragent){agents.Remove(agent);}voidUpdate(){if(playernull)return;Vector3playerPosplayer.position;// 一个 Update 里处理所有敌人避免大量独立 Update 调用for(inti0;iagents.Count;i){varagentagents[i];floatsqrDist(agent.transform.position-playerPos).sqrMagnitude;intinterval;if(sqrDistnearSqr)interval1;elseif(sqrDistmidSqr)interval2;elseinterval4;agent.Tick(interval,Time.deltaTime);}}}敌人这边就变得很轻只负责被调用不自己跑 UpdateusingUnityEngine;publicclassManagedAnimator:MonoBehaviour{Animatoranimator;intframeCounter;floataccumulatedTime;voidAwake(){animatorGetComponentAnimator();animator.enabledfalse;// 交给管理器驱动}voidOnEnable(){AnimationLODManager.Instance?.Register(this);}voidOnDisable(){AnimationLODManager.Instance?.Unregister(this);}// 由管理器统一调用publicvoidTick(intinterval,floatdeltaTime){accumulatedTimedeltaTime;frameCounter;if(frameCounterinterval)return;frameCounter0;animator.Update(accumulatedTime);accumulatedTime0f;}}这么改之后几十个敌人也只有一个 Update 在跑调度逻辑集中改起来也方便。六、参数哈希缓存养成习惯就行这个前一篇提过这里给个完整点的写法。核心就是别每帧传字符串usingUnityEngine;publicclassEnemyAnimationController:MonoBehaviour{// 提前把参数名转成哈希 ID只算一次staticreadonlyintSpeedHashAnimator.StringToHash(Speed);staticreadonlyintIsAimingHashAnimator.StringToHash(IsAiming);staticreadonlyintFireTriggerAnimator.StringToHash(Fire);Animatoranimator;voidAwake(){animatorGetComponentAnimator();}publicvoidSetSpeed(floatspeed){// 用哈希 ID不用字符串animator.SetFloat(SpeedHash,speed);}publicvoidSetAiming(boolaiming){animator.SetBool(IsAimingHash,aiming);}publicvoidFire(){animator.SetTrigger(FireTrigger);}}单次省的时间微乎其微但架不住每帧几十个角色反复设参数。这纯粹是好习惯没什么理由不这么写。七、程序化动画也别每帧都算FPS 的手感靠一堆程序化动画撑着——后坐力、摇摆、呼吸。这些通常在LateUpdate里跑。远处的敌人身上其实没必要跑这些细节。拿一个简单的后坐力做例子加个距离判断usingUnityEngine;usingUnity.Profiling;publicclassWeaponRecoil:MonoBehaviour{staticreadonlyProfilerMarkers_MarkernewProfilerMarker(WeaponRecoil.Update);publicTransformrecoilTarget;publicfloatrecoilAmount2f;publicfloatreturnSpeed10f;publicfloatsnappiness12f;// 只有近处才跑这套细节publicfloatmaxDistance20f;Transformplayer;Vector3currentRotation;Vector3targetRotation;voidStart(){varpGameObject.FindWithTag(Player);if(p!null)playerp.transform;}voidLateUpdate(){// 太远就直接不算省下这部分开销if(player!null){floatsqr(transform.position-player.position).sqrMagnitude;if(sqrmaxDistance*maxDistance)return;}using(s_Marker.Auto()){targetRotationVector3.Lerp(targetRotation,Vector3.zero,returnSpeed*Time.deltaTime);currentRotationVector3.Slerp(currentRotation,targetRotation,snappiness*Time.deltaTime);recoilTarget.localRotationQuaternion.Euler(currentRotation);}}// 开枪时调用publicvoidKick(){targetRotationnewVector3(-recoilAmount,Random.Range(-recoilAmount,recoilAmount)*0.5f,0);}}远处敌人身上的后坐力、摇摆这些细节玩家根本注意不到直接跳过就行。八、GPU Skinning 试试看但一定要实测蒙皮网格变形这一步可以从 CPU 挪到 GPU。开启很简单Project Settings → Player → Other Settings → 找到 GPU Skinning勾上。但我要泼个冷水GPU Skinning 不一定更快。它把负担从 CPU 移到了 GPU如果你的游戏本来 GPU 就吃紧或者角色骨骼多但顶点少开了反而可能更慢。所以务必用 Profiler 前后对比别听人说好就无脑开。看这两个数据CPU 那边的MeshSkinning.Update有没有降下来GPU 那边的耗时有没有涨太多平衡好了再决定用不用。九、Optimize Game Objects导入时勾一下这个不是代码但很重要。选中你的角色模型在 Inspector 的Rig标签页里找到Optimize Game Objects勾上它。Unity 会用更紧凑的方式存骨骼数据算起来 cache 命中率更高速度更快。如果你需要在某根骨头上挂东西比如武器要挂在手上点开旁边的 Extra Transforms to Expose把需要的骨头单独暴露出来就行不影响优化效果。收个尾把这几套东西串起来一个大概的优化流程就是先用 Profiler ProfilerMarker 找到真正的瓶颈别瞎猜看不见的用 Culling 停掉一行代码的事远处的用动画 LOD 降频配合中央管理器统一调度不动的关掉 Animator别空转参数用哈希、程序化动画按距离裁剪这些是日常习惯GPU Skinning 和 Optimize Game Objects 试一下实测决定这些代码都是从实际项目里抠出来的思路你不一定要全用挑你项目里瓶颈最明显的地方下手就行。优化这事儿最忌讳一上来就全套堆上去很多时候改一两个地方帧率就回来了。老规矩改完记得回 Profiler 看数字降了才算数。
返回列表