场景
场景构成
对于场景一级节点要做好命名和管理,有助于场景的编辑与优化。按功能划分一级节点或使用空节点注释与隔断在性能上并没有太大区分,取决于项目喜好。
- 摄像机
- 灯光(2d场景不一定有)
- 静态场景游戏对象
- 代码动态创建的游戏场景对象
- UI节点
- 辅助游戏节点对象
场景结构设计原则
- 合理设计场景一级节点的同时,避免场景节点深度太深,一些代码生成的游戏对象如果不需要随父节点进行Transform的,一律放到根节点下。
- 太深的层级保存引用或GO节点查找、父子节点的transform(2018Unite提到,有父子节点的transform效率比没有父子节点差很多,原因在于下图的逐级脏标记)都会带来效率问题
- 太深的层级保存引用或GO节点查找、父子节点的transform(2018Unite提到,有父子节点的transform效率比没有父子节点差很多,原因在于下图的逐级脏标记)都会带来效率问题
- 尽量使用Prefab节点构建场景,而不是直接创建的GameObject节点
- 通过对比是否使用Prefab节点的场景文本文件
- 发现直接创建会带有GO节点的所有组件信息在场景文件中
- 使用Prefab节点则是用field id和guid指向Prefab节点的实例引用
- 使用Prefab能减少场景文件大小从而减低包体体积
- 配合asset bundle能节省内存使用量,多个场景引用的同一个prefab是同一块内存(有待测试)
- 避免DontDestroyOnLoad节点下有太多生命周期过长或引用资源过多的复杂节点对象。Additve模式添加的场景要尤为注意。
- Unity通过场景切换来创建和卸载资源
- 如果DontDestroyOnLoad节点引用太多资源,则不会被释放
- 实在需要DontDestroyOnLoad需要自己手动释放一些暂时用不到的资源
- 最好为一些需要经常访问的节点添加tag,静态节点一定要添加Static标记。
- 复杂场景中,对于设置好Tag的节点,使用FindGameObjectWithTag方法取查找该节点更高效
- 静态物体可以让其忽略去参与一些功能的预计算和逻辑更新,减少游戏开销
- 复杂场景中,对于设置好Tag的节点,使用FindGameObjectWithTag方法取查找该节点更高效
预制体
预制体Prefab
Unity中的预制体是用来存储游戏对象、子对象及其所需组件的可重用资源,一般来说预制体资源可充当资源模版,在此模版基础上可以在场景中创建新的预制体实例。
使用预制体的好处
- 由于预制体系统可以自动保持所有实例副本同步,因此可以比单纯地简单复制粘贴游戏对象做到更好的对象管理。
- 此外通过预制体嵌套(Nested Prefabs)可以将一个预制体嵌套到另一个预制体中,从而创建多个易于编辑的复杂游戏对象层级视图(如房子由窗户、家具、地板等预制体组合)。
- 可以通过覆盖各个预制体实例的设置来创建预制体变体(Prefabs Variant),从而可以将一系列覆盖组合在一起形成有意义预制体的变化。
嵌套预制体与单预制体相比的优点与缺点
- 优点:
- 嵌套预制体方便预制体管理,方便资源重复利用,易于统计场景复杂度
- 美术制作时可以比较合理的分配UV,和贴图利用率。意思是不用直接将整个场景直接设计建模展庞大uv,而是从基础单元入手去组合。
- 方便关卡设计人员发挥,充分合理利用资源
- 嵌套预制体比较方便利用工具做LOD,LOD效果也比较好。因为基础组成都是简单闭包模型,工具自动生成lod不容易出错,生成的uv也会比较合理(复杂模型容易出现点面没有焊接好,从而导致工具生成算法出错)
- 嵌套预制体修改方便,只需修改子预制体就可以做到所有嵌套预制体同步
- 比较方便做场景遮挡剔除,可以做到精细的遮挡剔除优化效果
- 缺点:
- 手动做Bundle依赖时要按Scene方式处理,依赖关系较为复杂(因为越往上引用的东西越多)
- 可能会增加材质数量与Drawcall数量(都是小零件,材质也不同,合批容易被打断。但可以使用遮挡剔除和srp batch优化)
- 不太适合做大规模远景对象。因为用基础物体组合无法做场景剔除和LOD(感觉是因为都看见了,所以不剔除,然而小物体太多也没有合批,所以性能会差一点),还是要用模型替代体或模型面片
- 美术与关卡设计人员要充分考虑组合复杂度与特例场景显示,避免重复性和单一性,需要更多的沟通成本
使用Prefab变体的一些限制
- 不能改变本体Prefab游戏对象 (GameObject)层级
- 不能删除本体Prefab中的游戏对象,但可以通过Deactive游戏对象来达到与删除游戏对象同样的效果
- 对于Prefab变体要保持其Override属性的变化,不能通过Apply to base把这些变化应用到本体Prefab上,这样会破坏基础Prefab的结构和功能。
UGUI
除了课程视频外,推荐这篇文章学习:https://zhuanlan.zhihu.com/p/125054290
Unity UI性能的四类问题
- Canvas Re-batch 时间过长
- Canvas Over-dirty, Re-batch次数过多
- 生成网格顶点时间过长(使用profiler查看每个批次的顶点数)
- Fill-rate overutilization(UI shader中GPU片元着色器利用率过高,即overdraw问题)
相关名词解释
Canvas画布
- Canvas负责管理UGUI元素,负责UI渲染网格的生成与更新,并向GPU发送DrawCall指令
- 这部分工作在引擎native层,由C++完成
- 对于每个Canvas对象,在绘制之前都要进行一次合批的过程,如果canvas底下的元素这一帧和上一帧保持不变,会使用上一帧合批的结果来节省性能
- 如果画布发生变化则要重新匹配几何体,被标记为dirty触发重新合批
Canvas Re-batch过程
这部分工作在C++层以多线程进行,即不同设备因核心数不同工作速度也不同。
- 根据UI元素深度关系进行排序
- 检查UI元素的覆盖关系
- 检查UI元素材质并进行合批
Canvas Over-dirty原因
Unity的UI渲染是基于画布(Canvas)的,画布下面挂载了一些UI元素,每个UI元素又可以包含其他UI元素。当一个UI元素发生变化时,Unity会将该元素所在的画布标记为Dirty,表示需要重新渲染。
UGUI的构建顺序是由底至顶的,合批的构建顺序是在hierarchy中由上至下遍历并将使用相同材质、贴图且没有中间层隔断的物体合并到一个 draw call。所谓的“中间层”即使用了不同材质且与可以合批的物体产生了遮挡关系的物体,中间层会打断合批。
合批被打断在图文混排的时候最容易发生,文字的实际边缘是比肉眼可见的文字要大一些的一个透明背景,这个透明部分可能与图片发生了重叠,这时需要调制物体的顺序使不能合批的全都放在最上层,或者调制物体位置使他们不会互相遮挡。这两个操作都可以在开启 Frame Debugger 时进行,方便观察效果。
合批步骤的第二步,覆盖关系的改变导致的Over-dirty问题:
- 课程中举了个例子,一个ui按钮包括底板和文字,分别不同的材质负责,所以是两个批次
- 如果创建多一个按钮,此时按钮之间没有覆盖关系(准确的说是底板和文字两者的覆盖关系没有发生变化时),还是两个批次
- 如果将第二个按钮的底板遮挡了第一个按钮的文字,则覆盖关系发生改变,打断了合批,从而变成了四个批次
UGUI渲染细节
- UGUI中渲染是在Transparent半透明渲染队列中完成的,半透明队列的绘制顺序是从后往前画,由于UI元素做Alpha Blend,我们在做UI时很难保障每一个像素不被重画,UI的Overdraw太高,这会造成片元着色器利用率过高,造成GPU负担。
- UI SpriteAtlas图集利用率不高的情况下,大量完全透明的像素被采样也会导致像素被重绘,造成片元着色器利用率过高;同时纹理采样器浪费了大量采样在无效的像素上,导致需要采样的图集像素不能尽快的被采样,造成纹理采样器的填充率过低,同样也会带来性能问题。
Re-Build过程
Re-Build是在Re-Batch过程中完成的,主要逻辑在c#层,用来进行计算layout布局与渲染网格重建
- 在WillRenderCanvases事件调用PerformUpdate::CanvasUpdateRegistry接口,主要工作有:
- 通过ICanvasElement.Rebuild方法重新构建Dirty的Layout组件
- 通过ClippingRegistry.Cullf方法,任何已注册的裁剪组件Clipping Compnents(Such as Masks)的对象进行裁剪剔除操作
- 任何Dirty的 Graphics Compnents都会被要求重新生成图形元素
- Layout Rebuild(以下情况被标记成dirty)
- UI元素位置、大小、颜色发生变化
- 优先计算靠近Root节点,并根据层级深度排序
- Graphic Rebuild(以下情况被标记成dirty)
- 顶点数据被标记成Dirty
- 材质或贴图数据被标记成Dirty
chatgpt 回答的rebuild和rebatch关系
在Unity中,Canvas是UI的基础组件,用于呈现2D UI元素。当Canvas需要重新渲染的时候,Unity会通过执行两个主要步骤来更新它:Rebuild和Rebatch。
Rebuild(重建)是指重新生成Canvas的几何结构。这个过程包括计算和设置UI元素的位置、大小、缩放以及裁剪区域等。在这个过程中,Unity会检查所有的UI元素是否需要更新并标记为Dirty。Dirty标记表示该元素需要重新构建和渲染。
Rebatch(重新批处理)是指将所有Dirty元素进行批处理。这个过程会将所有需要重新渲染的UI元素进行排序、合并和划分,以最小化渲染状态的切换次数,从而提高性能。
Canvas Over-dirty问题是由于在Canvas重建期间,每个UI元素的Dirty标记被设置,即使在它们不需要被重新构建的情况下也是如此。这会导致一些不必要的重建和渲染操作,从而影响性能。为了避免这个问题,可以使用优化技术,如合并相邻的UI元素、使用不透明的UI元素和减少UI元素的数量等。
UGUI性能优化
使用Canvas的基本准则:
- 将所有可能打断合批的层移到最下边的图层,尽量避免UI元素出现重叠区域(特别小心小物体,比如字体)
- 可以拆分使用多个同级或嵌套的Canvas来减少Canvas的Rebatch复杂度(但是canvas间不会合批,需要权衡)
- 拆分动态和静态对象放到不同Canvas下。
- 不使用Layout组件
- Canvas的RenderMode尽量Overlay模式,减少Camera调用的开销
UGUI射线(Raycaster)优化:
- 必要的需要交互UI组件才开启“Raycast Target”
- 开启“Raycast Targets”的UI组件越少,层级越浅,性能越好
- 对于复杂的控件,尽量在根节点开启“Raycast Target”
- 对于嵌套的Canvas,OverrideSorting属性会打断射线,可以降低层级遍历的成本
UI字体
-
避免字体框重叠,造成合批打断
-
字体网格重建(UIText和TMP Text Pro都是四边形网格,由两个三角形组成)
- UIText组件发生变化时
- 父级对象发生变化时
- UIText组件或其父对象enable/disable时
- 以上重建都会重新计算用于显示文本的多边形
-
动态字体(字体导入中的Dynamic选项)与字体图集
- 运行时,根据UIText组件内容,动态生成字体图集,只会保存当前Actived状态的 UIText控件中的字符
- 不同的字体库维护不同的Texture图集
- 字体Size、大小写、粗体、斜体等各种风格都会保存在不同的字体图集中(有无必要,影响图集利用效率(可以用第三方工具对原字体进行修改,比如去掉斜体),一些利用不多的特殊字体可以采用图片代替或使用Custom Font,Font Assets Creater创建静态字体资源)
- 当前Font Texture不包含UIText需要显示的字体时,当前Font Texture需要重建
- 如果当前图集太小,系统也会尝试重建,并加入需要使用的字形,文字图集只增不减,尺寸规格为2的幂次方宽高
- 利用Font.RequestCharacterInTexture可以有效降低启动时间和动态扩展时的时间
-
UI控件优化注意事项
- 不需要交互的UI元素一定要关闭Raycast Target选项
- 如果是较大的背景图的UI元素建议也要使用Sprite的九宫格拉伸处理,充分减小UI Sprite大小,提高UI Atlas图集利用率
- 对于不可见的UI元素,一定不要使用材质的透明度控制显隐,因为那样UI网格依然在绘制,也不要采用active/deactive UI控件进行显隐,因为那样会带来gc和重建开销。尽量通过canvas控件的激活与关闭(当然也要看情况,如果是一张的话建议挪到看不见的地方吧)。
- 使用全屏的UI界面时,要注意隐藏其背后的所有内容,给GPU休息机会(禁用对应的camera component)。
- 在使用非全屏但模态对话框时,建议使用OnDemandRendering接口,对渲染进行降频。这个接口只降低渲染频率,而targetfps降低渲染频率同时还会影响输入频率
- 优化裁剪UI Shader,根据实际使用需求移除多余特性关键字。
-
滚动视图Scroll View优化(大量元素会有实例化开销和rebuild开销)
- 使用RectMask2d组件裁剪,需要标准矩形(原理是rect相交检测而不是模板缓冲)。不规则需要pixel mask写额外着色器
- 使用基于位置的对象池作为实例化缓存
- 网上有很多优化方案
物理
hh怎么也算编辑器资源优化
Unity中的物理
解决方案 | 描述 |
---|---|
Box2D | 2d的默认解决方案,是一套高效、开源的2d刚体物理c++代码库 |
Nvidia PhysX | 3d的默认解决方案,由NVIDIA收购而来,支持cuda加速。但缺点是非确定性物理模拟库(状态同步表现会不同) |
Unity Physics | 符合dots理念并为确定性物理模拟,比起Nvidia PhysX会缺少一些物理模拟功能,高性能简单功能模拟,可以在单帧内多次物理模拟从而预测走向(不支持显卡加速,但有dots多核加持) |
Havok Physics for Unity | 符合dots设计理念,数据与Unity Physics通用,可以用来增强Unity Physics。但是需要授权开发($) |
Unity物理设置
设置位置
Project Settings->Physics/Physics 2D
部分设置讲解
设置 | 描述 |
---|---|
Layer Collision Matrix | 最重要的设置,要确定好那些Layer需要互相碰撞 |
Auto Sync Transforms | Transforms组件发生变化时强制同步物理,一般不开启,等待FixedUpdate,表现上差别不会太大 |
Reuse Collision Callbacks | 推荐开启,物理引擎发生碰撞回调时会重用之前的Collision对象实例,减少gc开销 |
Default Solver [Velocity] Iterations | 分别与物理碰撞精度和碰撞后物理模拟精度有关,一般保持默认,有特殊需求可以修改这个迭代次数(越高越精确越耗性能) |
Broadphase Type | 与场景加速算法有关(应该是八叉树BVH那类),一般保持默认,有特殊需求可以设置 |
Auto Simulation | 去掉勾选后需要手动用Physics.Simulation去手动调用物理更新 |
Time-Fixed Timestep | 物理计算频率,如果过低子弹穿薄物体可能会有问题,可以用射线检测或两帧之间的位置创建碰撞盒检测 |
Time-Maximum Allowed Timestep | 帧数下降时限制物理更新的最大间隔,避免物理错误,一般推荐设置8-10fps |
优化建议
Unity中的物理组件Collider部分的优化
- Trigger与Collider
- Trigger对象的碰撞会被物理引擎所忽略,通过OnTriggerEnter/Stay/Exit函数回调
- Collider对象由物理引擎触发碰撞,通过OnCollisionEnter/Stay/Exit函数回调
- 发生碰撞或触发都必须至少有一个对象有RigidBody组件,两者还要有Collider
- Trigger对象更高效
Unity中的物理组件Collider部分的优化
- 尽量少使用MeshCollider,可以用简单Collider代替,即使用多个简单Collider组合代替也要比复杂的MeshCollider来的高效
- MeshCollider是基于三角形面的碰撞
- MeshCollider生成的碰撞体网格占用内存也较高
- MeshCollider即使要用也要尽量保障其是静态物体
- 可以通过PlayerSetting选项中勾选Prebake Collision Meshes选项来在构建应用时预先Bake出碰撞网格。
Unity中的物理组件RigidBody部分的优化
- Kinematic与RigidBody
- Kinematic对象不受物理引擎中力的影响,但可以对其他RigidBody施加物理影响。
- RigidBody完全由物理引擎模拟来控制,场景中RigidBody数量越多,物理计算负载越高
- 勾选了Kinematic选项的RigidBody对象会被认为是Kinematic的,不会增加场景中的RigidBody个数
- 场景中的RigidBody对象越少越好
Unity中的RayCast与Overlap部分的优化
- Unity物理中RayCast与Overlap都有NoAlloc版本的函数,在代码中调用时尽量用NoAlloc版本,这样可以避免不必要的GC开销
- 尽量调用RayCast与Overlap时要指定对象图层进行对象过滤,并且RayCast要还可以指定距离来减少一些太远的对象查询
- 此外如果是大量的RayCast操作还可以通过RaycastCommand的方式批量处理,充分利用JobSystem来分摊到多核多线程计算。
动画
Unity动画系统回顾
- Animation
- Animator
- Playable API
Animation的一些细节
- 播放单个AnimationClip速度,Legacy Animation系统更快,因为老系统是直接采样曲线并直接写入对象对应组件属性。而新的Mecanim系统需要用于混合的临时缓冲区,并会对采样曲线以及其他数据进行额外的赋值。(对于单个clip播放而言)
- 针对动画的缩放曲线比位移、旋转曲线开销更大
- 常数曲线不会每帧写入场景,更高效
Animator的一些细节
- 不要使用字符串来查询Animator
- 使用曲线标记来处理动画事件
- 使用Target Marching函数来协助处理动画
- 将Animator的CullingMode设置成Based On Renderers来优化动画,并禁用SkinMesh Renderer的Update When Offscreen属性来让角色不可见时动画不更新
Internal Animation Update
- 白色部分是动画系统更新的回调
- 灰色部分是动画系统的关键步骤
- ProcessGraph对需要评估的所有Animation Clip进行采样以及计算根骨动画
- ProcessAnimation计算动画图混合结果
- WriteTransform是将所有动画的变换从工作线程写入场景的transform中
- WriteProperties是将主线程中其他对象属性写入场景对象中
- 详情可查阅https://docs.unity3d.com/Manual/ExecutionOrder.html
Animator VS Animation
- Animation可以将任何对象属性制作成Animation Clip, Animator是将Animaiton Clip组织到状态机流程图中使用
- Animation与Animator播放动画时的效率是有个临界点的,这个临界点是根据动画曲线条数来的,当动画曲线条数小于这个临界点时Animation快,当动画曲线条数大于这个临界点时Animator快
- 当Cpu核数较少时,Animation播放动画有优势,当Cpu核数较多时,Animator表现会更好
- 动画组件比较复杂时Animator运行效率高
- Animator Controller Graph中的所有动画节点的Animation Clip都会载入到内存中,当有海量动画状态机节点时,内存开销较大
Playable API VS Animator
建议学习了解Playable API,来解决动画状态机过复杂时导致的内存问题,以及可以扩展Timeline(从而作为技能编辑器/过场动画编辑器)
Playable API优点
- 支持动态动画混合,可为场景中的对象提供自己的动画,并可以动态添加到PlayableGraph当中使用
- 允许创建播放单个动画,而并不会产生创建和管理AnimatorController资源所涉及的开销,可更加灵活的控制PlayableGraph的数据流,可以插入自定义的AimationJob来并行处理。
- 可以控制动画文件加载策略,按需加载、异步加载等(Animator初始化时动画加载开销问题)
- 允许用户动态创建混合图,并直接逐帧控制混合权重(甚至可以混合AniationClip与AnimatorController动画)
- 可以运行时动态创建,根据条件添加可播放节点。而不需要提前提供一套PlayableGraph运行时启动和禁用节点,可以做到自由度更高的override机制
- 可加载自定义配置数据,更加方便的和其他游戏系统整合
Playable API缺点
- 没有直接使用Animator直观
- 混合模式没有现成的,需要自己实现
- 需要开发更多的配套工具
- 有一定学习成本
解决方案选择
- 一些简单、少量曲线动画可以使用Animation或动画区间库如Dotween\iTween等完成,如UI动画,Transform动画等。
- 角色骨骼蒙皮动画如果骨骼较少,Animation Clip资源不多,对动画混合表现要求不高的项目可以采用Legacy Animation。注意控制总体曲线数量
- 一些角色动画要求与逻辑有较高的交互、并且动画资源不多的项目可以直接用Animator Graph完成(小心动画机节点数量,不然初始化内存问题)
- 一些动作游戏,对动画混合要求较高、有一些高级动画效果要求、动画资源量庞大的项目,建议采用Animator+Playable API扩展Timeline的方式完成。(状态机中只有基础的动画节点,其他行为由扩展增加)