静态优化
Import
Unity 工程目录结构及用途
Asset
这是存储所有游戏资源的文件夹,包括脚本、纹理、声音和自定义编辑器脚本。文件夹的组织可能因项目而异,因组织而异。资产文件夹中可以有许多子文件夹,具体取决于项目的组织方式。例如,可能有一个用于场景的文件夹,一个用于脚本,一个用于音频,或者一个用于 Sprite,组织有多深是没有限制的
{% note primary %}
Unity 在 Assets 文件夹下保留了几个特殊文件夹名称。这些文件夹是编辑器、编辑器默认资源、Gizmos、资源、标准资产和流资产,并非每个项目都有所有这些文件夹
Assets / Editor:此文件夹用于扩展 Unity 编辑器功能的自定义编辑器脚本。这些脚本将在编辑器中运行,但在运行时不会在项目中运行。资产文件夹中可以存在多个编辑器文件夹,编辑器脚本的执行方式因编辑器文件在文件夹结构中的位置而异。
Assets / Editor Default Resources: 这是存储编辑器脚本使用的资产文件的地方。只能有一个这样的文件夹,它必须放在资产文件夹根目录中,此文件夹中可能有子文件夹
Assets / Gizmos: Gizmos 是场景视图中的图形,可以帮助可视化设计细节。此文件夹存储用于小发明的图像,只能有一个文件夹,它必须放在资产文件夹根目录中
Assets / Resources:此文件夹存储资源,以便在 Unity 项目中按需加载。可以有多个资源文件夹。按需加载有助于动态加载在设计期间没有设计师创建的实例的游戏对象。换句话说,这些资源在设计时可能没有在场景中放置相应的游戏对象,并在运行时动态加载,但现在都使用 Assets Bundle 构建
Assets / Standard Assets :此文件夹存储已导入项目的任何标准资产包。脚本编译优先级最高,只能有一个标准资产文件夹。标准资产是由 Unity 维护的免费资产
Assets / Streaming Assets : 此文件夹用于保留其原始格式的资产,然后流式传输到 Unity 应用程序中,而不是直接将其纳入项目的构建中。一个例子是来自文件系统的视频文件。项目中只能有 1 个流媒体资产文件夹
{% endnote %}
Library
用来存储项目内部资产数据信息的目录,即 Unity 用于导入资产的本地缓存。它可以被删除,并将由 Unity 自动重新生成,重新创建文件夹所需的只是 Assets 文件夹 和 .meta
文件。如果此文件夹被删除,Unity 将重新导入所有资产,并在下次在编辑器中打开项目时重新生成文件夹。此文件夹不应包含在版本控制中,Unity 使用这些导入的资产来节省 Unity 运行时的时间
库文件夹中特别值得注意的是软件包缓存文件夹。这包含有关当前项目安装的所有软件包的信息。虽然这可以像库文件夹中的其他项目一样由 Unity 重新生成,但出于存档目的,重要的是不要删除此文件。这是因为能够查看项目中包含哪些软件包可能会有所帮助,而无需重新生成缓存,这需要以适当的编辑器版本打开项目
Packages
用来存储项目的包文件信息,此文件夹包含 JSON 格式的清单文件,用于维护软件包之间的依赖项链接。它还包含一个文件,其中列出了与项目一起安装的单个软件包。这些由 Unity 软件包管理器使用。软件包管理器已添加到 Unity 2018.1 中。在这些情况下,先前版本的 Unity 将不包含软件包管理器,并且软件包文件夹将不存在
Project Settings
用来存储项目设置的信息,此文件夹包含所有项目设置。项目设置菜单中设置的所有内容。它还包括编辑器版本号和构建设置,以及 Unity 系统使用的许多其他设置。编辑器版本号作为独立文件,直到 Unity 5 才添加。对于之前的任何版本,编辑器版本号都可以在项目设置文件中找到
UserSettings
用来存储用户设置信息
Temp
用来存储使用Unity编辑器打开项目时的临时数据,一旦关闭Unity编辑器也会被删除
Logs
用来存储项目的日志信息(不包含编辑器日志信息)
忽略导入的文件夹
1. 隐藏的文件夹
2. 以 `.` 开头的文件和文件夹
3. 以 `~` 结尾的文件和文件夹
4. 扩展名为 `.cvs` 的文件和文件夹
5. 扩展名为 `.tmp` 的文件夹
Assets 目录结构设计(仅 UP 建议,不作为标准)
一级目录设计原则:
- 目录尽可能少
- 区分编辑模式与运行模式
- 区分工程大版本
- 访问场景文件、全局配置文件便捷
- 不在一级目录做资源类别区分,只有 Video 类视频建议直接放到 StreamAssets 下
二级目录设计原则:
- 只区分资源类型
- 资源类型大类划分要齐全
- 不做子类型区分
- 不做功能区分
- 不做生命周期区分
三级目录设计原则:
- Audio / Texture / Models 三级目录做子类型区分
- 其他类型资源可按功能模块 / 生命周期区分
四级目录设计原则:
- 只有 Audio / Texture / Models 做四级目录,可按功能模块 / 生命周期划分
要点
- 不要在文件和文件夹名称中使用空格,因为我们 Unity 命令行工具无法自动处理带有空格的路径
- 不要在根目录中存储任何资产文件,尽可能使用子目录。
- 除非您真的需要,否则不要在根目录中创建任何额外的目录
- 与命名保持一致,如果决定将驼峰命名法用于目录名称,那么所有文件都应保持一致
- 使用单独的第三方文件夹来存储从资产商店导入的资产,他们通常有自己的结构,不应该改变
- 对于不完全确定的任何实验,请使用 SandBox 目录,当您与其他人一起处理项目时,请创建您的个人沙盒子目录,如:SandBox / San
资源导入工作流
手动编写工具
优点:根据项目特点自定义安排导入工作流,并且可以和后续资源制作与大包工作流结合
缺点:存在开发和维护成本,会让编辑器菜单界面变得复杂,对新人理解工程不友好
适合类型:大型商业游戏团队
AssetPostprocessor
:
编写编辑器代码继承 AssetPostprocesser 对象自定义实现一些列 OnPreprocessXXX 接口修改资源导入设置属性
AssetsModifiedProcessor
(新试验接口):
资源被添加、删除、修改、移动时回调该对象的 OnAssetsModified 接口
利用 Presets 功能
优点:使用简单方便,只需要 Assets 目录结构合理规范即可
缺点:无法和后续工作流整合,只适合做资源导入设置。
适合类型:小型团队或中小规模项目
利用 AssetGraph 工具
优点:功能全,覆盖 Unity 资源工作流全流程,节点化编辑,直观
缺点:有一定上手成本,一些自定义生成节点也需要开发,不是 Unity 标准包,Unity 新功能支持较慢
适合类型:任何规模项目和中大型团队
AssetGraph 仓库地址:[https://github.com/Unity-Technologies/AssetGraph]
Create
场景结构设计原则
合理设计场景一级节点的同时,避免场景节点深度太深,一些代码生成的游戏对象如果不需要随父节点进行 Transform 的,一律放到根节点下
尽量使用 Prefab 物体构建场景,而不是直接创建的 GameObject 物体
{% note info %} 用文本方式打开 Unity 场景会看到一系列引用信息,而如果创建一个 GameObject,则会记录详细相关参数,而如果引用的 Prefab,会直接引用加载好的 bundle 中的对象,这样构建引用速度会更快,更多参考下面的 Prefab {% endnote%}
避免
DontDestroyOnLoad
节点下有太多生命周期过长或引用资源过多的复杂节点对象。Additve 模式添加的场景要尤为注意最好为一些需要经常访问的节点添加 tag,静态节点一定要添加 Static 标记
{% note primary%}
注意:复杂场景中,对于设置好 Tag 的节点,使用 FindGameObjectWithTag
方法取查找该节点更高效
{% endnote%}
Prefab
Unity 中的预制体是用来存储游戏对象、子对象及其所需组件的可重用资源,一般来说预制体资源可充当资源模版,在此模版基础上可以在场景中创建新的预制体实例
优点
- 由于预制体系统可以自动保持所有实例副本同步,因此可以比单纯地简单复制粘贴游戏对象做到更好的对象管理
- 此外通过预制体嵌套(Nested Prefabs)可以将一个预制体嵌套到另一个预制体中,从而创建多个易于编辑的复杂游戏对象层级视图
- 可以通过覆盖各个预制体实例的设置来创建预制体变体(Prefabs Variant),从而可以将一系列覆盖组合在一起形成有意义预制体的变化
嵌套预制体与单预制体相比的优点与缺点
优点:
- 嵌套预制体方便预制体管理,方便资源重复利用,易于统计场景复杂度
- 美术制作时可以比较合理的分配 UV、贴图利用率
- 方便关卡设计人员发挥,充分合理利用资源
- 嵌套预制体比较方便利用工具做 LOD,LOD 效果也比较好
- 嵌套预制体修改方便,只需修改子预制体就可以做到所有嵌套预制体同步
- 比较方便做场景遮挡剔除,可以做到精细的遮挡剔除优化效果
缺点:
- 手动做 Bundle 依赖时要按 Scene 方式处理,依赖关系较为复杂
- 可能会增加材质数量与 Drawcall 数量
- 不太适合做大规模远景对象
- 美术与关卡设计人员要充分考虑组合复杂度与特例场景显示,避免重复性和单一性,需要更多的沟通成本
注意
- 使用单独的预制件进行专业化,不要专业化实例
如果您有两个敌人类型,并且它们仅因其属性而不同,请为属性制作单独的预制件,并将它们链接起来,这使得有可能在一个地方对每种类型进行更改或者无需更改场景即可进行更改
- 将预制件链接到预制件
不要将实例链接到实例:将预制件放入场景时,会保留预制件的链接;不会保留到实例的链接。尽可能链接到预制件可以减少场景设置,并减少更改场景的需要
- 尽可能在实例之间自动建立链接
如果您需要链接实例,请以编程方式建立链接。例如,玩家预制件可以在启动时向 GameManager 注册自己,或者 GameManager 可以在启动时找到 Player 预制件实例。
- 如果您想添加其他脚本,请不要将网格放在预制件的根部
当您从网格制作预制件时,首先将网格父到空游戏对象,并将其作为根。将脚本放在根上,而不是网格节点上。这样,用另一个网格替换网格就容易得多,而不会丢失您在检查器中设置的任何值
使用 Prefab 变体的一些限制
- 不能改变本体 Prefab 游戏对象 (GameObject)层级
- 不能删除本体 Prefab 中的游戏对象,但可以通过 Deactive 游戏对象来达到与删除游戏对象同样的效果
- 对于 Prefab 变体要保持其 Override 属性的变化,不能通过 Apply to base 把这些变化应用到本体 Prefab 上,这样会破坏基础 Prefab 的结构和功能
Unity UI
UI 性能常见问题:
GPU 片段着色器利用率过高(即填充率过度使用)
重建画布批次花费过多的 CPU 时间
画布批次的重建次数过多(过度 dirty)
生成顶点花费的 CPU 时间过高(通常来自文本)
{% note info %} Canvas 是一个原生代码 Unity 组件,由 Unity 的渲染系统使用,以提供将在游戏世界空间中绘制或顶部绘制的分层几何图形
Canvas 负责将其组成几何图形再组合成批处理,生成适当的渲染命令并将其发送到 Unity 的图形系统。所有这些都是在原生 C++ 代码中完成的,被称为重新批处理或批处理构建。当画布被标记为包含需要重新装包的几何图形时,画布被认为是 dirty {% endnote %}
Canvas
Canvas 负责管理 UGUI 元素,负责 UI 渲染网格的生成与更新,并向 GPU 发送 DrawCall 指令
Re-batch 过程
- 根据 UI 元素深度关系进行排序
- 检查 UI 元素的覆盖关系
- 检查 UI 元素材质并进行合批
UGUI 渲染细节
UGUI 中渲染是在 Transparent 半透明渲染队列中完成的,半透明队列的绘制顺序是从后往前画,由于 UI 元素做 Alpha Blend,我们在做 UI 时很难保障每一个像素不被重画,UI 的Overdraw 太高,这会造成片元着色器利用率过高,造成 GPU 负担
UI SpriteAtlas 图集利用率不高的情况下,大量完全透明的像素被采样也会导致像素被重绘,造成片元着色器利用率过高;同时纹理采样器浪费了大量采样在无效的像素上,导致需要采样的图集像素不能尽快的被采样,造成纹理采样器的填充率过低,同样也会带来性能问题
Re-build 过程
在
WillRenderCanvases
事件调用PerformUpdate::CanvasUpdateRegistry
接口- 通过
ICanvasElement.Rebuild
方法重新构建 Dirty Layout 组件 - 通过
ClippingRegistry.Cullf
方法,任何已注册的裁剪组件 Clipping Compnents (Such as Masks) 的对象进行裁剪剔除操作 - 任何 Dirty 的 Graphics Compnents 都会被要求重新生成图形元素
- 通过
Layout Rebuild
- UI 元素位置、大小、颜色发生变化
- 优先计算靠近 Root 节点,并根据层级深度排序
Graphic Rebuild
- 顶点数据被标记成 Dirty
- 材质或贴图数据被标记成 Dirty
图形和布局组件都依赖于 CanvasUpdateRegistry
类,该类未在 Unity Editor 的界面中公开。该类跟踪必须更新的布局组件和图形组件集,并在关联的 Canvas 调用 willRenderCanvases
事件时根据需要触发更新
使用 Canvas 的基本准则
- 将所有可能打断合批的层移到最下边的图层,尽量避免 UI 元素出现重叠区域
- 可以拆分使用多个同级或嵌套的 Canvas 来减少 Canvas 的 Rebatch 复杂度
- 拆分动态和静态对象放到不同 Canvas 下
- 不使用 Layout 组件
- Canvas 的 RenderMode 尽量 Overlay 模式,减少 Camera 调用的开销
UGUI 射线优化
- 必要的需要交互 UI 组件才开启 Raycast Target
- 开启 Raycast Targets 的 UI 组件越少,层级越浅,性能越好
- 对于复杂的控件,尽量在根节点开启 Raycast Target
- 对于嵌套的 Canvas,OverrideSorting 属性会打断射线,可以降低层级遍历的成本
UI字体
避免字体框重叠,造成合批打断
字体网格重建
- UIText 组件发生变化时
- 父级对象发生变化时
- UIText 组件或其父对象 enable/disable 时
TrueTypeFontImporter
- 支持 TTF 和 OTF 字体文件格式导入
动态字体与字体图集
- 运行时,根据 UIText 组件内容,动态生成字体图集,只会保存当前 Actived 状态的 UIText 控件中的字符
- 不同的字体库维护不同的 Texture 图集
- 字体 Size、大小写、粗体、斜体等各种风格都会保存在不同的字体图集中(有无必要,影响图集利用效率,一些利用不多的特殊字体可以采用图片代替或使用 Custom Font,Font Assets Creater 创建静态字体资源)
- 当前 Font Texture 不包含 UITex t需要显示的字体时,当前 Font Texture 需要重建
- 如果当前图集太小,系统也会尝试重建,并加入需要使用的字形,文字图集只增不减
- 利用
Font.RequestCharacterInTexture
可以有效降低启动时间
UI控件优化注意事项
- 不需要交互的 UI 元素一定要关闭 Raycast Targe t选项
- 如果是较大的背景图的 UI 元素建议也要使用 Sprite 的九宫格拉伸处理,充分减小 UI Sprite 大小,提高 UI Atlas 图集利用率
- 对于不可见的 UI 元素,一定不要使用材质的透明度控制显隐,因为那样 UI 网格依然在绘制,也不要采用 active/deactive UI 控件进行显隐,因为那样会带来 gc 和重建开销
- 使用全屏的 UI 界面时,要注意隐藏其背后的所有内容,给 GPU 休息机会
- 在使用非全屏但模态对话框时,建议使用
OnDemandRendering
接口,对渲染进行降频 - 优化裁剪 UI Shader,根据实际使用需求移除多余特性关键字
滚动视图 Scroll View 优化
- 使用 RectMask2d 组件裁剪
- 使用基于位置的对象池作为实例化缓存
Unity 中的物理解决方案
面向对象的项目内置物理引擎
- 2D 物理系统
Box2D
Erin Catto 用 C++ 编写的免费开源二维物理模拟器引擎
- 3D 物理系统
NVIDIA PhysX
是一个可扩展的多平台物理解决方案,支持各种设备,从智能手机到高端多核 CPU 和 GPU
面向数据的项目物理引擎包
- Unity Physics
Unity Physics 软件包是 Unity 面向数据技术堆栈(DOTS)的一部分,提供了一个确定性刚体动力学系统和空间查询系统
- Havok Physics for Unity
Havok Physics 提供了最快、最强大的碰撞检测和物理模拟技术,这就是为什么它已成为游戏行业的黄金标准,并被这一代游戏机一半以上的畅销游戏所使用
这个软件包将 Havok Physics 的力量带到了 Unity 的 DOTS 框架中。它建立在 Unity Physics 之上,Unity 和 Havok 为 DOTS 编写的 C# 物理引擎
Collider 组件部分优化
Trigger 与 Collider
- Trigger 对象的碰撞会被物理引擎所忽略,通过
OnTriggerEnter/Stay/Exit
函数回调 - Collider 对象由物理引擎触发碰撞,通过
OnCollisionEnter/Stay/Exit
函数回调 - Trigger 对象不需要 RigidBody 组件,Collider 对象必须至少有一个 Collider 对象有RigidBody 组件
- Trigger 对象更高效
- Trigger 对象的碰撞会被物理引擎所忽略,通过
尽量少使用 MeshCollider,可以用简单 Collider 代替,即使用多个简单 Collider 组合代替也要比复杂的 MeshCollider 来的高效
- MeshCollider 是基于三角形面的碰撞
- MeshCollider 生成的碰撞体网格占用内存也较高
- MeshCollider 即使要用也要尽量保障其是静态物体
- 可以通过 PlayerSetting 选项中勾选 Prebake Collision Meshes 选项来在构建应用时预先 Bake 出碰撞网格
RigidBody 组件部分优化
- Kinematic 与 RigidBody
- Kinematic 对象不受物理引擎中力的影响,但可以对其他 RigidBody 施加物理影响
- RigidBody 完全由物理引擎模拟来控制,场景中 RigidBody 数量越多,物理计算负载越高
- 勾选了 Kinematic 选项的 RigidBody 对象会被认为是 Kinematic 的,不会增加场景中的 RigidBody 个数
- 场景中的 RigidBody 对象越少越好
RayCast 与 Overlap 部分的优化
- Unity 物理中 RayCast 与 Overlap 都有 NoAlloc 版本的函数,在代码中调用时尽量用 NoAlloc 版本,这样可以避免不必要的 GC 开销
- 尽量调用 RayCast 与 Overlap 时要指定对象图层进行对象过滤,并且 RayCast 要还可以指定距离来减少一些太远的对象查询
- 此外如果是大量的 RayCast 操作还可以通过 RaycastCommand 的方式批量处理,充分利用 JobSystem 来分摊到多核多线程计算
{%note info%} Job System 允许您编写简单安全的多线程代码,以便您的应用程序可以使用所有可用的 CPU 内核来执行代码。这可以帮助提高应用程序的性能 {% endnote%}
动画
Animation 的一些细节
- 播放单个 AnimationClip 速度,Legacy Animation 系统更快,因为老系统是直接采样曲线并直接写入对象 Transform
Unity 的当前动画系统具有用于混合的临时缓冲区,并会对采样曲线和其他数据进行额外复制。当前系统布局已针对动画混合和更复杂设置进行优化
- 针对动画的缩放曲线比位移、旋转曲线开销更大
{%note warning%} 注意:这不适用于常量曲线(具有相同动画剪辑长度值的曲线)。常量曲线经过优化,成本低于比普通曲线。常量曲线的值与默认场景值相同时,常量曲线不会每帧都写入场景 {% endnote %}
- 大多数时间,Unity 都在估算动画,并将动画层和动画状态机的开销保持在最低水平。向 Animator 添加另一层(无论同步与否)的成本取决于层播放的动画和混合树。层的权重为零时,Unity 会跳过层更新
Animator 的一些细节
- 不要使用字符串来查询 Animator,而使用哈希来查询 Animator
- 使用曲线标记来处理动画事件
- 使用 Target Marching 函数来协助处理动画
- 将 Animator 的 CullingMode 设置成 Based On Renderers 来优化动画,并禁用 SkinMesh Renderer 的 Update When Offscreen 属性来让角色不可见时动画不更新
Playable API
Playables API 提供一种通过组织和评估树状结构(称为 PlayableGraph)中的数据源来创建工具、效果或其他游戏机制的方法。PlayableGraph 允许您混合、融合和修改多个数据源,并通过单个输出播放它们。
Playables API 支持动画、音频和脚本。Playables API 还提供通过脚本与动画系统(Mecanim)和音频系统进行交互的能力。
尽管 Playables API 目前仅限于动画、音频和脚本,但它是一种通用 API,最终可供视频和其他系统使用
Playable API 优点:
- 支持动态动画混合,可为场景中的对象提供自己的动画,并可以动态添加到 PlayableGraph 当中使用
- 允许创建播放单个动画,而并不会产生创建和管理 AnimatorController 资源所涉及的开销,可更加灵活的控制 PlayableGraph 的数据流,可以插入自定义的 AimationJob
- 可以控制动画文件加载策略,按需加载、异步加载等
- 允许用户动态创建混合图,并直接逐帧控制混合权重(甚至可以混合 AniationClip 与 AnimatorController 动画)
- 可以运行时动态创建,根据条件添加可播放节点。而不需要提前提供一套 PlayableGraph 运行时启动和禁用节点,可以做到自由度更高的 override 机制
- 可加载自定义配置数据,更加方便的和其他游戏系统整合
Playable API 缺点:
- 没有直接使用 Animator Graph 直观
- 混合模式没有现成的,需要自己实现
- 需要开发更多的配套工具
- 有一定学习成本
解决方案选择
- 一些简单、少量曲线动画可以使用 Animation 或动画区间库如 Dotween/iTween 等完成,如UI 动画,Transform 动画等。
- 角色骨骼蒙皮动画如果骨骼较少,Animation Clip 资源不多,对动画混合表现要求不高的项目可以采用 Legacy Animation。注意控制总体曲线数量
- 一些角色动画要求与逻辑有较高的交互、并且动画资源不多的项目可以直接用 Animator Graph 完成
- 一些动作游戏,对动画混合要求较高、有一些高级动画效果要求、动画资源量庞大的项目,建议采用 Animator+Playable API 扩展 Timeline 的方式完成