扩展 Unity 工作流:中大型项目的经验教训

本篇博文是 Mega Cat Studios 系列文章的第一篇,他们将在文中分享其 Unity 专业知识以及针对现实世界商业游戏开发挑战的解决方案。查看本系列中涵盖输入、关卡设置和环境设计的其他文章:
我们希望你能学到一些很棒的技巧!
你有一个很棒的想法,代码随着你的敲击速度飞快地生成。随着每一次提交,一个新功能便初具形状。但正是你想法形成的速度,可能会让你很快面对一团巨大的、充满 Bug 的混乱代码。
在 Mega Cat Studios,我们以满腔热情开启所有项目,因此我们理解那种追求速度、不拘小节并尽快完成任务的诱惑。对于构建原型而言,这种方法没问题,事实上,我们还很推荐这样做!一位明智的 개발자 知道何时该优先考虑迭代速度,何时该优先考虑稳定性。因为当你离开构建原型阶段时,那种“追求速度、不拘小节”的方法就会成为一种负担。
我们在 Mega Cat Studios 多次经历了这种过渡,并且在每个项目中,我们都能学到新东西。我们想分享一些我们在为最近的项目 Backyard Baseball 准备发布时学到的经验教训。
经验教训 1:为缩放构建结构

缩放问题很少是由糟糕的代码引起的;相反,它们更多是由于缺乏规划的架构而产生的。如果开发자或美术人员无法在10秒内找到资产,工作流程就需要改变。为项目设置缩放的一些技巧包括:
- 按类型和用途组织:我们按类型分组,然后按用途分组。类型包括美术、代码和音频等类别。用途是指它们被用于什么地方。直观的组织结构减少了入门阻力;新来的美术人员应该无需询问就能确切知道“角色空闲”精灵属于哪里。
- 保持场景简单:我们摒弃了“大型场景”,转而选择较小的场景,例如包含保存数据和关键系统的主场景,以及标题画面和棒球场场景,这些场景会根据玩家是否在匹配中进行附加加载。同样,我们使用预制件来实现独立的功能,这样更改就不太可能序列化到包含它们的场景文件中。这使得美术人员可以在处理环境的同时,设计师可以在同一个“关卡”中调整游戏玩法,而不会产生文件冲突(稍后会详细介绍)。
- 设置 Addressables 系统:我们不使用传统的 Resources 文件夹,而是使用 Addressables 系统 来仅在需要时加载素材资源,从而保持较低的内存占用。此外,使用 Addressable 键加载资产比通过文件路径加载到 Resources 文件夹中的指定位置更清晰,且不易出错。
困难的部分不在于理解这些最佳实践。而在于从一开始就坚持执行,并在多年后依然保持这种纪律。
了解你处于开发的哪个阶段,以及在该阶段优先考虑什么。
“在原型设计阶段,由于几乎没有代码,先实现功能再考虑模块化是可以的,”Backyard Baseball 的开发자 Paolo Roxas 说道。“我们想在使项目变得复杂之前,先看看它的实际表现如何。”

第 2 课:与 Unity 协作,而非对抗它

“没有什么比创建专注、简洁且自包含的构建块(无论是组件、ScriptableObjects 还是自定义类)更强大的了,”Mega Cat Studios 工程主管 David Chávez Armenteros 说道。Unity 的核心在于拥抱模块化。为了保持灵活性,我们遵循三个经典原则:
1.单一职责:每个脚本或类都应具有一个定义明确的角色。
2.松耦合:系统应通过接口或事件而非直接引用进行交互,且仅在必要时进行。我们建议在代码中实现系统之前,先绘制出系统间的依赖关系图,以避免循环或混乱的架构。
3.即插即用:组合小型、专注的单位来创建复杂的行为,而不是编写庞大的“上帝”脚本。
第 3 课:利用限制来获得自由

