Introduction
Reactive Programming is a programming paradigm that focuses on data streams and events. It is inspired by the Observer pattern, Iterator pattern and functional programming. It allows us to describe applications in such a way, that whenever a new set of data is emitted by the stream, the chain of operations is applied to that data to either emit a transformed value or perform some operations with the use of this data by subscribing to stream changes. The paradigm is provided by ReactiveX libraries, which are available in various programming languages (full list is available here). Since reactive programming operates on asynchronous data streams, to understand this approach, it might be helpful to get familiar with asynchronous programming in the Unity game engine described in my previous article.
Reactive Extensions for .NET
Reactive Extensions for .NET (Rx.NET for short) is an official and open-source implementation of Rx for C# language that brings the power of reactive programming to the .NET platform by extending the use of LINQ (Language Integrated Query) operators to asynchronous collections. In fact, it was the first implementation of reactive extensions in history. It would be our first pick, but unfortunately, it does not work with the Unity game engine and has problems with the iOS IL2CPP compiler.
UniRx - Reactive Extensions for Unity
UniRx is a reimplementation of Rx.NET designed specifically for the Unity game engine. It can be integrated into the project by using the Unity Asset Store or Unity’s package manager. If you choose the later option, you can go to the project's GitHub for more information. It can also be used smoothly with the UniTask introduced in my previous article. So as the possibility of reactive programming is now open for us, let's move to the problem example.
The problem
In this article, we will assume that a player is able to spawn fireballs in our game. To do so, they have to tap a spacebar or an UI button. Then, the fireball spawns and the cooldown is applied, so the next one will not be spawnable for a certain amount of time. We will review two solutions to do this exact thing:
- The first of them will be a classic approach of combining the
Update
function for spacebar checking and assigning a callback to a UI button. - The second approach will use reactive programming to solve the same problem.
As both solutions will require a similar setup, it will be covered briefly in this section, but the article assumes that you have basic knowledge of the Unity game engine. To keep things simple, we will leave actual spawning of fireballs as an implementation detail. So to keep going, we need an object that we will later on attach a script to. For this purpose, we can add a cube to a scene. We also have to create a canvas and a button inside of it, which will provide us with everything that we need.
Solution 1: Non-reactive
First of all, we have to create a new MonoBehaviour
and attach it to our cube. The next step will be to prepare entry points for both fireball spawn cases. The key-pressed check will be performed on every update and, for the button click, we have to prepare a separate method that will be bound through the editor. To be visible in the editor, the method has to be marked as public
.
So the MonoBehaviour
skeleton may look as follows.
public class UpdateBehaviour : MonoBehaviour
{
void Update()
{
}
public void onButtonClicked()
{
}
}
Then, in the editor, we have to select our button and, in the OnClick section reference, the onButtonClicked
method of our cube’s UpdateBehaviour
.
As we are done with the setup, now we can proceed to the implementation.
First, let’s define both cases of spawning a fireball. So in the Update
method, we can check if the spacebar is being pressed by using the Input.GetKeyDown
, provided for us by the Unity game engine. If that’s the case, we can schedule a cooldown check and fireball spawning. The UI button tap handler can call this logic directly.
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
spawnFireballIfNotCoolingDown();
}
}
public void onButtonClicked()
{
spawnFireballIfNotCoolingDown();
}
In order to satisfy the cooldown check, we have to introduce a cooldown field. It will hold time left that needs to pass before we can fire another fireball. As we start with no cooldown, we initially assign a value of 0 to the variable. We introduce another helper property to determine if the cooling down really happens at any given moment.
private float cooldown = 0.0f;
private bool isCoolingDown => cooldown > 0;
The actual cooldown check will be the implementation of the spawnFireballIfPossible
method.
It will check whether or not the cooldown has already passed and apply appropriate logic for each case.
private void spawnFireballIfNotCoolingDown()
{
if(isCoolingDown)
{
reduceCooldown();
}
else
{
applyCooldown();
spawnFireball();
}
}
So this would be an abstracted logic for spawning fireballs by utilizing the classical approach without applying reactive programming.
Solution 2: UniRx
As with the first solution, we have to prepare our MonoBehaviour
. This time however, we will setup everything in the Start
method. We will need a field that will hold a reference to our button, so that we can observe clicks that are performed on it and act accordingly. We will make the field public
so that it’s actually available in the editor and we can set up the reference there.
public class ReactiveBehaviour : MonoBehaviour
{
public Button button;
void Start()
{
}
}
After tying everything together, we can move to reactive programming itself. First of all, we will create an observable that describes our first scenario - the spacebar pressing. We can define the logic inside the Start
method. There is no possibility to react on the keystrokes itself, so we will have to do a manual check on every update. Since we will actually have two different events that we want to react to in a single subscription, we need to make sure the result of each of the observables is of the same type. To ensure that, we can map the result of each observation by using the Select
operator and change it to the Unit
instance, indicating that the result is not important.
var spaceObservable = Observable
.EveryUpdate()
.Where(_ => Input.GetKeyDown(KeyCode.Space))
.Select(_ => Unit.Default);
The second event that we want to observe is the interaction with the button. UniRx provides multiple extension methods. One of them is just what we need, because it will emit an event on every button tap, so basically it solves our problem. As previously, we have to map the result to the Unit
using the Select
method to unify the results.
var buttonObservable = button
.OnClickAsObservable()
.Select(_ => Unit.Default);
Now that we have all of the cases covered, we can implement the logic that will act on them.
To act on multiple observables, we can use the Merge
operator, which will emit an event whenever any of the observables that's being merged, emits its own one.
Our problem solution has to include a cooldown mechanic. To accomplish this, we can make use of the ThrottleFirst
operator and pass a cooldown duration as an argument to it. What the operator will do, is that it will emit only the first event out of all the events that happen in a particular time window and ignore the rest. Once the time window is complete, it will allow the emission of events again.
We have our cooldown mechanic - that’s cool. We are ready to actually subscribe to the events and spawn our fireball by passing a lambda to the Subscribe
method, so we end up with the following piece of code.
Observable.Merge(spaceObservable, buttonObservable)
.ThrottleFirst(TimeSpan.FromSeconds(3))
.Subscribe(_ => spawnFireball());
We’ve completed the part of code that does the logic. But in a reactive world, we have to remember and do one more thing. What we’ve created is a subscription that will go on and on with every emission of the event that happens in these two scenarios. We would actually like to stop this behaviour when the object gets destroyed. Every call to Subscribe
returns a cancellable that we can hold in a field or variable and call Dispose
on it, whenever we want to stop the subscription. If we had many subscriptions going on and we would like to cancel all of them, it might get really inefficient to do so separately. UniRx provides us with a CompositeDisposable
that we can add the cancellables to and keep them all together for easier management. Let’s define a field that will hold this disposable for us.
private CompositeDisposable _compositeDisposable;
As a next step, we should create an instance of it, so we can add the following code at the top of the Start
method to do so.
_compositeDisposable = new CompositeDisposable();
Now, having it set up, we can attach the cancelable to the disposable we’ve defined.
To do so, we have to add another step after our Subscribe
call, which would be calling AddTo
on the result as follows:
.AddTo(_compositeDisposable);
The actual disposal of the subscriptions in our case will happen on GameObject destruction. In the Unity game engine, we can do this by providing OnDestory
method in the script we’ve just created.
private void OnDestroy()
{
_compositeDisposable.Dispose();
}
By writing this down, we actually have finished our Rx solution to the problem we’ve previously defined.
Conclusions
As you can see, reactive programming lets us describe and combine our logic in a more event-based approach by using asynchronous streams. We’ve learned that we can bring it to the Unity game engine by integrating UniRx to the project. By the way, some of the operations done on observables can be treated like queries, so it even opens the possibility of using LINQ query syntax on them if you wish.