如果您是游戏开发人员,但还没有听说过Reactive Programming,请立即放弃您正在做的所有事情,然后花几分钟阅读。 我很认真。

那么,我引起了您的注意吗?
大! 我会尽力不要让你松懈。
什么是反应式编程?
现在,我不会首先从无聊的内容开始,概述什么是反应式编程,它是如何实现的以及为什么它如此有意义。 我将使您更实用。
当您使用像Unity3D这样精心设计的游戏引擎时,作为程序员的大部分时间都是很有趣的。 您正在编写自己的脚本组件,使用这些构建基块构建对象-通常,所有内容都可以很好地融合在一起。 在执行此操作时,对大多数软件设计问题的答案就很自然地出现了。
我应该在哪里存放球员健康?
当然在播放器组件内部!
一旦火箭击中玩家,我应在哪里存储由火箭造成的伤害量?
在Rocket组件内部!
我将剩余的时间存储在跑步关卡中的哪里?
你猜到了。 在“关卡”组件中。
现在,这使我们处于一个非常幸福的地方。一切似乎都坐在它自然应有的位置,世界上一切都很好。 但是众所周知,它永远不会这样长时间存在。
胶水代码-开始发粘时…
现在,由于我们要制作一款非开发人员可以玩的游戏,因此我们的玩家实际上并不关心其内部存储的健康值如何整洁。 他们所关心的只是他们看到它显示在游戏UI中。 那么我们该怎么办?
容易吧? 在Player组件内部,如何放置对UI标签的引用,并且每次运行状况更改时也更新UI?
继续。 该关卡的剩余播放时间如何?
为什么不一样呢? 在关卡组件中,我们放置了对另一个UI标签的引用,并在减少剩余游戏时间时对其进行更新。
但是,现在还不是全部。 因为我们的设计师选择了额外的花哨,并且每当玩家的生命值降低时,都会有出色的闪烁动画。
好吧,那为什么不回到Player类,添加另一个对动画的引用,并在每次运行状况降低时才开始动画?
现在等等 您可以收集一次加电,让您控制另一个玩家一段时间,以显示该玩家的健康状况,而不是您自己的健康状况吗?
好吧,请稍等…啊

响应式编程以营救!
为了理解反应式编程的真正好处,我们将不得不有所理论。 但不必担心,我们将使其尽可能简单。
编写游戏大多是用命令式语言完成的,并且使用C#为Unity3D编写脚本也不例外。 现在,当我们在一方面谈论命令性语言时,另一方面,我们通常将它们与功能编程语言进行比较。 但是我们今天不走这条路,而是在谈论另一种规模。
命令式与声明式
命令式语言和声明式语言之间的差异可以很容易地分解:命令式语言描述了如何 某些事情要做,声明性语言描述了要做什么。
现在我们不能改变C#是命令式语言的事实。 在核心,我们只能使用它来编写命令式命令。 但是,通过使用某些在内部为我们进行繁重工作的框架,我们仍然可以使用某些声明性范式。
这些范例之一是Reactive编程,以Unity的Reactive Extensions(UniRx)形式提供给Unity和C#。
给我看一些代码!
目前还没有足够的理论。 让我们弄脏双手吧!
我们首先看一下我们之前讨论的Player类示例:
使用UnityEngine;
使用UnityEngine.UI;
公共类玩家:MonoBehaviour {
公共int健康= 100;
公共文本scoreLabel;
公共无效ApplyDamage(int损坏){
健康-=伤害;
scoreLabel.text = health.ToString();
}
}
现在,在我们对其进行任何操作之前,让我们仔细看一下,寻找该组件内部的“胶水”,并讨论使它如此粘稠的原因。
使用UnityEngine;
使用UnityEngine.UI;
公共类玩家:MonoBehaviour {
公共int健康= 100;
公共文本scoreLabel;
公共无效ApplyDamage(int损坏){
健康-=伤害;
scoreLabel.text = health.ToString();
}
}
在那里! 那黏的东西。
现在让我们摆脱它,并用一些Reactive Extensions魔术代替它:
使用UnityEngine;
使用UnityEngine.UI;
使用UniRx;
公共类玩家:MonoBehaviour {
公共ReactiveProperty 健康
=新的ReactiveProperty (100);
公共无效ApplyDamage(int损坏){
health.Value- =伤害;
}
}
现在标签参考去了哪里? 我们正在将它移到其他地方。 但是,让我们先讨论一下这个花哨的ReactiveProperty类。
现在,我们只说它是一种特殊的容器,它可以存储通常存储在变量内的任何内容(在这种情况下,它是整数,但也可以是任何其他简单类型或引用)。 我们可以为其分配一个起始值,也可以随时访问和更改随其存储的值。
ReactiveProperty的反应超级能力
向我们的游戏新玩家介绍UiPresenter类:
使用UnityEngine;
使用UnityEngine.UI;
使用UniRx;
公共课程UiPresenter:MonoBehaviour {
公开播放器播放器;
公共文本scoreLabel;
无效Start(){
player.health.SubscribeToText(scoreLabel);
}
}
再次有我们的scoreLabel。 但是,现在等等,这里还发生了什么?