程序集定义 (AsmDefs) 是用于对代码进行分组的 C# 结构。它们宣传的优势是缩短编译时间,但它们真正的杀手锏是强制实现模块化。
Nico Gaudenzi,首席开发者兼意大利面条式代码的痛恨者,对它们推崇备至。
“在 Backyard Baseball 中,Input 层和游戏玩法层位于不同的 DLL 中。游戏玩法完全不依赖于 Input 细节。”
这使得每个依赖项都成为经过深思熟虑的决策,从而避免了工程师们陷入困境。如果真的有必要,我们可以重写整个 Input System——从手柄处理到 Netcode——而不会冒着破坏玩家物理或 AI 行为的风险。更可能的情况是,它让一名工程师可以在代码库的单个领域内工作,而不会意外地将更改级联到另一个系统,并减少了开发人员在实现功能和修复错误时需要理清的代码量。
第 4 课:测试要更聪明,而不是更辛苦
随着项目规模的扩大,“多米诺效应”可能会接管一切:这里的一个小改动可能会导致那里的功能损坏。良好的架构大有裨益,但它并非万能药。
在功能变更到达我们受人尊敬的品质保证团队进行手动测试之前,游戏会通过一系列自动化单元测试进行严格分析。
“测试就像需求列表一样,”Nico 说道。“它们描述了预期的结果并提供了关键的使用案例。”
当 Backyard Baseball 中的角色投出快球、盗垒或击出本垒打时,我们希望实现特定的游戏玩法结果,例如确保球以符合投球的真实速度飞行,跑垒员的时机与盗垒机制保持一致,或者防守队员对击球做出正确的反应。角色需要与地面正确碰撞,球需要以正确的速度移动,并且更细粒度的系统(例如跟踪挥臂、挥棒或在垒间冲刺等动作的玩家控制器标志)也需要正常工作。
当我们对 Pablo Sanchez 的力量进行修改,或者更关键的是,调整管理挥棒的共享代码时,我们需要确保每一次交互,从接触时机到球的轨迹,在整个游戏中表现一致。
通常,损坏的东西往往是你意想不到的,这正是测试如此重要的全部原因。
随着该系统集成到我们的工作流程中,我们可以在特定需求损坏的瞬间察觉到,这减少了测试和故障排除的会话次数,这些工作就像大海捞针一样耗时。
Unity 的 Test Runner 在系统模块化时工作效率最高,这也是我们使用程序集定义 (Assembly Definitions) 的另一个原因。
第 5 课:让您的素材资源准备就绪

“人非圣贤,孰能无过;宽恕他人,乃是神性。”
但从一开始就建立系统来防止人为报错,这绝对是传奇之举。
经过数小时的编码和调试,难免会有眼花缭乱的开发人员出现几次误点,或者不小心将原本只是临时的更改推送到仓库中。虽然可以原谅(我作为偶尔眼花缭乱的人之一这样说),但导入设置配置不当的资产可能会产生巨大的后果。由于许多开发人员在性能强大的机器上工作,最糟糕的情况是性能问题直到后期才被发现,例如当一个包含角色、动画和特效的体育场场景在低端硬件上开始出现卡顿或不稳定时。
为了降低这种风险,您可以限制谁有权访问资产,但这会导致瓶颈,因为游戏内容需要调整的原因有很多:
- 模型太大,无法与摄像机设置相匹配。
- 此音频剪辑的音量较小,因此需要一个指定的特效。
- 着色器更改后,每个纹理都需要进行调整。
在像 Backyard Baseball 这样的游戏中,视觉标识至关重要,随着我们在发布前不断完善外观和感觉,模型和视觉特效会经历数百次调整。
“无论有多少技术规范,都无法回避这样一个事实:内容多样性意味着必须处理不同资产之间细微但显著的差异,”Nico 说道。
自动化在这里也很有帮助:
- AssetPostprocessor:我们编写了强制执行项目标准的自定义导入逻辑。
- OnValidate:我们使用 OnValidate 方法在 编辑器 中报告缺失的 引用,该方法总是在 构建 之前触发。
最后,不要让所有这些关于自动化的讨论分散您对简单、手动修复的注意力,因为它们有时更快。
“永远不要花 10 天时间去自动化一个手动只需 10 分钟就能完成的任务,”David 告诫道。
第 6 课:掌握人为因素(协作)

