使用 ScriptableObjects 实现更好的场景工作流程

在 Unity 中管理多个场景可能是一个挑战,改进此工作流程对于游戏性能和团队生产力都至关重要。在这里,我们分享一些关于如何设置场景工作流程以适应更大的项目的技巧。
大多数游戏都涉及多个级别,并且级别通常包含多个场景。在场景相对较小的游戏中,您可以使用预制件将它们分成不同的部分。但是,要在游戏过程中启用或实例化它们,您需要引用所有这些预制件。这意味着随着游戏变得越来越大,并且这些引用在内存中占用更多空间,使用场景会变得更加高效。
您可以将级别分解为一个或多个 Unity 场景。找到管理所有这些的最佳方法成为关键。您可以在编辑器中打开多个场景,并在运行时使用 多场景编辑。将级别分成多个场景还有一个好处,就是可以使团队合作更容易,因为它可以避免 Git、SVN、Unity Collaborate 等协作工具中的合并冲突。
在下面的视频中,我们展示了如何通过将游戏逻辑和关卡的不同部分分解为几个不同的 Unity 场景来更有效地加载关卡。然后,在加载这些场景时使用 附加场景加载模式 ,我们会与游戏逻辑一起加载和卸载所需的部分,这是持久的。我们使用预制件作为场景的“锚点”,这在团队工作时也提供了很大的灵活性,因为每个场景都代表关卡的一部分,并且可以单独编辑。
您仍然可以在编辑模式下加载这些场景并随时按“播放”,以便在创建关卡设计时将它们全部可视化。
我们展示了两种不同的方法来加载这些场景。第一个是基于距离的,非常适合非室内关卡,比如开放世界。这种技术对于一些视觉效果(例如雾)也很有用,可以隐藏加载和卸载过程。
第二种技术使用 触发器 来检查要加载哪些场景,这在处理内部时效率更高。
现在一切都在级别内部进行管理,您可以在其顶部添加一个层以更好地管理级别。
我们希望在整个游戏过程中跟踪每个级别以及所有级别的不同场景。一种可能的方法是,在 MonoBehaviour 脚本中使用静态变量和单例模式,但这种解决方案存在一些问题。使用单例模式可以使系统之间建立刚性连接,因此它不是严格模块化的。这些系统不能独立存在并且总是相互依赖。
另一个问题涉及静态变量的使用。由于您无法在 Inspector 中看到它们,因此您需要更改代码来设置它们,这使得艺术家或关卡设计师更难轻松测试游戏。当您需要在不同的场景之间共享数据时,您可以使用静态变量与DontDestroyOnLoad结合使用,但只要可能,应避免使用后者。
要存储有关不同场景的信息,您可以使用 ScriptableObject,它是一个主要用于存储数据的可序列化类。与用作附加到 GameObject 的组件的 MonoBehaviour 脚本不同,ScriptableObjects 不附加到任何 GameObject,因此可以在整个项目的不同场景之间共享。
您希望能够将此结构用于关卡,也用于游戏中的菜单场景。为此,创建一个 GameScene 类,其中包含级别和菜单之间的不同公共属性。
未知的块类型“codeBlock”,请在“serializers.types”属性中为其指定一个序列化器
请注意,该类继承自 ScriptableObject 而不是 MonoBehaviour。您可以根据您的游戏需要添加任意数量的属性。完成此步骤后,您可以创建 Level 和 Menu 类,它们均继承自刚刚创建的 GameScene 类 - 因此它们也是 ScriptableObjects。
未知的块类型“codeBlock”,请在“serializers.types”属性中为其指定一个序列化器
在顶部添加 CreateAssetMenu 属性可让您从 Unity 中的 Assets 菜单创建一个新的级别。您可以对 Menu 类执行相同的操作。您还可以包含一个枚举,以便能够从检查器中选择菜单类型。
未知的块类型“codeBlock”,请在“serializers.types”属性中为其指定一个序列化器
现在您可以创建级别和菜单,让我们添加一个列出级别和菜单的数据库以便于参考。您还可以添加索引来跟踪玩家的当前级别。然后,您可以添加方法来加载新游戏(在这种情况下将加载第一个级别)、重播当前级别以及进入下一个级别。请注意,这三种方法之间只有索引会发生变化,因此您可以创建一种使用索引加载级别的方法,以便多次使用它。
未知的块类型“codeBlock”,请在“serializers.types”属性中为其指定一个序列化器
菜单也有方法,您可以使用之前创建的枚举类型来加载所需的特定菜单 - 只需确保枚举中的顺序和菜单列表中的顺序相同。
现在,您最终可以通过在项目窗口中单击鼠标右键,从“Assets”菜单中创建级别、菜单或数据库 ScriptableObject。

