INNOGAMES STORIES

Reactive programming in Unity game development.

As a game developer and especially as a mobile game developer when you build a game you have to deal with a lot of asynchronous work. In most of the modern games you either have a server which sends you responses to your requests to update game state or you have to download a lot of assets in parallel and write them to the disk or you receive messages via WebSocket because server wants to push data and update a game state as soon as possible. Even input actions such as touches and clicks are asynchronous. As a developer, you don’t want to be blocked waiting for a result as well. You want to have the result pushed to you when it is ready. That is the reason why event-driven architectures are so widely used and popular. Unfortunately, instruments which are currently being used in game development to work with events have a lot of down-sides. Especially in Unity where you only have regular callbacks and the primitive async instruments such as coroutines. That’s why it’s always worth to take a look at an alternative solutions like functional reactive programming. 

Reactive programming offers a solution and smart tools to handle a whole set of problems connected to asynchronous event managing, receiving push-messages or any propagation of a change in the game. In this article, we are going to look at one of the most popular reactive frameworks to demonstrate benefits of the reactive approach.

This Unity framework is called UniRx and it’s a port of famous Reactive Extension (ReactiveX) framework originally developed for .NET.

Reactive programming in action.

It’s pretty hard to just talk about reactive paradigm and understand it without examples so let’s try to demonstrate what is actually so appealing about the reactive approach. One of the most popular examples which demonstrates a few core concepts of Reactive approach (and also benefits of it) is an implementation of triple click. Usually in Unity when you have to implement a triple touch you will end up with callbacks, a few variables to hold time since the last click, another one to hold an amount of clicks and some if conditions to check the time between clicks. And if we want to make a proper solution lets say that all three clicks should happen within a certain period of time. First, let’s see how this problem can be solved in the regular Unity manner.

private float firstTapTimer; //time of the first click
private float secondTapTimer; //time of the second click
private float tapThreshold = 0.25f; //time threshold in which all three taps should happen
private int tapCount; //tap count in current detection attempt
 
private void Update()
{
   if (Input.GetMouseButtonDown(0))
   {
      if (tapCount == 0)
      {
         firstTapTimer = Time.time;
         tapCount++;
      }
 
      if (tapCount == 1)
      {
         if (Time.time - firstTapTimer > tapThreshold)
         {
            firstTapTimer = Time.time;
            return;
         }
         secondTapTimer = Time.time;
         tapCount++;
      }
 
      if (tapCount == 2)
      {
         if (Time.time - secondTapTimer > tapThreshold)
         {
            firstTapTimer = Time.time;
            tapCount = 1;
            return;
         }
         if (Time.time - firstTapTimer <= tapThreshold)
         {
            OnTripleTap();
         }
         else
         {
            firstTapTimer = secondTapTimer;
            secondTapTimer = Time.time;
         }
      }
   }
}

That doesn’t look that simple, right? Although the problem seems quite trivial solution looks complex and confusing. We have to handle the number of clicks, remember when they happened, check timers, handle all of these with a bunch of conditions etc. Such complexity comes from the lack of instruments to handle this sort of asynchronous events.

The reactive approach allows you to describe this in a much simpler and readable way:

var clickStream = Observable.EveryUpdate()  // creating stream (in UniRX they called Observables) form update events
  .Where(_ => Input.GetTouch(0)); // filtering to the new stream of touch events
 
clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))  // gather click events which happens within 250 milliseconds into bundles
  .Where(xs => xs.Count >= 3)  // filtering bundles with more that 3 click events into a new stream
  .Subscribe(xs => Debug.Log("Triple click detected! Count:" + xs.Count)); // listening to resulting triple click stream

Very nice! The reactive framework allows you to implement a solution to the same problem in just a couple of lines. We will come back to this implementation to explain how exactly it works, but now let’s say a couple of more words what is Reactive approach.

What does it even mean, reactive functional programming?

Reactive programming is a programming paradigm that focuses on how programs react to change. If you want to fit an explanation in just one sentence then it will be:

Reactive programming is programming with asynchronous data streams.

Doesn’t explain a lot, right? So let’s take a closer look. When some changes occur which trigger reactions in code, those reactions often result in further changes that cascade into subsequent reactions. In a sense, changes flow through our game code. And this is what the reactive programming paradigm encourages us to model. A flow of changes and reactions to these changes. But instead of treating events as discrete isolated moments, reactive programming is focused on modeling and processing streams of interdependent events.

There is nothing special about it. In the nutshell, it’s well-known Observer pattern which instead of working with one event at the time considers a sequence of events that arrive over time. Event buses or typical touch events can be perceived as asynchronous event streams, which you can observe and then do some actions. Reactive approach expands a definition what can be a stream of events, it also can be: variables, user inputs, properties, caches, data structures, push messages from socket server, etc.