Version Control 系统(如 Git)是每天协调数十名开发人员的数百次更改时首先想到的工具之一。David 为我们在 Mega Cat 的所有 项目 提倡这些久经考验的方法:
- 小型、原子化的更改:避免一次性触及多个系统的“大型提交”。将工作隔离在单独的功能分支中,直到它们稳定并经过审查。将单独的更改保留在单独的提交中,以获得记录良好的版本历史,这在需要时也使 cherry picking 和其他 Git 魔术变得更容易。
- 每日从主分支合并:使功能分支、部门分支和长期存在的分支与主分支保持同步,因为这可以减少最终合并的 大小 和复杂性,有助于防止大规模冲突。
- 审查合并请求:这是 品质 保证的第一道防线,您可以在这里捕获错误、执行 项目 标准,并确保与整个系统的一致性。
“在大型 Unity 项目中,代码审查不仅仅是一种形式,”David 建议道。“它们是防止冲突和确保整体 项目 品质 的关键部分。”
只需确保那些审查 代码 的人具备所实现领域的专业知识,并了解 代码 最佳实践,这样他们才能准确评估正确性和可维护性。
有一些 指定的 技巧和窍门,可以使 Version Control 在 Unity 中尽可能顺畅。场景和预制件构成了您项目的基础,因此不仅要针对CPU性能进行优化,还要针对开发者的协作进行优化。
我们总是倾向于使用更小、可添加和嵌套的组件,而不是大型场景或单一的预制件。这样,开发者就可以并行工作而不会产生冲突。
这一点很重要,因为场景和预制件中的合并冲突最难解决,因为它们的数据不容易被开发者阅读。为了简化此过程,我们将这些文件序列化为文本而非二进制,并为我们的Git配置启用YAML文件的自动合并功能。这使得Git更有可能自行解决合并冲突,并为开发者节省时间,用于开发新功能的重要工作。
但尽管如此:
“预防冲突通常比试图解决冲突是更好的策略,”Nico说。
明确的资产所有权对此大有帮助。
“定义谁可以修改指定的场景或预制件,”David说。“然后团队成员在请求变更其所有权之外的内容,而不是直接编辑素材资源。”
Nico将类似的程序描述为“信号量系统”。这基本上是一个电子表格,开发者在其中记录他们何时更改资产,从而有效地“锁定”它。如果另一位开发者需要对该文件进行更改,他们需要等到锁定该文件的开发者将其更改推送到仓库并“解锁”它。
一如既往,找到最适合您团队的程序。
为未来构建

在Mega Cat Studios,我们了解到扩展Unity项目与其说是“更努力地编码”,不如说是关于架构纪律。通过尊重Unity基于组件的特性,利用程序集定义强制执行边界,并考虑到未来需求来组织素材资源,我们在不让项目在发布前陷入技术债务的情况下,保持了构建原型阶段的大部分广告创意素材流程。
虽然这些经验很重要,但请记住,没有完美的代码库。软件开发是一场史诗般的战斗,推荐的编程模式与实际考量每天都在发生冲突。如果遵循其中一项原则导致开发停滞,却未能带来有益的权衡,这表明你需要更关注自己团队的具体情况,而不是教科书式的建议。毕竟,每个项目、团队和个人都是不同的。
我们在 Mega Cat Studios 每天都在努力寻求这种平衡。随着每一个新项目的开展,以及我们的游戏库不断壮大,我们希望成为更好的 Unity 开发者和更好的合作者。
