如何以版本控制为重点编写场景和预制件

网上有很多关于 .meta 文件和版本控制的混淆说法。.meta文件应始终 纳入版本控制。它们包含重要信息,例如连接资产间所有引用的文件 GUID。它们应与其源文件保持同步(名称和位置应始终与相关源文件一致)。切勿在 Unity 编辑器之外移动或重命名资产文件,除非为此目的开发了特定工具或完全了解 .meta 文件的功能。
默认的版本控制 .meta 文件模式是可见文件,即在操作系统中显示磁盘上的 .meta 文件,而不是隐藏它们。如果使用Perforce,请选择 Perforce 模式。
任何团队在使用 Unity 和版本控制时,首先要做的就是设置智能合并。默认情况下,Unity 将YAML文件存储为文本文件,这样就可以在版本控制中进行合并。将 "资产序列化模式"更改为二进制,将取消通过版本控制合并这些文件的功能。
由于 Unity 的 YAML 文件是基于文本的,因此很多版本控制合并软件都会尝试使用文本或编码规则来合并它们。Smart Merge 由 Unity 构建,合并时会考虑 YAML 结构。我们建议将 Smart Merge 作为所有 YAML 文件的默认合并工具。
智能合并将大大减少因合并冲突而造成的工作损失,但如果你的团队对潜在工作损失的容忍度为零,我们建议你也使用文件锁定,并对任何 YAML 文件执行手动合并冲突解决方案。


场景是任何 Unity 应用程序的宏构件。每个场景都被序列化(保存)到一个文件中,这意味着它们可以用来以有利于源控制和同步编辑的方式组织内容。添加式场景加载通常可有效用于创建超大型场景、流媒体内容,甚至是由多个独立组件组成的非常简单的场景。
一般来说,我们建议在场景中存储与场景相关的数据。例如,就烘焙照明而言,灯光、光贴图和环境设置都取决于它们所处的场景。几乎所有其他东西都可以存储在预制件中。如果一个场景很小,而且其中的特定内容只能在该场景中编辑,那么将所有数据存储在一个场景中可能是合理的。不过,需要注意的是,如果一个场景中有两个 GameObject,对其中一个对象的任何更改都会导致场景文件的更改。
虽然智能合并功能可以处理两个内容创建者同时更改场景中两个不同对象的情况,但更复杂的更改可能会导致无法解决的冲突。我们建议使用预制件来帮助缓解这一问题。
在明确的场景结构方面,Unity Netcode 演示包含了一个简单项目的标准场景布局流程图,与我们在许多场景中看到的类似。
预制件可用于构建模块化的独立内容。如需了解更多信息,请查看预制件和嵌套预制件教程。该教程中最重要的部分是 "最佳实践"部分。没有任何关于使用预制件构建内容的明确规定。每个团队都必须根据自己的项目决定最适合自己的方式。不过,还是有一些好的指导原则可以遵循:
- 将预制构件视为房屋或项目的积木。一般来说,预制构件的根部代表房屋的地基。在这些组件中,每个可重复使用的组件都有预制件,这些预制件构成了房屋的其余部分。它们可以像窗台一样细小,也可以像有窗户的墙壁一样宽阔。所需的粒度取决于内容创建者希望如何编辑预制件。
- 预制件可以嵌套。也就是说,在上面的例子中,可能有一个房屋预制件,墙壁、屋顶、窗户和门等预制件组成了房屋。嵌套 Prefabs 需要注意的一点是,层次结构越深,项目越有可能遇到性能问题。我们通常建议将预制构件层次结构的深度保持在 5-7 层以下。
- 在嵌套预制件时,通常最好在预制件模式下编辑预制件。这样可以保证预制件属性或重载设置在正确的位置。在场景视图中编辑 Prefab 属性可能会导致覆盖驻留在错误的 Prefab 中或场景本身中。这可能会产生意想不到的后果,造成合并冲突。有时,有必要覆盖父 Prefab 中的子 Prefab 属性(通过这种方式可以实现变化,而不会影响对特定 Prefab 的每次引用)。这是一个标准的工作流程,但必须注意在适当的预制件或场景中进行更改和应用覆盖。
- 当加载和实例化 Prefab 时,Prefab 中的所有内容都将在运行时加载和实例化到内存中。这意味着,如果预制件中包含视觉效果或并非始终存在的附加对象,这些对象将被实例化到内存中。这会导致内存膨胀,因为每个实例化的 Prefab 都会实例化其中的所有内容。如果对象偶尔会附加视觉效果或模型,那么最好有一个池系统和一个附件管理器,在运行时添加这些组件。水池系统中的任何东西一般都不能放在预制房屋内。
- 一切都不必是预制构件。较小的构件可以是预制件甚至场景中的 GameObjects。如果某个对象是场景或 Prefab 所独有的,则无需为其创建 Prefab。
- 应谨慎使用预制变体。一般来说,当一个对象的核心构件完全相同,只有简单的差异时,预制变体的使用效果最好。例如,对于功能相同但视觉效果不同的游戏组件,使用 Prefab 变体可能会有所帮助。在这种情况下,更改核心功能将同时影响 Prefab 及其变体的功能,但视觉效果仍将被覆盖。制作过于复杂的 Prefab 变体时要小心,因为对根 Prefab 的更改可能会对被覆盖的 Prefab 产生意想不到的后果。一般来说,除非系统非常简单,否则像角色变异系统或任何其他复杂的可视化皮肤系统都不应基于预制变体。
我们建议创建一个统一的策略,以确定哪些应该是预制件,哪些应该是预制件或场景中的游戏对象。