让我们首先尝试消除混乱。
还记得我们如何谈论声明式和命令式,以及反应式扩展如何通过给我们命令式扩展使我们成为声明性的方式使我们成为声明式的? 现在更困惑了吗?
我是故意这样做的! 因为如果您现在感到困惑,那么您将处于正确的心态,很快就会将所有问题联系起来。 不用担心,我们将逐步进行。
让我们从实际上在这里做的事情,或者我们必须告诉计算机去做的事情开始(这是第一个点)。 我们告诉它订阅一个ReactiveProperty。 内部如何完成此操作由我们正在使用的Reactive Extensions定义(因此,我们不必担心它的确切工作方式-,对吗?)。
那是什么意思呢? 订阅ReactiveProperty? 为了回答这个问题,我们必须首先阐明一些概念。
ReactiveProperty是一种特殊的Observable,基类Reactive Properties围绕着它构建。 您可以将可观察对象视为价值流 。

价值流听起来很棒,对吗? 但是可能您真的不知道该如何描述有用的东西。
因此,让我们快速地替换看起来不那么好看但更实用的东西,该图像看起来并没有真正的帮助我们:

你看到了什么? 台球桌!
是的,还有什么?

我看到6个可观察物!
现在要了解什么是可观察物,您需要做的就是增加时间。 想象一下游戏在进行,然后一个接一个地-可观察者将独立地“发射”一个球。 在下图中,我们看到了每个可观察到的孔,图的X轴是前进的时间:

好的,因此可观察的是随着时间流逝的价值流。 现在,关于声明式编程的全部内容是什么?
好了,一旦我们确定了什么是可观察的,就可以通过在它们之上定义通用操作将其提升到一个新的水平。
回到我们的例子:想一想水池球一旦消失在一个洞中会发生什么。 它们开始在桌子下面的某个路径上滚动,并且-就本示例而言-假设它们最终出现在连续的一行中。 所以我们想要的是:

现在描述这种行为,我们可以使用Reactive Extensions提供给我们的声明性语言,如下所示:
all_balls_in_order = Observable.Merge(h1,h2,h3,h4,h5,h6);
现在的关键是,我们还没有真正做任何事情。 单独运行此代码完全无效。 我们在这里不执行任何处理任何数据的操作。 取而代之的是,我们描述给定流将发生什么 ,并创建一个模型,该模型以后可用于表示另一个流(并结束了我们通往理论世界的小旅程)。
回到现实世界:无功超级大国。
现在我们甚至可以穿越时空!
不过,只有向前。 抱歉让您失望。

