我与Unity在一起已经有好几年了,为iOS和Android创建移动游戏以及在几次Ludum Dare游戏中使用它。 通常,我非常喜欢Unity以及使用它免费获得的所有内容。 但是,首先将自己视为程序员,我一直在努力寻找创建模块化且可维护的代码库的好方法。 在Unity中构建脚本的一般方法(在大多数在线教程中都有显示)通常会产生紧密耦合的整体脚本。 这使代码难以测试,难以调试且难以理解。 当许多开发人员共同致力于一个更大的项目时,情况将变得更加糟糕。 上述问题一直困扰着我,使我在开发游戏时动力不足,有时甚至使我感到悲伤。
在Unite 2016期间看到Richard Fine的演讲确实使我对Unity中可脚本化对象的使用大开眼界。 Ryan Hipple在Unite 2017上的演讲进一步利用了Scriptable Objects,并向观众介绍了共享状态变量(如Scriptable Objects)和漂亮的事件侦听器模式的概念。 我真的建议您观看以上两个讲座,并阅读有关游戏架构和可脚本编写对象的文章。
即使我真的相信您应该在继续之前观看上面的视频并将您的头围绕可脚本编写的对象,但我知道这是一个很大的承诺。 因此,我将通过列出一些使脚本功能强大的属性,以及与MonoBehaviours的不同之处,来尝试解释和总结为什么Scriptable Objects是Unity工具箱中的一个出色工具。
- 与MonoBehaviours不同,可脚本编写的对象不存在于场景中,不附加到GameObjects上,也没有变换组件。
- 可以另存为
.asset
文件,但也可以在运行时实例化并.asset
在内存中。 - 在Unity编辑器中序列化,因此可以在Unity Inspector中查看。
- 除了
OnEnable
和OnDisable
之外,没有大多数习惯于MonoBehaviours的生命周期回调。 - 非常适合数据存储。
可脚本化对象的所有这些特性使它们非常适合替换原本会存在于MonoBehaviours中的状态和功能。 通过定义很小的状态(例如,只是一个变量),将微小的可重用函数分解为可编写脚本的对象,同时将Unity的Inspector视为注入依赖的一种方式,我们可以构建可重复使用的模块化代码和状态并注入我们的MonoBehaviours。
开发者说明是描述变量以进行记录的文本,“ 值”是变量的实际值,“ 旧值”是变量通过代码更改后拥有的最后一个值。 更改和更改历史记录将在本文后面的“游戏事件”部分中进行说明。
我们将创建的IntVariables命名为Health
和MaxHealth
并设置它们的初始值(在本例中为100)。 创建它们之后,我们可以通过Unity的检查器将它们放在PlayerHealth和HealthBar组件上,如下所示:
变量为我们提供了一种将游戏的共享状态与实际实现分开的方式。 由于我们不需要在脚本中引用其他MonoBehaviour,因此这也减少了代码的耦合。 我们不需要像这样在PlayerHealth.cs
脚本中引用PlayerHealth.cs
脚本:
[SerializeField]
私人玩家玩家;
为更少的耦合代码欢呼! 🎉
游戏事件是我们的游戏中发生的其他脚本或实体可以监听和订阅的事件。 游戏事件(如变量)也是位于特定场景之外的可编写脚本的对象。 在Unity Atoms中,游戏事件可以具有不同的类型,从而将长数据传递给侦听器。 默认情况下,变量确实可以引发两个特定的游戏事件:
- 已更改 -每次更改变量的值时引发。 游戏事件包含新值。
- 随着历史记录更改 -每次更改变量的值时也会引发。 但是,此游戏事件包含新值和旧值。
这比仅使用变量更容易使我们的游戏更多地驱动数据。 让我们看一下最后一个示例中的外观。 我们可以通过右键单击并创建/ Unity Atoms / Int / Event并将其命名为HealthChangedEvent
将新的IntEvent创建为.asset
文件:
然后将其放在我们的IntVariable上,以确保玩家的健康,如下所示:
然后,我们可以修改HealthBar.cs
脚本,使其看起来像这样(暂时不用担心IGameEventListener
东西):
现在,我们对全局状态更改做出反应,而不是在每个Update滴答声中检查Variable值。 换句话说,我们仅在实际需要时才更新Image组件。 真是太好了!
仍然存在一个问题,即HealthBar.cs
脚本负责将自身注册为侦听器,同时定义引发游戏事件时发生的情况。 我们需要分开关注它! 这将我们带入Unity Atoms的第三个概念,即游戏事件监听器。 游戏事件侦听器侦听(有时也称为观察或订阅)游戏事件,并通过触发零到许多响应来做出响应。 游戏事件侦听器是MonoBehaviours,因此生活在一个场景中。 它们可以看作是游戏事件和响应之间的黏合剂(请参阅本文的下一部分)。
我们上一个示例中的HealthBar.cs
脚本实际上是一个Game Event Listener,但是它是一个非常具体的实现。 我们可以做得更好! 让我们在场景中创建一个GameObject并将其称为HealthListener。 Unity Atoms附带了一些预定义的游戏事件监听器。 在这种情况下,我们想听一个IntEvent,所以我们将按下HealthListener上的Add Component按钮,创建一个IntListener并放入HealthChangedEvent
:
现在,我们可以HealthBar.cs
脚本中的某些代码,如下所示:
HealthBar.cs
脚本现在仅负责玩家的健康状况发生变化时发生的情况。 太好了吧?
上面创建的HealthChanged函数实际上是Unity事件类型的Response。 但是,在Unity Atoms中,还可以将响应创建为可编写脚本的对象,称为游戏动作。 游戏动作是不返回值的可编写脚本对象的功能。 附带说明一下,Unity Atom中还包含游戏功能,它们与游戏动作完全一样,但确实会返回一个值。 为了演示游戏动作作为响应的概念,让我们创建一个名为HealthLogger.cs
的游戏动作,该游戏动作对控制台有所帮助,并在玩家改变时记录玩家的健康状况:
每当玩家的健康发生变化时,我们现在就注销玩家的健康。 这个特定的例子非常简单,但是我相信您可以为它提出许多其他用例(例如播放声音或发出一些粒子)。
这就对了! 我们已经介绍了Unity Atoms的四个最基本的核心部分。 接下来,我们将深入研究库中包含的其他一些便捷工具。
当您开始考虑这种事件侦听器响应模式时,您很快就会意识到游戏中的所有内容都可以用这种方式表示。 实际上,本机Unity生命周期方法可以看作是我们监听并响应的事件。 Unity的生命周期方法与游戏事件之间的桥梁称为MonoHooks。
在我们的示例中,我们可以使用MonoHook OnTriggerEnter2DHook完全替换Harmful.cs
脚本。 我们将从项目中删除Harmful.cs
脚本开始,并在Harmful.cs
删除其组件。 接下来,我们将向Harmful GameObject添加两个新组件,即OnTriggerEnter2DHook和Collider2DListener(两者均在Unity Atoms中预定义)。 然后,我们将创建一个Collider2DEvent(创建/ Unity Atoms / Collider2D / Event),并将其HarmfulTriggerEvent
。 我们将对刚刚创建的OnTriggerEnter2DHook和Collider2DListener都放到事件的HarmfulTriggerEvent
上。 最后,我们将创建一个新的名为DecreasePlayersHealth.cs
Collider2DAction,如下所示:
我们还可以添加MonoHook OnStartHook来设置场景开始时我们的Health
变量的初始值。 为此,我们将在场景中创建一个新的GameObject并为其添加一个OnStartHook组件和一个VoidListener组件(默认情况下都包含在Unity Atoms中)。 然后,我们将创建一个类型为void的游戏事件,并将其添加到两个组件的Event中。 最后,我们可以创建一个名为SetIntVariable的预定义游戏动作(Create / Unity Atoms / Int / Set变量),该动作将实际设置Health
变量的值并将其命名为SetPlayerHealth
:
场景开始时,这会将播放器的健康状况正确设置为100。
有关MonoHooks的其他一些注意事项: 具有GO Ref的事件是一个游戏事件,它将像常规Event一样引发,但引用了带有OnStartHook组件的GameObject(默认情况下)。 如果定义了Select GO Ref ,我们可以选择带有GO Ref的事件将引用的GameObject。 例如,当我们想使用MonoHook组件引用GameObject的父级或子级时,这可能会很有用。
常量是变量的简化版本。 当值更改时,“ 旧值”字段均不包含任何“游戏事件”。 不能通过代码更改常量的值。 想法是将常量用于不因更改而引起的全局共享值,例如。 标签,图层名称,乘数等。在我们的示例中,最好将MaxHealth
设置为常量:
并在DecreasePlayersHealth.cs
脚本中更改玩家标签:
可以通过Unity Inspector在使用常量或使用变量之间切换引用。 当引用设置为使用常量时,其功能完全类似于MonoBehaviour脚本中的常规序列化变量。 但是,将其设置为使用变量时,其功能与变量完全相同。 这是直接从Ryan Hipple的Unite 2017演讲中复制的内容。
列表是存储为可编写脚本的对象的值的数组。 发生以下情况时,可以将游戏事件添加到列表中:
- 一项添加到列表中。
- 从列表中删除了一个项目。
- 该列表已清除。
列表非常适合存储需要在运行时进行跟踪和创建的数据或对游戏对象的引用。
Unity Atoms是开源的,可以从Github下载并通过此处在您的项目中使用。 该项目包含了本文中使用的示例的最终版本。
从这里开始的想法是添加更多功能以及更通用的游戏动作和游戏功能,这些功能不仅可以在我当前的游戏中使用,而且可以在将来的游戏中或您的游戏中重复使用。 如果可以重用经过实践检验的,经过验证的模块化代码段,而不是从头开始编写游戏中的所有功能,则可以减少错误(更少的代码=更少的错误)。 我已经创建的可重用代码段的一些示例是:
- GetUnusedGameObject —一个游戏函数,用于向GameObject列表查询未使用的GameObject。 如果找到一个,则返回它,否则使用给定的预制实例化一个新的,并将其添加到列表中。
- ChangeScene —一个按字符串名称加载场景的游戏动作。
我希望您喜欢阅读此博客文章,并希望阅读后认识到使用Scriptable Objects可以实现的所有出色功能。 很想在下面的评论中听听您对此帖子的看法。 谢谢阅读! 💚给我一些👏,如果您喜欢刚读的内容,请在Twitter上关注我。 干杯!