搜索 Unity

ScriptableObject(可编程对象)为团队和代码带来的六个好处

2023年4月20日 类别 Engine & platform | 17 分 阅读
6 ways ScriptableObjects can benefit your team and your code | Hero image
6 ways ScriptableObjects can benefit your team and your code | Hero image
涵盖的主题
分享

Is this article helpful for you?

Thank you for your feedback!

我们很高兴地发布新电子书,“Create modular game architecture in Unity with ScriptableObjects”,该书收集了职业开发者在实际生产里应用ScriptableObject的最佳做法。

本书还配有一个GitHub演示项目可下载。这个受经典弹球游戏启发的项目展示了如何用ScriptableObject创建可测试、可扩展的游戏组件,让设计师也能轻松使用。尽管编写这种游戏完全用不到这么多代码,但它的主要作用是展示ScriptableObject的实际使用。

A ball and paddle demo accompanies the e-book.
与书配套的ScriptableObject演示项目已在Github上开放。

本篇博文主要解释ScriptableObject的好处,但不会覆盖总的编程基础。若您还是初次接触Unity编程,请前往Unity Learn学习入门教程。书的第一章同样也有翔实的介绍。

接下来我们看看使用ScriptableObject的六大好处。想了解更多?所有例子都在电子书和演示里有详细的解释。

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都会有不会在运行时改变的重复数据。很多时候,多个GameObject都会有不会在运行时改变的重复数据。与其为每个对象复制一份数据,不如把它收集到一个ScriptableObject上,让每个对象引用共享的资产,而不是重复复制这些数据。若项目包含有数千个这种对象,这种做法能带来非常明显的性能提升。

Many objects with duplicate local data leads to performance inefficiencies.
许多对象自己带有一份相同的数据会造成性能低下。
Many objects sharing data (rather than duplicating it) via a ScriptableObject
让对象共享一份ScriptableObject上的数据(不再进行复制)。

软件设计领域有一种称为“轻量模式”(flyweight)的优化方式。ScriptableObject可以通过避开多次复制数值、降低内存痕迹来实现代码的“轻量化”。我们的电子书“Level up your code with game programming patterns”可以帮您进一步了解Unity里的各种设计模式。

3.Enum类

ScriptableObject简化代码的另一个好例子是用作对比的枚举类。它可用于表示一套类别或物品类型,比如冰冻、点燃、电击、魔法等特殊伤害效果。

如果应用中有一个装备物品的物品栏系统,ScriptableObject可用于表示物品类型或武器栏。你可在检视器内拖放对象到这些字段来完成配置。

Drag-and-drop ScriptableObject-based categories
基于ScriptableObject创建的可拖拽类别系统。

用作枚举类的ScriptableObject在扩展和添加更多数据上更有优势。不想普通的枚举类,它们可以包含额外的字段和方法,不需要创建单独的参考表或关联到新的数据组。不需要创建单独的参考表或关联到新的数据组。

传统的枚举类保存着一套固定的数值,而ScriptableObject的枚举类可在运行时创建和修改,在需要时添加或删除数据。

如果一列长长的枚举值不带有明确的序数,添加或移除一条数据会改变列表的顺序。修改后的顺序可能会产生不易察觉的Bug或意料之外的行为。ScriptableObject的枚举类就没有这种问题,你不必修改代码就能为项目删除或添加数据。

假设我们想要让一款RPG里的某样物品变成可装备物品,可以向ScriptableObject加上一条额外的布尔字段。不想让特定角色持有某些物品?部分物品带有魔法或特殊效果?ScriptableObject的枚举都能做到。

4.委托对象

支持编写方法的ScriptableObject同样也能包含逻辑或行为。将移动的逻辑从MonoBehaviour转移到ScriptableObject能让后者成为一个委托对象,使移动行为更为模块化。

如果需要执行某些特定的任务,你可以将算法封装到单独的对象中。“四人帮”将这种设计称为策略模式。下方例子使用了一个抽象类来应用EnemyAI,进一步扩展了这种模式。最终派生出的ScriptableObject包含不同的行为,每种行为都可像插头一样拔插和替换,你只需把需要的对象拖放到MonoBehaviour上即可。

Pluggable behaviors can change at runtime or in the Editor.
可拔插行为可在运行时或编辑器内改变。

要想详细了解怎样用ScriptableObject驱动行为,请观看Pluggable AI with ScriptableObjects视频系列。视频中演示了一种基于有限状态机的AI系统,它可以用ScriptableObject设定状态、动作和状态过渡。

