Reactive programming in Gamedev. Let's understand the approach on Unity development examples
Everything you wanted to know about reactive programming with examples
Hello everyone. Today I would like to touch on such a topic as reactive programming when creating your games on Unity. In this article we will touch upon data streams and data manipulation, as well as the reasons why you should look into reactive programming.
So here we go.
What is reactive programming?
Reactive programming is a particular approach to writing your code that is tied to event and data streams, allowing you to simply synchronize with whatever changes as your code runs.
Let's consider a simple example of how reactive programming works in contrast to the imperative approach:
As shown in the example above, if we change the value of B after we have entered A = B + C, then after the change, the value of A will also change, although this will not happen in the imperative approach. A great example that works reactively is Excel's basic formulas, if you change the value of a cell, the other cells in which you applied the formula will also change - essentially every cell there is a Reactive Field.
So, let's label why we need the reactive values of the variables:
When we need automatic synchronization with the value of a variable;
When we want to update the data display on the fly (for example, when we change a model in MVC, we will automatically substitute the new value into the View);
When we want to catch something only when it changes, rather than checking values manually;
When we need to filter some things at reactive reactions (for example LINQ);
When we need to control observables inside reactive fields;
It is possible to distinguish the main approaches to writing games in which Reactive Programming will be applied:
It is possible to bridge the paradigms of reactive and imperative programming. In such a connection, imperative programs could work on reactive data structures (Mostly Used in MVC).
Object-Oriented Reactive Programming. Is a combination of an object-oriented approach with a reactive approach. The most natural way to do this is that instead of methods and fields, objects have reactions that automatically recalculate values, and other reactions depend on changes in those values.
Functional-reactive programming. Basically works well in a variability bundle (e.g. we tell variable B to be 2 until C becomes 3, then B can behave like A).
Asynchronous Streams
Reactive programming is programming with asynchronous data streams. But you may object - after all, there is Event Bus or any other event container, which is inherently an asynchronous data stream too. Yes, however Reactivity is similar ideas taken to the absolute. Because we can create data streams not only from events, but anything else you can imagine - variables, user input, properties, caches, structures, and more. In the same way you can imagine a feed in any social media - you watch a stream and can react to it in any way, filter and delete it.
And since streams are a very important part of the reactive approach, let's explore what they are:
A stream is a sequence of events ordered by time. It can throw three types of data: a value (of a particular type), an error, or a completion signal. A completion signal is propagated when we stop receiving events (for example, the propagator of this event has been destroyed).
We capture these events asynchronously by specifying one function to be called when a value is thrown, another for errors, and a third to handle the completion signal. In some cases, we can omit the last two and focus on declaring a function to intercept the values. Listening to a stream is called subscribing. The functions we declare are called observers. The stream is the object of our observations (observable).
For Example, let's look at Simple Reactive Field:
private IReactiveField<float> myField = new ReactiveField<float>();
private void DoSomeStaff() {
var result = myField.OnUpdate(newValue => {
// Do something with new value
}).OnError(error => {
// Do Something with Error
}).OnComplete(()=> {
// Do Something on Complete Stream
});
}
Reactive Data stream processing and filtering in Theory
One huge advantage of the approach is the partitioning, grouping and filtering of events in the stream. Most off-the-shelf Reactive Extensions solutions already include all of this functionality.
We will, however, look at how this can work as an example of dealing damage to a player:
And let's immediately convert this into some abstract code:
private IReactiveField<float> myField = new ReactiveField<float>();
private void DoSomeStaff() {
var observable = myField.OnValueChangedAsObservable();
observable.Where(x > 0).Subscribe(newValue => {
// Filtred Value
});
}
As you can see in the example above, we can filter our values so that we can then use them as we need. Let's visualize this as an MVP solution with a player interface update:
// Player Model
public class PlayerModel {
// Create Health Reactive Field with 150 points at initialization
public IReactiveField<long> Health = new ReactiveField<long>(150);
}
// Player UI View
public class PlayerUI : MonoBehaviour {
[Header("UI Screens")]
[SerializeField] private Canvas HUDView;
[SerializeField] private Canvas RestartView;
[Header("HUD References")]
[SerializeField] private TextMeshProUGUI HealthBar;
// Change Health
public void ChangeHealth(long newHealth) {
HealthBar.SetText($"{newHealth.ToString("N0")} HP");
}
// Show Restart Screen
public void ShowRestartScreen() {
HUDView.enabled = false;
RestartView.enabled = true;
}
public void ShowHUDScreen() {
HUDView.enabled = true;
RestartView.enabled = false;
}
}
// Player Presenter
public class PlayerPresenter {
// Our View and Model
private PlayerModel currentModel;
private PlayerView currentView;
// Player Presenter Constructor
public PlayerPresenter(PlayerView view, PlayerModel model = null){
currentModel = model ?? new PlayerModel();
currentView = view;
BindUpdates();
currentView.ShowHUDScreen();
currentView.ChangeHealth(currentModel.Health.Value);
}
// Bind Our Model Updates
private void BindUpdates() {
var observable = currentModel.Health.OnValueChangedAsObservable();
// When Health > 0
observable.Where(x > 0).Subscribe(newValue => {
currentView.ChangeHealth(newValue);
});
// When Health <= 0
observable.Where(x <= 0).Subscribe(newValue => {
// We Are Dead
RestartGame();
});
}
// Take Health Effect
public void TakeHealthEffect(int amount) {
// Update Our Reactive Field
currentModel.Health.Value += amount;
}
private void RestartGame() {
currentView.ShowRestartScreen();
}
}
Reactive Programming in Unity
You can certainly use both ready-made libraries to get started with the reactive approach and write your own solutions. However, I recommend to take a look at a popular solution proven over the years - UniRX.
UniRx (Reactive Extensions for Unity) is a reimplementation of the .NET Reactive Extensions. The Official Rx implementation is great but doesn't work on Unity and has issues with iOS IL2CPP compatibility. This library fixes those issues and adds some specific utilities for Unity. Supported platforms are PC/Mac/Android/iOS/WebGL/WindowsStore/etc and the library.
So, you can see that the UniRX implementation is similar to the abstract code we saw earlier. If you have ever worked with LINQ - it will be easy enough for you to understand the syntax:
var clickStream = Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonDown(0));
clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))
.Where(xs => xs.Count >= 2)
.Subscribe(xs => Debug.Log("DoubleClick Detected! Count:" + xs.Count));
In conclusion
So, I hope my article helped you a little bit to understand what reactive programming is and why you need it. In game development it can help you a lot to make your life easier.
I will be glad to receive your comments and remarks. Thanks for reading!