文章

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

MATTHEW WOJTECHKO / MEGA CAT STUDIOSLead Game Developer
Mar 31, 2026
Backyard Baseball,由 Mega Cat Studios 和 Playground Productions 制作
为方便起见,此网页已进行机器翻译。我们无法保证翻译内容的准确性或可靠性。如果您对翻译内容的准确性有疑问,请参阅此网页的官方英文版本。

本篇博文是 Mega Cat Studios 系列文章的第一篇,他们将在文中分享其 Unity 专业知识以及针对现实世界商业游戏开发挑战的解决方案。查看本系列中涵盖输入、关卡设置和环境设计的其他文章:

我们希望你能学到一些很棒的技巧!

你有一个很棒的想法,代码随着你的敲击速度飞快地生成。随着每一次提交,一个新功能便初具形状。但正是你想法形成的速度,可能会让你很快面对一团巨大的、充满 Bug 的混乱代码。

在 Mega Cat Studios,我们以满腔热情开启所有项目,因此我们理解那种追求速度、不拘小节并尽快完成任务的诱惑。对于构建原型而言,这种方法没问题,事实上,我们还很推荐这样做!一位明智的 개발자 知道何时该优先考虑迭代速度,何时该优先考虑稳定性。因为当你离开构建原型阶段时,那种“追求速度、不拘小节”的方法就会成为一种负担。

我们在 Mega Cat Studios 多次经历了这种过渡,并且在每个项目中,我们都能学到新东西。我们想分享一些我们在为最近的项目 Backyard Baseball 准备发布时学到的经验教训。

经验教训 1:为缩放构建结构

场景的预制件层级视图展示了其组织方式,即划分为定义明确的组和父级预制件,从而促进了轻松的导航和高效的修改。
场景的预制件层级视图展示了其组织方式,即划分为定义明确的组和父级预制件,从而促进了轻松的导航和高效的修改。

缩放问题很少是由糟糕的代码引起的;相反,它们更多是由于缺乏规划的架构而产生的。如果开发자或美术人员无法在10秒内找到资产,工作流程就需要改变。为项目设置缩放的一些技巧包括:

  • 按类型和用途组织:我们按类型分组,然后按用途分组。类型包括美术、代码和音频等类别。用途是指它们被用于什么地方。直观的组织结构减少了入门阻力;新来的美术人员应该无需询问就能确切知道“角色空闲”精灵属于哪里。
  • 保持场景简单:我们摒弃了“大型场景”,转而选择较小的场景,例如包含保存数据和关键系统的主场景,以及标题画面和棒球场场景,这些场景会根据玩家是否在匹配中进行附加加载。同样,我们使用预制件来实现独立的功能,这样更改就不太可能序列化到包含它们的场景文件中。这使得美术人员可以在处理环境的同时,设计师可以在同一个“关卡”中调整游戏玩法,而不会产生文件冲突(稍后会详细介绍)。
  • 设置 Addressables 系统:我们不使用传统的 Resources 文件夹,而是使用 Addressables 系统 来仅在需要时加载素材资源,从而保持较低的内存占用。此外,使用 Addressable 键加载资产比通过文件路径加载到 Resources 文件夹中的指定位置更清晰,且不易出错。

困难的部分不在于理解这些最佳实践。而在于从一开始就坚持执行,并在多年后依然保持这种纪律。

了解你处于开发的哪个阶段,以及在该阶段优先考虑什么。

“在原型设计阶段,由于几乎没有代码,先实现功能再考虑模块化是可以的,”Backyard Baseball 的开发자 Paolo Roxas 说道。“我们想在使项目变得复杂之前,先看看它的实际表现如何。”

在试玩广告角色、游戏模式和生动的环境之间,Backyard Baseball 的庞大规模使得良好的架构成为必要。
在试玩广告角色、游戏模式和生动的环境之间,Backyard Baseball 的庞大规模使得良好的架构成为必要。

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

小型、专注的考量和行动组合在一起,形成了复杂的现场决策。没有“上帝”脚本,只有协同工作的可组合构建块。
小型、专注的考量和行动组合在一起,形成了复杂的现场决策。没有“上帝”脚本,只有协同工作的可组合构建块。

“没有什么比创建专注、简洁且自包含的构建块(无论是组件、ScriptableObjects 还是自定义类)更强大的了,”Mega Cat Studios 工程主管 David Chávez Armenteros 说道。Unity 的核心在于拥抱模块化。为了保持灵活性,我们遵循三个经典原则:

1.单一职责:每个脚本或类都应具有一个定义明确的角色。

2.松耦合:系统应通过接口或事件而非直接引用进行交互,且仅在必要时进行。我们建议在代码中实现系统之前,先绘制出系统间的依赖关系图,以避免循环或混乱的架构。