5.事件频道

大型项目还有另一个常见的难题:几个GameObject需要共享数据或状态,却又不能直接引用相应的对象。否则,大规模的对象依赖关系管理起来费时费力,又容易出错。很多开发者使用了单例模式——创建一个不会在加载时销毁的全局实例。然而单例产生的全局状态非常难以测试。如果一个预制件引用了单例,单个功能的测试势必会导入所有依赖项,降低代码的模块化程度和调试效率。这种方法像是一种观测者模式,即一个主体向一个或多个松散的观测者广播信息。

每个观测的客体可以独立地根据主体消息做出反应,并且不会意识到其他观测者的存在。这种方法像是一种观测者模式,即一个主体向一个或多个松散的观测者广播信息。每个观测的客体可以独立地根据主体消息做出反应,并且不会意识到其他观测者的存在。这个主体可以被叫做“发布者”或“广播者”,而观测者可被称为“订阅者”或“收听者”。

观测者模式同样也能用MonoBehaviour或C#实施。虽然这种做法在Unity开发里非常常见,但只用代码实施意味着设计师们需要依靠程序员来设立游戏期间需要的每种事件。

The ScriptableObject-based event channel
基于ScriptableObject的事件频道

使用ScripatbleObject看上去是给模式平添开销,但它有着自己的优点。使用ScripatbleObject看上去是给模式平添开销,但它有着自己的优点。

作为资产,它们可以为层级结构里的所有对象所访问,不会在场景加载时消失。而ScriptableObject不用许多不必要的依赖项即可提供相同的好处。

任何对象都能成为发布者(事件的广播源),也能成为订阅者(事件的收听者)。而ScriptableObject不用许多不必要的依赖项即可提供相同的好处。

你可以把它看作一种“事件频道”,ScriptableObject就是一座广播塔,任意数量的对象都可以收听它的信号。相关的MonoBehaviour都可以收听这个频道,在事件发生时做出反应。

演示项目展示了怎样用观测者模式建立UI、音效和得分的游戏事件。

6.Runtime Set(运行时设置)

我们经常需要在运行时跟踪场景里的对象和组件,比如一队敌人。但随着更多敌人被生成和打败,这个列表也会不断变化。单例模式的确能提供便利的全局访问,但它有一定的缺点。与其使用单例,我们可以考虑储存数据到ScriptableObject上作为“Runtime Set”。ScriptableObject作为项目文件存在,意味着它的数据可对任意场景的任何对象开放,形成类似的全局访问。由于数据是在一个资产上,公开的物品列表可以随时被访问。

这个用法里,对象成为了一种专门的数据容器,维护着一个公开的元素集合,同时又带有添加和移除元素的基本方法。这可以降低对单例的依赖,提高测试的可行性和模块化程度。

A Runtime Set provides global access to a collection of data.
“Runtime Set”能为一套数据集合开放全局访问。

直接从ScriptableObject读取数据同样要优于用Object.FindObjectOfTypeGameObject.FindWithTag等查找命令搜索场景的层级。视用法和层级的大小不同,这些开销较大的方法会使得每帧的更新变得低效。

除了这六种情形,ScriptableObject还有更多的用法。有的团队经常使用这些对象,有的则只用来加载静态数据或拆分逻辑与数据。最终的用法还是需要视项目需要而定。

Unity编程高级指南

Create modular game architecture in Unity with ScriptableObjects”是我们第三篇面向中等到高级Unity程序员的指南。该系列的每篇指南都由经验丰富的程序员编写,针对开发的重要话题提供了多种最佳做法。

Create a C# style guide: Write cleaner code that scales”能帮您制定风格指南,统一编写方法,打造一个更连贯的代码基础。

Level up your code with game programming patterns ”着重介绍了SOLID原则和其他常见编程模式的最佳运用方法,为Unity项目创建一个可扩展的代码架构。

这个系列主要为有经验的创作者提供可实施的技巧和灵感,但它们不是铁律。项目可以采用很多种结构,一个项目的结构并不一定适合另一个。请在实际运用之前与团队讨论每种推荐方法、提示和模式的优劣。

更多高级指南和文章请在Unity最佳实践中心查看。

E-book cover for “Create modular game architecture in Unity with ScriptableObjects”
2023年4月20日 类别 Engine & platform | 17 分 阅读

Is this article helpful for you?

Thank you for your feedback!

涵盖的主题
加入论坛讨论
相关文章