从那里,只需继续添加您需要的级别和菜单,调整设置,然后将它们添加到场景数据库中。下面的示例向您展示了 Level1、MainMenu 和 Scenes 数据的样子。

现在是时候调用这些方法了。在此示例中,当玩家到达关卡末尾时,用户界面 (UI) 上出现的“下一关”按钮会调用 NextLevel 方法。要将方法附加到按钮,请单击按钮组件的 On Click 事件的加号按钮以添加新事件,然后将 Scenes Data ScriptableObject 拖放到对象字段中并从 ScenesData 中选择 NextLevel 方法,如下所示。

现在,您可以对其他按钮执行相同的过程 - 重播关卡或转到主菜单等等。您还可以从任何其他脚本引用 ScriptableObject 来访问不同的属性,例如背景音乐的 AudioClip 或后期处理配置文件,并在级别中使用它们。
- 尽量减少装载/卸载
在视频中展示的ScenePartLoader脚本中,可以看到玩家可以多次进入和离开碰撞器,从而触发场景的重复加载和卸载。为了避免这种情况,可以在脚本中调用场景的加载和卸载方法之前添加协程,并在玩家离开触发器时停止协程。
- 命名惯例
另一个通用技巧是在项目中使用固定的命名约定。团队应该事先就如何命名不同类型的资产达成一致——从脚本和场景到材料和项目中的其他东西。这不仅使您更容易,而且也使您的队友更容易地开展和维护项目。这始终是一个好主意,但在这种特殊情况下,使用 ScriptableObjects 进行场景管理至关重要。我们的示例使用了基于场景名称的直接方法,但有许多不同的解决方案较少依赖场景名称。您应该避免使用基于字符串的方法,因为如果您在给定上下文中重命名 Unity 场景,则游戏的另一部分将不会加载该场景。
- 定制工具
避免整个游戏的名称依赖性的一种方法是设置脚本以引用场景作为 对象 类型。这使您可以将场景资产拖放到 Inspector 中,然后在脚本中安全地获取其名称。但是,由于它是一个编辑器类,您无法在运行时访问 AssetDatabase 类,因此您需要将这两部分数据结合起来,以获得在编辑器中有效、防止人为错误且在运行时仍有效的解决方案。您可以参考 ISerializationCallbackReceiver 接口作为如何实现对象的示例,该对象在序列化后可以从场景资源中提取字符串路径并将其存储以供运行时使用。
此外,您还可以创建自定义检查器,以便更轻松地使用按钮将场景快速添加到 构建设置 ,而不必通过该菜单手动添加它们并保持它们同步。
作为此类工具的示例,请查看开发人员 JohannesMP 的 这个出色的开源实现 (这不是官方的 Unity 资源)。
这篇文章仅展示了 ScriptableObjects 在使用多个场景和预制件时可以增强您的工作流程的一种方法。不同的游戏管理场景的方式有很大差异——没有一种解决方案适用于所有游戏结构。实施您自己的自定义工具以适应您的项目组织是非常有意义的。
我们希望这些信息能够帮助您完成项目,或者激励您创建自己的场景管理工具。
如果您有任何疑问,请在评论中告诉我们。我们很想听听您使用什么方法来管理游戏中的场景。您还可以随意建议您希望我们在未来的博客文章中涵盖的其他用例。