ScriptableObject:为游戏开发带来六大好处

我们很高兴发布了新电子书“ Create modular game architecture in Unity with ScriptableObjects 用 ScriptableObjects 创建模块化的游戏架构 ”,该书收集了职业开发者在实际生产里应用 ScriptableObject 的最佳做法。
本书还配有一个 GitHub 演示项目可供下载。这个受经典弹球游戏启发的项目展示了如何用 ScriptableObject 创建可测试、可扩展的游戏组件,让设计师也能轻松使用。尽管编写这种游戏完全用不到这么多代码,但它的主要作用是展示 ScriptableObject 的实际应用。
本篇博文主要介绍 ScriptableObject 带来的好处,但不会覆盖编程基础知识。若您还是初次接触 Unity 编程,可以前往 Unity 中文课堂学习 入门教程 ,电子书的第一章同样也有翔实的介绍。

1. 为团队协作提质增效

尽管这里分享的许多技巧同样能用 C# 类实现,ScriptableObject 最主要的好处在于可为艺术家和设计师所用。他们可以使用对象来配置和实施游戏逻辑,不必亲自编写代码。
ScriptableObject 可以轻松地在编辑器内查看和编辑,让设计师可以不依赖开发团队的支持来创建游戏内的数值。同样,类似 NPC 行为等游戏逻辑也可以用 ScriptableObject 添加(详情请见下方应用模式)。
如果有两个人同时修改了一个预制件或场景,储存在一个 MonoBehaviour 的数据和逻辑会产生合并冲突,耗费大量时间。但只要把共享的数据用 ScriptableObject 拆分成迷你文件或资产,设计师就能与程序员并行建立游戏玩法,不必再等后者写好代码后进行测试。
多人同时访问游戏代码和资产会产生许多问题,但有了 ScriptableObject,程序员可以控制项目的可编辑部分。另外,用它组织代码可以很自然地形成一种更模块化、测试起来更高效的代码基础。

游戏设计师怎样使用 ScriptableObject