有两种工作流程可供选择,它们允许在 FBX 模型和预制构件之间建立耦合度较低的结构:
将 FBX 文件直接添加到场景中,然后将组件或结构修改添加到场景中的游戏对象上。在这种情况下,任何更改都会存储在场景文件中。如果很多人在同一场景中使用此工作流程,这可能会导致合并冲突。我们只建议在必须在场景中进行更改且不存在冲突的情况下使用此工作流程。
从爆炸模型中创建标准的 Unity 预制件。在这种方法中,FBX 模型被拖入场景,然后完全解压缩。然后用它来创建一个预制件。这种方法将 FBX 文件与预制件完全分离。如果希望在 FBX 文件和预制件之间实现非常松散的耦合,这将非常有用。它将不再继承对原始 FBX 文件所做的任何结构更改。这种耦合只存在于网格、材料和动画的名称之间。其他所有内容都将保留在 Prefab 文件中。这对于创建完全独特的 FBX 模型变体非常有用。例如,如果两个角色使用相同的模型,但需要完全不同的网格、材质,甚至不同的层次结构,那么这种方法可能比创建多个 FBX 文件更好,因为后者会占用额外的内存和磁盘空间。当需要极其复杂的组件设置时,这将非常方便。与其让 GameObject 消失并丢失所有组件,不如让该对象继续存在,并将组件转移到新对象中,或者将重命名的网格或材质重新插入现在引用丢失的网格或材质的 GameObject 中。
在下面两张图片中,对 FBX 文件进行了修改,然后重新导入。球上团结徽标周围的圆圈被删除。主支架更名。主球的材料从黑色变为绿色,在支架徽标上方引入了一个新的母球,上面的变体使其高于模型。

这一切在 FBX 文件和模型预制件中都是紧密配合的。在非模型预制构件中,保留了原有的层次结构、名称和材料。删除的网格现在是一个丢失的网格,但 GameObject 仍然存在。重命名后的网格也不可见,因为它引用的网格名称在模型中已经不存在了。更改后的素材不会更新,因为 GameObject 仍然引用原始素材。此外,由于网格的父网格没有发生变化,因此不会尊重层次结构的变化,网格也不会改变位置。

在下面的前后图片中,场景层次结构中显示了上述更改的结果。场景中直接引用的 FBX 文件和标准模型预制件会响应对原始 FBX 文件所做的任何更改。未打包的预制件保留其原始层次结构,不会对删除或名称更改做出反应。

在特定情况下,必须谨慎决定使用哪种方法。如果希望在编辑器和 FBX 文件之间实现紧密耦合,那么标准模型 Prefab 可能是最佳选择。如果需要非常松散的耦合(例如一个非常灵活的角色系统,网格或材料可能会经常更换),那么创建非模型 Prefabs 并对 FBX 文件的组件进行软引用效果会更好。

我们经常看到团队将源内容存储在不同的位置,从网络驱动器到本地计算机。我们建议将所有重要的源内容以某种方式纳入版本控制。
我们最常见的方法是将源内容放到版本控制中资产文件夹之外的文件夹中。这样可以确保 Unity 不会直接从源文件中导入任何内容。如果Maya、3ds Max、Blender 和 Photoshop文件放在 assets 文件夹中的任何位置,它们会自动导入到模型和纹理中。虽然 Unity 支持这种做法,但我们并不推荐。此外,我们建议将源目录镜像到资产目录中的内容,这样可以相对容易地跟踪资产。
源内容对用户来说应该是可屏蔽的,因为大多数用户并不需要全部内容,而且源内容在磁盘上可能非常大(TB 级)。在某些版本控制软件中,创建内容掩码相当简单。在 Unity 版本控制中,这是通过隐身文件实现的。在 Perforce 中,视图用于屏蔽客户端内容。然而,Git 的设计并非如此。因此,我们建议为源内容创建一个单独的 Git 仓库,或为每种类型的内容创建一个单独的仓库(例如,3D 艺术家可能永远不需要同步完整的音频源,反之亦然)。
创建可与版本控制配合使用的内容并支持多个用户在同一区域工作是一项艰巨的任务。不过,Unity 提供了构建模块,只要经过深思熟虑和规划,就可以创建大型内容,而不会导致无法解决的合并冲突或工作丢失。