And here there is a functional part of approach kicks in. It gives us a set of powerful tools to work with these data streams, we can combine, modify and change streams to produce new streams via LINQ-style operations. If you have ever worked with LINQ then you know that it allows you to query sequences and you know how much it does to simplify work with collections. The same is happening here, the reactive approach drastically simplifies work with any collections of asynchronous events.

Back to the triple click problem.

So let us get back to the triple-click problem and the reactive solution of it. 

var clickStream = Observable.EveryUpdate()  // creating stream (in UniRX they called Observables) form update events
  .Where(_ => Input.GetTouch(0)); // filtering to the new stream of touch events
 
clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))  // gather click events which happens within 250 milliseconds into bundles
  .Where(xs => xs.Count >= 3)  // filtering bundles with more that 3 click events into a new stream
  .Subscribe(xs => Debug.Log("Triple click detected! Count:" + xs.Count)); // listening to resulting triple click stream

As we mentioned before reactive framework works with streams of events. You can imagine a reactive stream as some sort of a timeline with some event of certain type marked on it. 

When you call any of operations over the stream (like Where or Buffer), it returns a new stream based on the initial stream without changing it. These give us immutability and allows to write safer code and also use all functional magic.


Another nice instrument from UniRx library called ReactiveProperty. Things which many developers end up trying to implement it’s automatic binding or update of certain fields when some other fields get updated. In UniRX this can be easily done by setting up your field as ReactiveProperty which means that this property will generate an event steam (new event generated each time when property changes) to which you can subscribe or generate an another stream out of it. In this example we will create a reactive property of player’s health and we can update all views reactively every time when health property changes:

// Reactive Notification Model
public class Player
{
  public ReactiveProperty<long> CurrentHp { get; private set; }
  public ReactiveProperty<bool> IsDead { get; private set; }
  public Player(int initialHp)
  {
    // Declarative Property
    CurrentHp = new ReactiveProperty<long>(initialHp);
    IsDead = CurrentHp.Select(x => x <= 0).ToReactiveProperty();
  }
}
 
// Subscribe to the model.
player.CurrentHp.SubscribeToText(MyText);
player.IsDead.Where(isDead => isDead == true)
.Subscribe(_ => AnimateDeath());

Neat! Now you don’t have to write update method for whole model when just one property has updated. And because our reactive property is a stream too we can you use to create new streams which we do here to create reactive property for IsDead flag.

Another handy usage of reactive stream is multi threading, you can easily make easily execute streams on different threads and wait till both of them is done by combining both streams into one:

// Observable.Start is start factory methods on specified scheduler
// default is on ThreadPool
var heavyMethod = Observable.Start(() =>
{
  // heavy method...
  System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1));
  return 10;
});
 
var heavyMethod2 = Observable.Start(() =>
{
  // another heavy method...
  System.Threading.Thread.Sleep(TimeSpan.FromSeconds(3));
  return 10;
});
 
// Join and await two other thread values
Observable.WhenAll(heavyMethod, heavyMethod2)
  .ObserveOnMainThread() // return to main thread
  .Subscribe(xs =>
  {
    // Unity can't touch GameObject from other thread
    // but use ObserveOnMainThread, you can touch GameObject naturally.
    (GameObject.Find("myGuiText")).guiText.text = xs[0] + ":" + xs[1];
  });

These examples are just the tip of the iceberg: you can apply the same operations (and many many other) on different kinds of streams like streams of API responses, UI events, network requests and responses. You can combine, filter them to create new streams in order to produce desirable results. You can find more examples in a great tutorial The introduction to Reactive Programming you’ve been missing.

Unity specific coroutines.

Usually, for async operation in Unity, we use Coroutines. Unfortunately, Coroutines are a primitive async tool and have a set of issues. For example, it’s impossible to return a value from Coroutine (because the return type of Unity Coroutines is IEnumerator) and you have to use callbacks instead. Another issue is an inability to use try-catch for Coroutines properly because yield return statements cannot be surrounded by a try-catch construction.

These issues make Coroutines hard to compose together and often you will end up with huge and tightly coupled IEnumerators. Fortunately, UniRX allows converting Coroutines to Observable streams so you can write async code in Coroutines and combine them later and use all powerful tools of reactive programming framework.

Conclusion.

Reactive programming offers a natural paradigm for dealing with sequences of events which happens over time. It raises the level of abstraction of your code so you can focus on the interdependence of events that define the game logic, rather than having to constantly deal with a large number of implementation details of handling asynchronous operations. The code in RP will likely be more concise. Event streams can be sorted, filtered, or even deferred for later processing. Beyond helping us more expressively describe when things happen in games, reactively based architectures also offer benefits in implementing game save and playback features, networking synchronisation, and powerful diagnostic tools for developers.

Of course, this approach requires some time to get used to it and slight mind shift, but even small adoption will bring clarity and will ease your work with asynchronous events. But when it really starts to shine – if your program reaches some “critical mass” of streams usage and you start combining different streams you would be amazed how easy some tasks become.