您想找什么?
Hero background image
使用 ScriptableObjects 分离游戏数据和逻辑
此页面为机器翻译。如需查看原文以确保准确性并作为权威参考,

本页介绍如何使用 ScriptableObjects 作为数据容器,将数据与游戏代码中的逻辑分开。

这是六个迷你指南系列中的第二部分,旨在帮助 Unity 开发人员使用电子书附带的 演示使用 ScriptableObjects 在 Unity 中创建模块化游戏架构》

该演示的灵感来自经典的球和桨街机游戏机制,并展示了 ScriptableObjects 如何帮助您创建可测试、可扩展且设计人员友好的组件。

电子书、演示项目和这些迷你指南共同提供了在 Unity 项目中使用 ScriptableObject 类的 编程设计模式 的最佳实践。这些技巧可以帮助您简化代码,减少内存使用,并提高代码的可重用性。

本系列包括以下文章:

开始前的重要说明

在深入研究 ScriptableObject 演示项目和本系列迷你指南之前,请记住,设计模式本质上只是想法而已。它们并不适用于所有情况。这些技术可以帮助您学习使用 Unity 和 ScriptableObjects 的新方法。

每种模式都有优点和缺点。仅选择那些对您的特定项目有意义的内容。您的设计师是否严重依赖 Unity 编辑器?基于 ScriptableObject 的模式可能是帮助他们与您的开发人员合作的一个不错的选择。

最终,最好的代码架构是适合您的项目和团队的架构。

tab2
数据容器

软件开发人员通常关注模块化——将应用程序分解为更小的、独立的单元。每个模块负责应用程序功能的某个特定方面。

在 Unity 中,ScriptableObjects 可以帮助将数据与逻辑分离。

ScriptableObjects 擅长存储数据,尤其是静态数据。这使得它们非常适合游戏统计、物品或 NPC 的配置值、角色对话等等。

将游戏数据与行为逻辑隔离可以使项目的每个独立部分更易于测试和维护。当您进行必要的更改时,这种“关注点分离”可以减少意外和不必要的副作用。

tab3
常规工作流

如果您想复习 ScriptableObject 工作流程,这篇 Unity Learn 文章 可以为您提供帮助。否则,这里有一个简短的解释:

定义一个 ScriptableObject:要创建一个,请定义一个从 ScriptableObject 基类继承的 C# 类,该类具有要存储的数据的字段和属性。ScriptableObjects 可以存储与 MonoBehaviours 相同的数据类型,使其成为多功能数据容器。从编辑器添加 CreateAssetMenuAttribute,使在项目中创建资产更加容易。

创建资产:一旦定义了 ScriptableObject 类,就可以在项目中创建该 ScriptableObject 的实例。这显示为保存到磁盘的资产,您可以在不同的游戏对象和场景中重复使用。

设置值:创建资产后,通过在检查器 (Inspector) 中设置其字段和属性的值来用数据填充它。

使用资产:一旦资产保存了数据,就可以从变量或字段中引用它。对 ScriptableObject 资产所做的任何更改都将反映在整个项目中。

您可以将 ScriptableObjects 重新用作游戏不同部分的数据容器。例如,您可以在 ScriptableObject 中定义武器或角色的属性,然后从项目中的任何位置引用该资产。

注意:您还可以通过 CreateInstance 方法在运行时生成 ScriptableObjects。但是,对于数据存储,您通常会使用 CreateAssetMenuAttribute提前创建 ScriptableObject 资源。

tab4
ScriptableObjects 与 MonoBehaviours

为了更好地理解为什么 ScriptableObjects 比 MonoBehaviours 更适合数据存储,请比较每个对象的空版本。确保将 资产序列化 设置为 模式:强制文本 在项目设置中以文本形式查看 YAML 标记。

创建一个具有空的 MonoBehaviour 的新 GameObject。然后,将其与空的 ScriptableObject 资源进行比较。将它们并排放置,它们应该看起来如上图所示的比较。

ScriptableObjects 与 MonoBehaviours 相比更轻量,并且不会产生与后者相关的开销,例如 Transform 组件。这使得 ScriptableObjects 占用更小的内存,并使其更适合数据存储。

tab5
Patterns demo

ScriptableObjects 被保存为资产,因此它们在播放模式之外仍然存在,这很有用。例如,即使您加载了新场景,ScriptableObject 数据也可从任何地方获取。

模式 演示示例具有一个您可以自行测试的基本信用屏幕。修改Credits_Data ScriptableObject,然后按“更新”以查看存储的文本出现。

如果您拥有一款包含大量对话的 RPG 游戏或一款带有预先编写的脚本的教程场景,那么这是一种存储大量数据的常用方法。

虽然 ScriptableObject 中的数据在修改时会立即更新,但我们的项目需要一个更新按钮来手动刷新屏幕。基于 UI 工具包的屏幕只需构建一次,并且需要在数据发生改变时收到通知。

如果您想自动同步更新,请在 ScriptableObject 内创建一个事件。例如,此 ExampleSO 脚本将在每次 ExampleValue 发生改变时调用 OnValueChanged 事件。看下面的代码示例。

然后,让您的监听 UI 对象订阅 OnValueChanged 并进行相应更新。

tab6
使用享元模式存储游戏数据

当许多对象共享相同的数据时,ScriptableObjects 就会发挥作用。例如,如果您正在构建一款策略游戏,其中多个单位具有相同的攻击速度和最大生命值,则在每个游戏对象上单独存储这些值是效率低下的。

