本页解释如何使用 ScriptableObjects 作为逻辑容器。通过这样做,您可以将它们视为委托对象,或者在需要时可以调用的小动作包。
这是六个迷你指南系列中的第四个,旨在帮助 Unity 开发人员使用电子书附带的 演示 “ 使用 ScriptableObjects 在 Unity 中创建模块化游戏架构”。
该演示的灵感来自经典的球和桨街机游戏机制,并展示了 ScriptableObjects 如何帮助您创建可测试、可扩展且设计人员友好的组件。
电子书、演示项目和这些迷你指南共同提供了在 Unity 项目中使用 ScriptableObject 类的 编程设计模式 的最佳实践。这些技巧可以帮助您简化代码,减少内存使用,并提高代码的可重用性。
本系列包括以下文章:
在深入研究 ScriptableObject 演示项目和本系列迷你指南之前,请记住,设计模式本质上只是想法而已。它们并不适用于所有情况。这些技术可以帮助您学习使用 Unity 和 ScriptableObjects 的新方法。
每种模式都有优点和缺点。仅选择那些对您的特定项目有意义的内容。您的设计师是否严重依赖 Unity 编辑器?基于 ScriptableObject 的模式可能是帮助他们与开发人员合作的一个好选择。
最终,最好的代码架构是适合您的项目和团队的架构。
使用 策略模式,您可以定义一个接口或基 ScriptableObject 类,然后使这些委托对象在运行时可互换。
一种应用是将执行特定任务的算法封装到ScriptableObject中,然后在其他上下文中使用该ScriptableObject。
例如,如果您正在为 EnemyUnit 类编写 AI 或寻路系统,则您可能会创建具有路径搜索技术(如 A*、Dijkstra 等)的 ScriptableObject。
EnemyUnit 本身实际上并不包含任何寻路逻辑。相反,它会保留对单独的“策略”ScriptableObject 的引用。这种设计的结果是,您只需交换对象就可以切换到不同的算法。这是在运行时选择不同行为的一种方法。
当 MonoBehaviour 需要执行任务时,它会调用 ScriptableObject 上的外部方法而不是自己的方法。例如,ScriptableObject 可能包含 MoveUnit 或 SetTarget 的 公共方法,用于驱动敌方单位并指定目的地。
您可以使用抽象基类或接口来改进这种模式。这样做意味着任何实现该策略的 ScriptableObject 都可以与另一个进行交换。这个可热插拔的 ScriptableObject “插入”到引用它的 MonoBehaviour 中 – 即使在运行时也是如此。
如果需要 EnemyUnit 根据游戏条件改变行为,外部环境(MonoBehaviour)可以检查这些条件。然后,它可以插入不同的 ScriptableObject 作为响应。
通过将实施细节分离到 ScriptableObject 中,您还可以促进团队之间更好地划分责任。一位开发人员可以专注于 ScriptableObject 内部的算法,而另一位开发人员则致力于 MonoBehaviour 上下文。
要创建此可插入行为,请确保:
- 为该策略定义一个基类或接口:该类或接口应该包括执行策略所需的方法和属性。
- 创建 ScriptableObject 类:每个都可以提供该策略的不同实现。例如,您可以创建一个实现简单 AI 算法的类和另一个实现更复杂算法的类。
- 创建一个实现该策略的 ScriptableObject:填写缺失的逻辑并使用任何必要的值填充 Inspector。
- 在上下文中使用该策略:在MonoBehaviour中,调用ScriptableObject中实现的方法和属性。为了更容易地调用这些方法,将依赖项作为参数传入。
像这样组织代码可以更轻松地在同一策略的不同实现之间切换。这种可插入行为变得更容易调试和维护。
算法或策略不必太复杂。例如,PaddleBallSO 项目演示了 SimpleAudioDelegate 中相当基本的音频播放系统。
抽象类 AudioDelegateSO定义了一个接受 AudioSource 参数的 Play 方法。然后具体实现会覆盖它。
SimpleAudioDelegateSO 子类定义了一个 AudioClips 数组。它选择一个随机剪辑并使用重写的 Play 方法实现播放它。这会在自定义范围内添加音调和音量的变化。
虽然只有几行,但您可以使用下面的代码片段制作许多不同的音频效果。
虽然这个特定的例子并不真正适合大量音频使用,但它在这里作为策略模式中 ScriptableObjects 的基本使用演示进行展示。
设计师可以创建许多不同的 ScriptableObject 来表现声音效果,而无需接触代码。再次,一旦基础 ScriptableObject 完成,这只需要开发人员提供最少的支持。
在 PaddleBallSO中,任何人现在都可以设置一组新的声音,当球击中关卡墙壁时播放这些声音。设计师获得了创作的独立性和灵活性,因为他们完全在编辑器中工作。这种方法释放了编程资源,因为开发人员不再需要协助做出每个设计决策。
您还可以在 Patterns 演示中看到音频示例。每个声音都来自略有不同的 SimpleAudioDelegateSO 资源,实例之间有细微的差异。
在这个例子中,每个角落都包括一个 AudioSource。自定义 AudioModifier MonoBehaviour 使用基于 ScriptableObject 的委托来播放声音。
音调的差异仅源于每个 ScriptableObject 资源上的设置(BeepHighPitched_SO、BeepLowPitched_SO 等)。
使用 ScriptableObject 来控制动作逻辑可以让您的设计团队更轻松地尝试各种想法。这使得设计师能够更加独立于开发人员工作。
PaddleBallSO 项目也在其目标系统中使用了策略模式。虽然这不是需要在运行时改变的东西,但将每个对象封装在 ScriptableObject 中提供了一种灵活的方式来测试胜负条件。
抽象基类 ObjectiveSO 保存诸如目标名称和是否已完成之类的值。
然后,具体的子类(如 ScoreObjectiveSO)实现如何完成每个目标的实际逻辑。他们通过覆盖 ObjectiveSO 的 CompleteObjective 方法并添加胜利条件逻辑来实现这一点。
玩家是否需要达到特定的分数或击败一定数量的敌人?他们是否需要到达特定地点或领取特定物品?这些是可以成为基于 ScriptableObject 的目标的常见获胜条件。
ObjectiveManager 是 ScriptableObjects 的更大背景。它维护一个 ObjectiveSO 列表并依赖每个 ScriptableObject 来确定它是否完整。当每个ObjectiveSO显示完成状态时,游戏就结束。
例如,ScoreObjectiveSO 展示了实现评分目标的一种方法:
- 自定义的 PlayerScore 结构与玩家 ID、界面中的 UI 元素和实际得分值相匹配。
- 每次 ScoreManager 组件更新时,目标都会检查获胜条件。
- 如果玩家的得分达到或超过 m_TargetScore,那么它会将获胜的 PlayerScore 对象作为事件发送。
ObjectiveManager 仅关心所有给定的目标是否完成。它并不知道每个目标本身的细节。
再次强调,这里的目标是模块化。这使您可以自定义每个 ObjectiveSO,而不会影响预先存在的 ObjectiveSO。
PaddleBallSO 游戏实际上只有一个目标。如果其中一名玩家达到了获胜分数目标,游戏就结束。
但是,您可以扩展它或组合目标来创建更复杂的目标系统。进行试验,看看是否可以构建新的游戏模式(例如,在时间耗尽之前获得最低数量的分数)。
由于逻辑被封装在 ScriptableObject 中,因此您可以将任何 ObjectiveSO 交换为另一个。制定新的胜利条件只需重新配置 ObjectiveManager 中的列表。从某种意义上来说,目标是可以“插入”到周围环境中的。
请注意,ObjectiveSO 的一个方便的方面是用于在 GameObjects 之间发送消息的事件。接下来,我们将探讨如何使用 ScriptableObjects 来实现这种事件驱动的架构。
在电子书 《使用 ScriptableObjects 在 Unity 中创建模块化游戏架构》中了解有关使用 ScriptableObjects 的设计模式的更多信息。您还可以在 使用游戏编程模式提升您的代码水平中了解有关常见 Unity 开发设计模式的更多信息。