3.即插即用:组合小型、专注的单位来创建复杂的行为,而不是编写庞大的“上帝”脚本。

第 3 课:利用限制来获得自由

程序集定义文件包含指定系统的逻辑,并明确指定依赖关系。如图所示,在 Mega Cat Studios,我们使用许多小型程序集来保持模块化和条理清晰。只需注意不要引入“循环依赖”。
程序集定义文件包含指定系统的逻辑,并明确指定依赖关系。如图所示,在 Mega Cat Studios,我们使用许多小型程序集来保持模块化和条理清晰。只需注意不要引入“循环依赖”。

程序集定义 (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 课:掌握人为因素(协作)

在 Mega Cat Studios,开发人员使用 Version Control 和简单的指导方针跨部门协作,以避免在开发 游戏 时产生冲突。
在 Mega Cat Studios,开发人员使用 Version Control 和简单的指导方针跨部门协作,以避免在开发 游戏 时产生冲突。

Version Control 系统(如 Git)是每天协调数十名开发人员的数百次更改时首先想到的工具之一。David 为我们在 Mega Cat 的所有 项目 提倡这些久经考验的方法:

  • 小型、原子化的更改:避免一次性触及多个系统的“大型提交”。将工作隔离在单独的功能分支中,直到它们稳定并经过审查。将单独的更改保留在单独的提交中,以获得记录良好的版本历史,这在需要时也使 cherry picking 和其他 Git 魔术变得更容易。
  • 每日从主分支合并:使功能分支、部门分支和长期存在的分支与主分支保持同步,因为这可以减少最终合并的 大小 和复杂性,有助于防止大规模冲突。
  • 审查合并请求:这是 品质 保证的第一道防线,您可以在这里捕获错误、执行 项目 标准,并确保与整个系统的一致性。

“在大型 Unity 项目中,代码审查不仅仅是一种形式,”David 建议道。“它们是防止冲突和确保整体 项目 品质 的关键部分。”

只需确保那些审查 代码 的人具备所实现领域的专业知识,并了解 代码 最佳实践,这样他们才能准确评估正确性和可维护性。

有一些 指定的 技巧和窍门,可以使 Version Control 在 Unity 中尽可能顺畅。场景和预制件构成了您项目的基础,因此不仅要针对CPU性能进行优化,还要针对开发者的协作进行优化。

我们总是倾向于使用更小、可添加和嵌套的组件,而不是大型场景或单一的预制件。这样,开发者就可以并行工作而不会产生冲突。

这一点很重要,因为场景和预制件中的合并冲突最难解决,因为它们的数据不容易被开发者阅读。为了简化此过程,我们将这些文件序列化为文本而非二进制,并为我们的Git配置启用YAML文件的自动合并功能。这使得Git更有可能自行解决合并冲突,并为开发者节省时间,用于开发新功能的重要工作。

但尽管如此:

“预防冲突通常比试图解决冲突是更好的策略,”Nico说。

明确的资产所有权对此大有帮助。

“定义谁可以修改指定的场景或预制件,”David说。“然后团队成员在请求变更其所有权之外的内容,而不是直接编辑素材资源。”

Nico将类似的程序描述为“信号量系统”。这基本上是一个电子表格,开发者在其中记录他们何时更改资产,从而有效地“锁定”它。如果另一位开发者需要对该文件进行更改,他们需要等到锁定该文件的开发者将其更改推送到仓库并“解锁”它。

一如既往,找到最适合您团队的程序。

为未来构建

标志性的Pablo Sanchez和Mr. Clanky都在这里,准备好打球了。
标志性的Pablo Sanchez和Mr. Clanky都在这里,准备好打球了。

在Mega Cat Studios,我们了解到扩展Unity项目与其说是“更努力地编码”,不如说是关于架构纪律。通过尊重Unity基于组件的特性,利用程序集定义强制执行边界,并考虑到未来需求来组织素材资源,我们在不让项目在发布前陷入技术债务的情况下,保持了构建原型阶段的大部分广告创意素材流程。

虽然这些经验很重要,但请记住,没有完美的代码库。软件开发是一场史诗般的战斗,推荐的编程模式与实际考量每天都在发生冲突。如果遵循其中一项原则导致开发停滞,却未能带来有益的权衡,这表明你需要更关注自己团队的具体情况,而不是教科书式的建议。毕竟,每个项目、团队和个人都是不同的。

我们在 Mega Cat Studios 每天都在努力寻求这种平衡。随着每一个新项目的开展,以及我们的游戏库不断壮大,我们希望成为更好的 Unity 开发者和更好的合作者。