相反,您可以在一个中心位置合并共享数据并让每个对象引用该共享位置。在软件设计中,这是一种称为 享元模式的优化。以这种方式重构代码可以避免复制大量值并减少内存占用。

PaddleBallSO中,GameDataSO ScriptableObject 充当共享数据存储。

在可能的情况下,Paddle 和 Ball 脚本会引用相同的 GameDataSO 实例,而不是保留常用设置(速度、质量、物理弹性等)的单独副本。每个游戏元素都保留独特的数据,如位置和输入事件,但在可能的情况下默认为共享数据。

尽管仅使用两个或三个对象时内存节省可能并不明显,但编辑共享数据比手动编辑每个数据更快且更不容易出错。

例如,如果您需要修改桨叶速度,则在单个位置进行调整会更新每个场景中的两个桨叶。如果将它们作为唯一字段存储在 MonoBehaviours 中,一次错误的点击就很容易导致两个值不同步。

将数据卸载到 ScriptableObjects 中还可以帮助进行版本控制,并防止队友在同一个场景或预制件上工作时发生合并冲突。

tab7
PaddleBallSO 游戏数据

GameDataSO 展示了如何使用 ScriptableObject 作为数据容器。在 PaddleBallSO中,这包括配置游戏玩法的各种设置:

  • 桨叶数据:桨的速度、阻力和质量等属性决定了游戏过程中桨的运动和物理特性。
  • 球的数据:它保存了球的当前速度、最大速度和弹跳乘数,控制球与模拟交互时的行为。
  • 匹配数据:GameDataSO 包含有关比赛期间各点之间延迟的信息,有助于控制游戏节奏。
  • 玩家 ID:PlayerIDSO ScriptableObjects 充当每个球员的球队标识(例如,Player1 和 Player2)。
  • 玩家精灵:这些可选的精灵可以实现玩家头像的定制。
  • 级别布局:LevelLayoutSO 对象定义玩家和游戏元素(如目标和墙壁)的起始位置。

通过将这些设置和数据全部放在一个中心位置,GameDataSO 允许任何对象利用这些共享数据。这简化了您管理这些对象的方式并提高了整个项目的一致性。改变桨的物理特性?在此处进行一次更改,而不必调整多个脚本。

tab10
双重序列化示例:关卡布局

有时,鱼与熊掌可以兼得。通过双重序列化,您可以将数据存储在 ScriptableObject 中,同时以另一种格式维护它。

LevelLayoutSO 脚本演示了这一概念。除了保存球拍和球的起始位置之外,它还在自定义结构中存储墙壁和球门的变换数据。

这些值可以通过 ExportToJson 方法写入磁盘。JSON 文件 是人类可读的文本,可以在 Unity 之外直接进行修改。这使您可以在编辑器中使用 ScriptableObjects,然后将其数据存储在另一个位置,例如 JSON 或 XML 文件。

JSON 和 XML 等文件格式在编辑器中处理起来可能比较困难,但在 Unity 之外的文本编辑器中很容易修改它们。这为自定义或用户修改级别提供了可能性。

然后,GameSetup 脚本可以使用 LevelLayout ScriptableObject 或外部 JSON 文件来生成游戏级别。

为了加载自定义修改的级别,安装脚本在运行时使用 CreateInstance 生成一个 ScriptableObject。然后,它从 JSON 文件中读取文本来填充 ScriptableObject。

您的自定义数据将替换 ScriptableObject 的内容,并使您能够像使用其他级别一样使用此外部修改的级别。应用程序的其余部分正常运行,并未意识到这一切换。

tab11
ScriptableObject 数据容器的其他用途

虽然我们的桨球小游戏无法展示 ScriptableObject 数据容器的每个用例,但请在您自己的应用程序中考虑以下内容:

  • 游戏配置:考虑常量、游戏规则或任何其他在游戏过程中不需要改变的设置参数。其他组件随后可以引用该配置数据,而无需使用硬编码值。
  • 角色和敌人属性:使用 ScriptableObjects 来定义健康、攻击力、速度等属性。这使得您的设计师无需开发人员即可平衡和调整游戏元素。
  • 库存和物品系统:项目定义和属性(例如名称、描述和图标)非常适合 ScriptableObjects。您还可以将它们用作库存管理系统的一部分来跟踪玩家收集、使用或装备的物品。
  • 对话和叙述系统:ScriptableObjects 可以存储对话文本、角色名称、分支对话路径和其他与叙述相关的数据。它们可以为复杂的对话系统奠定基础。
  • 等级和进度数据:您可以使用 ScriptableObjects 来定义关卡布局、敌人生成点、目标和其他与关卡相关的信息。
  • 音频片段:正如在 PaddleBallSO 项目中所见,ScriptableObjects 可以存储一个或多个音频剪辑。这些可以定义游戏多个部分的音频效果或音乐。
  • 动画短片:ScriptableObjects 可用于存储动画剪辑,这对于定义多个 GameObject 或角色共享的常见动画很有用。

随着您深入研究 ScriptableObjects 并根据自己的项目对其进行定制,您将发现它们的更多应用。它们对于管理数据特别有用,并且使保持各个游戏元素之间的一致性变得更容易。

可编写脚本的结局
更多 ScriptableObject 资源

在电子书 《使用 ScriptableObjects 在 Unity 中创建模块化游戏架构》中了解有关使用 ScriptableObjects 的设计模式的更多信息。您还可以在 使用游戏编程模式提升您的代码水平中了解有关常见 Unity 开发设计模式的更多信息。

您喜欢本文吗?
是的!
还行。