扩展 Unity 工作流程:来自中大型项目的经验教训
Matthew Wojtechko - Mega Cat Studios
Lead Game Developer
本篇博文是 Mega Cat Studios 系列文章的第一篇,他们将在文中分享其在 Unity 方面的专业知识,以及针对现实世界商业游戏开发挑战的解决方案。查看本系列中涵盖 Input System、关卡设置和环境设计的其他文章:
- 百发百中:Unity 的事件驱动型 Input System 如何为 Backyard Baseball 2026 中的控制提供支持
- 如何通过关卡设置、世界构建和 VFX 为新一代重塑经典体育游戏
- 缩放渲染:针对海量对象数量的高效策略
我们希望你能学到一些很棒的技巧!
你有一个很棒的想法,代码编写的速度和你打字的速度一样快。随着每一次提交,一项新功能便初具形状。但正是你构思想法的速度,可能会让你很快面对一团庞大且充满 Bug 的乱麻。
在 Mega Cat Studios,我们所有的项目都是怀揣着热情开始的,因此我们理解那种追求速度、不拘小节并尽快完成任务的诱惑。对于构建原型来说,这种方法没问题,事实上,我们还很推荐这样做!一位明智的开发人员知道何时该优先考虑迭代速度,何时该优先考虑稳定性。因为当你离开构建原型阶段时,“追求速度、不拘小节”的方法就会成为一种负担。
我们在 Mega Cat Studios 多次经历过这种过渡,并且在每一个项目中,我们都能学到新东西。我们想分享一些经验教训,这些经验帮助我们为最近的项目 Backyard Baseball 做好发布准备。
经验 1:为缩放构建结构

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

在试玩广告角色、游戏模式和生动的环境之间,Backyard Baseball 的庞大规模使得良好的架构成为必需。
第 2 课:与 Unity 协作,而非对抗

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

程序集定义文件包含指定系统的逻辑,并明确指定依赖项。如图所示,在 Mega Cat Studios,我们使用许多小型程序集来保持模块化和条理清晰。只需小心不要引入“循环依赖项”。
Assembly Definitions (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 这样的游戏中,视觉标识至关重要,随着我们在发布前将外观和感觉调整得恰到好处,模型和 VFX 会收到数百次微调。
“再多的技术规范也无法避免这样一个事实,即拥有内容多样性意味着要处理不同素材资源之间细微但重大的差异,”Nico 说。
自动化在这里也有帮助:
- AssetPostprocessor:我们编写自定义导入逻辑以强制执行项目标准。
- OnValidate:我们使用 OnValidate 方法在编辑器中报告缺失的引用,该方法总是在构建之前触发。
最后,不要让所有这些关于自动化的讨论分散您对简单、手动修复的注意力,因为它们往往更快。
“永远不要花 10 天时间去自动化一个手动只需 10 分钟的任务,”David 告诫道。
第 6 课:掌握人为因素(协作)

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

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