让我们再来看一个池球示例,这一次我们试图模拟一个事实,即根据球进入哪个孔,它需要特定的时间才能滚动到底部。
all_balls_delayed = Observable.Merge(
h1.Delay(
TimeSpan.FromSeconds(1)
),
h2.Delay(
TimeSpan.FromSeconds(2)
),
h3.Delay(
TimeSpan.FromSeconds(3)
),
h4.Delay(
TimeSpan.FromSeconds(3)
),
h5.Delay(
TimeSpan.FromSeconds(2)
),
h6.Delay(
TimeSpan.FromSeconds(1)
)
);
现在那太简单了,不是吗?
仍然没有留下深刻印象?
在这种情况下,请转到rxmarbles.com,并查看使用简单的内置命令可以完成的更多出色工作。
解开UI示例
还记得我们实际上只是想摆脱一些UI粘合代码,而现在又以某种方式结束了关于时间旅行的讨论吗? 欢迎来到反应式编程的激动人心的世界!
但是,实际上是从前面的例子开始。 让我们讨论一下我们可以在可观察对象上定义的最基本的操作,以及在声明它们之后如何实际使用它们。 因此,让我们看一下UiPresenter的另一个版本:
使用UnityEngine;
使用UnityEngine.UI;
使用UniRx;
公共课程UiPresenter:MonoBehaviour {
公开播放器播放器;
公共文本scoreLabel;
无效Start(){
玩家健康
.Select(health => string.Format(“ Health:{0}”,health))
.Subscribe(text => scoreLabel.text = text);
}
}
首先让我们谈谈Select运算符(在rxmarbles和其他一些Reactive Extension实现上,它称为map )。 它的作用是将函数应用于流中的每个元素。 在这种情况下,我们将所有输入元素(为整数)转换为字符串。
现在,我们接下来要做的就是订阅此结果流。 还记得我们怎么说的那样:仅仅声明流还没有真正做任何事情吗?
订阅实际上开始处理流,将流中的所有值传递给提供的回调。 并且我们在回调中使用此值来更改标签的text属性。 我们之前使用的SubscribeToText命令只是一个快捷方式,其作用完全相同。
现在让我们将这个示例再扩展一次,以实现我们之前希望处理的所有三个需求。 首先让我们制作动画:
使用UnityEngine;
使用UnityEngine.UI;
使用UniRx;
公共课程UiPresenter:MonoBehaviour {
公开播放器播放器;
公共文本scoreLabel;
公共动画师动画师;
无效Start(){
玩家健康
.Select(health => string.Format(“ Health:{0}”,health))
.Do(x => animator.Play(“ Flash”))
.Subscribe(text => scoreLabel.text = text);
}
}
那很容易不是吗? 您可能会猜到,Do运算符只是为流中的每个值执行一个函数,而不更改它。
现在最后是第三件事,支持另一个主动控制的播放器:
使用UnityEngine;
使用UnityEngine.UI;
使用UniRx;
公共课程UiPresenter:MonoBehaviour {
公共ReactiveProperty 播放器
= new ReactiveProperty ();
公共文本scoreLabel;
公共动画师动画师;
无效Start(){
var playerHealth = this.player
.Where(玩家=>玩家!=空)
选择(player => player.health);
玩家健康
.Select(health => string.Format(“ Health:{0}”,health))
.Do(x => animator.Play(“ Flash”))
.Subscribe(text => scoreLabel.text = text);
}
}
那不是那么容易吗? 我们也将播放器也变成了ReactiveProperty,然后从那里拿走它。 因此,我们无需编写处理一个特定播放器的代码,而是将活动播放器从变量转换为流。
而且,一旦您进一步了解反应式编程,您将很快意识到:几乎所有内容都可以变成流。

包起来
现在,如果我还没写完这篇文章,那我应该至少让您对Reactive Programming感到兴奋,并且您想更深入地研究它。 完美的起点可能是您将要使用的相应Reactive Extensions的文档。 因此,对于Unity3D,它就是UniRx。
另外,还可以访问rxmarbles.com,以非常直观的方式直观了解操作,而这些触手可及。 但是请注意,这些操作的名称并不总是与UniRx中的名称匹配(例如, 映射在UniRx中称为Select )。
“您所缺少的反应式编程简介”也是一本好书。
最后,如果您喜欢这篇文章,请不要忘记在出门时按一下拍手按钮👏
谢谢阅读!