克里斯托·诺布斯(Christo Nobbs),一名专攻游戏系统设计和 Unity(C#)的资深技术游戏设计师,也参与了这本 Unity 游戏设计师实操手册 的编写,并且也是游戏系统设计系列博文的主要作者。他的博文 Systems that create ecosystems: Emergent game design 以系统造就生态:游戏涌现性设计Unpredictably fun: The value of randomization in game design “意外”的乐趣:随机化在游戏中的价值 列出了几种使用 ScriptableObjects 的有趣例子。

2. 数据容器

模块化是常见的软件开发原则,在 C# 中不用 ScriptableObject 也能实现。但正如上文提到的,ScriptableObjects 能让数据脱离逻辑,踏出代码模块化的第一步,强化干净的编程方法。脱离后,改动会更轻松,测试也更方便。
ScriptableObject 长于储存静态数据,是配置物品或 NPC 面板数据、角色对话等静态数值的便利手段。它们会被储存为资产,脱离于游戏本身持续存在,其中的静态配置在加载后可于运行时动态地变化。
尽管 ScriptableObject 数据的更改的确可在编辑器内长期存在,但它们并不适合用于保存游戏数据。倘若游戏性能非常关键,类似 JSON、XML 或二进制文件等序列化系统会是更好的选择。
由于 MonoBehaviour 需要一个 GameObject 以及默认一个 Transform 作为宿主,它一般会产生额外的开销。要想储存一个值,需要生成许多用不上的数据。而 ScriptableObject 可以减少内存痕迹,抛弃 GameObject 和 Transform,将数据储存为项目文件,方便从多个场景访问相同的数据。
很多时候,多个 GameObject 都有不会在运行时改变的重复数据。与其为每个对象复制一份数据,不如把它收集到一个 ScriptableObject 上,让每个对象引用共享的资产,而不是重复复制这些数据。若项目包含有数千个这种对象,这种做法能带来非常明显的性能提升。
图注:每个对象复制一份重复的本地数据会造成性能低下
图注:让对象共享一份 ScriptableObject 上的数据(不再进行复制)
软件设计领域有一种称为“ 轻量模式 ”(flyweight pattern)的优化方式。ScriptableObject 可以通过避免多次复制数值、降低内存痕迹来实现代码的“轻量化”。我们的电子书“ Level up your code with game programming patterns ”可以帮您进一步了解 Unity 里的各种设计模式。

3. Enum 类

ScriptableObject 简化代码的另一个好的例子是用作对比的枚举类(Enum)。它可用于表示一套类别或物品类型,比如冰冻、点燃、电击、魔法等特殊伤害效果。
如果应用中有装备游戏物品的物品栏系统,ScriptableObjects 可用于表示物品类型或武器栏。我们可在检视器内拖放对象到这些字段来完成配置。
图注:基于 ScriptableObject 创建的可拖拽类别系统
用作枚举类的 ScriptableObjects 在扩展和添加更多数据上更有优势。不同于普通的枚举类,它们可以包含额外的字段和方法,不需要创建单独的参考表或关联到新的数据组。
传统的枚举类保存着一套固定的数值,而 ScriptableObject 的枚举类可在运行时创建和修改,在需要时添加或删除数据。
如果一列长长的枚举值不带有明确的序数,添加或移除一条数据会改变列表的顺序,修改后的顺序可能会产生不易察觉的 Bug。ScriptableObject 的枚举类就没有这种问题,不必修改代码就能为项目删除或添加数据。
假设我们想要让一款 RPG 里的某样物品变成可装备物品,可以向 ScriptableObject 加上一条额外的布尔字段(boolean field)。不想让特定角色持有某些物品,或部分物品带有魔法或特殊效果,ScriptableObject 的枚举都能做到。

4. 委托对象

支持编写方法的 ScriptableObject 同样也能包含逻辑或行为。将移动的逻辑从 MonoBehaviour 转移到 ScriptableObject 能让后者成为一个委托对象,使移动行为更为模块化。
如果需要执行某些特定任务,可以将算法封装到单独的对象中。 The "Gang of Four" (Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides)将这种设计称为 策略模式 。下方例子使用了一个抽象类来应用 EnemyAI,进一步扩展了这种模式。最终派生出的 ScriptableObjects 包含不同的行为,每种行为都可像插头一样插拔和替换,只需把需要的 ScriptableObject 拖放到 MonoBehaviour 上即可。
图注:可插拔行为可在运行时或编辑器内改变
要想详细了解怎样用 ScriptableObject 驱动行为,请观看 Pluggable AI with ScriptableObjects 系列视频。视频中演示了一种基于有限状态机的 AI 系统,它可以用 ScriptableObjects 设定状态、动作和状态过渡。

5. 事件频道

大型项目还有另一个常见的难题:几个 GameObjects 需要共享数据或状态,却又不能直接引用相应的对象。否则,大规模的对象依赖关系管理起来费时费力,又容易出错。很多开发者使用了单例模式——创建一个不会在加载时销毁的全局实例。然而单例产生的全局状态非常难以测试。如果一个预制件引用了单例,单个功能的测试势必会导入所有依赖项,降低代码的模块化程度和调试效率。
一个解决方案是使用基于 ScriptableObject 的事件来帮助 GameObjects 的沟通。这种方法像是使用 ScriptableObject 来实现一种 观测者模式 ,即一个主体向一个或多个松散的观测者广播信息。每个观测的客体可以独立地根据主体消息做出反应,并且不会意识到其他观测者的存在。这个主体可以被叫做“发布者”或“广播者”,而观测者可被称为“订阅者”或“收听者”。
观测者模式同样也能用 MonoBehaviour 或 C# 对象 实施。虽然这种做法在 Unity 开发里非常常见,但只用代码实施意味着设计师们需要依靠程序员来设立游戏期间需要的每种事件。
图注:基于 ScriptableObject 的事件频道示意
使用 ScripatbleObject 看上去是给观测者模式平添开销,但它有着自己的优点。作为资产,ScripatbleObjects 可以为层级结构里的所有对象所访问,不会在场景加载时消失。
对特定资源的方便、持久的访问是许多开发者使用单例的原因。而 ScriptableObjects 不需引入不必要的依赖项也可提供相同的好处。
在基于 ScriptableObject 的事件中,任何对象都能成为发布者(事件的广播源),也能成为订阅者(事件的收听者)。ScriptableObject 则在中间,帮助转发信号,就像两者之间的集中中介。
我们可以把这种机制看作一种“事件频道”,ScriptableObject 就是一座广播塔,任意数量的对象都可以收听它的信号。相关的 MonoBehaviour 可以订阅这个频道,在事件发生时做出反应。
在开篇提到的演示项目中,也展示了怎样用观测者模式建立 UI、音效和得分的游戏事件。

6. Runtime Set(运行时设置)

我们经常需要在运行时跟踪场景里的对象和组件,比如一队敌人。但随着更多敌人被生成和打败,这个列表也会不断变化。单例模式的确能提供便捷的全局访问,但它有一定的缺点。与其使用单例,我们可以考虑储存数据到 ScriptableObject 上作为“Runtime Set”。ScriptableObject 作为项目文件存在,意味着它的数据可对任意场景的任何对象开放,形成类似的全局访问。由于数据是在一个资产上,公开的物品列表可以随时被访问。
这种用法中,对象成为了一种专门的数据容器,维护着一个公开的元素集,同时又带有添加和移除元素的基本方法。这可以降低对单例的依赖,提高测试的可行性和模块化程度。
图注:“Runtime Set”能为一套数据集开放全局访问
直接从 ScriptableObject 读取数据同样要优于用 Object.FindObjectOfType 或 GameObject.FindWithTag 等查找命令搜索场景的层级。根据用例和层级的大小,这些开销较大的方法可能使得每帧的更新变得低效。
除了这六种情形,ScriptableObject 还有更多的用法。有些团队会经常使用 ScriptableObjects,有的则只用来加载静态数据或拆分逻辑与数据。最终的用法还是需要视项目需要而定。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值