ReactiveX and Unity3D: part 1

Tightly-coupled code is a headache. There must be a natural force, like gravity, that drags your code into interwoven, unreadable, fragile messes as you write it. And for some reason in game development this force is doubly strong! Unless you consciously resist it, your game will eventually hit critical mass and collapse into a black hole. (Why yes, I have been reading a lot of science fiction lately.)

But I'm not here to define the problem, nor convince you of the need to solve it. Instead I'm going straight to what you can do about it.

Good code has a lot to do with the tools we use (at risk of repeating myself). In game dev, it's common to reach for tools like Entity Component Systems to control coupling. My goal is to familiarize you with another tool which (I think) is less commonly known in game development (so far): ReactiveX.

The tool

At a basic level, it's fair to say that using ReactiveX is a lot like event handling. Well, maybe like event handling with a turbo-charger. Instead of just firing events and handling them somewhere else, we get to treat sequences of events like first-class citizens. They even get a proper name: IObservable<T>. And we can morph them, delay them, filter them, combine and customize them in so many ways that they become far more powerful than that pedestrian old event handling you may be used to. (The ReactiveX folks can provide a more thorough intro if you like.)

I can sense you're skeptical. Isn't all this functional reactive mumbo-jumbo for web developers and big-data wonks?

I understand! A programmer's initial state is set to Missouri (the Show-Me State). You want to see the rubber hit the road! So as a demonstration, let's re-create Unity3D's Standard Assets FirstPersonController.

The task

The Standard Assets package is a nice way to start prototyping in Unity, and it provides a neat use-case. Its first-person controller is a bundle of scripts and game objects which provide the following features which are common to first-person games:

  1. Walk forwards, backwards, and strafe left and right with the WASD keys.
  2. Look around (camera pan and tilt) with the mouse.
  3. Basic collision physics, so you can't walk through walls or fall through the floor.
  4. Run by holding the Shift key.
  5. Camera bob: an effect which emulates walking by bouncing the camera up and down slightly.
  6. Jump with the Space key.
  7. Sound effects for footsteps, jumping, and landing.

It's not necessary for you to be familiar with Unity's FirstPersonController code. We won't be making a 100% perfect functional copy, but we'll get close. In the end, I think you may agree the Observables approach is more succinct, easier to understand, and easier to modify.

So let's get started! For brevity, I'll assume you have a basic familiarity with Unity. This series will be broken up into three parts, and we begin with the two most basic features: movement and mouse-look.

When all is said and done, this is the result:

Setup

After creating a Unity project, we import two packages from the Asset Store: Standard Assets, for some graphics and sound effects, and UniRx [documentation on GitHub], which provides the ReactiveX framework and some extra bells-and-whistles especially for Unity.

Now we add to the scene some lighting, a Quad for the floor, a Cube as an obstacle, a GameObject with a child Camera for the player, and a GameObject I'll call "GameController", just as a convenient place to put global scripts. It looks a bit like this:

Unity Editor screenshot.

I also placed a CharacterController component on the Player. This is a built-in component which simplifies movement and collision handling for input-driven characters. You could achieve the same thing with a capsule collider, a kinematic rigidbody, and some extra code, but we won't don't need to bother with that.

These boots are made for walking

Let's get our player moving. The classic way of doing that is with a script like this:

public class ClassicPlayerController : MonoBehaviour {
  private void Update() {
    // Read keyboard inputs
    // Translate into movement velocity
    // Apply that velocity to the character
  }
}

Already we've started tightly-coupling our code. Why should our player controller have to know how to read input? What if we want to change the way input is provided? Maybe from a dance pad, or a VR headset, or commands from Twitch, or some kind of sentient AI. We shouldn't need to write a new player controller for each of these things. Even if you know your game will always be driven by a keyboard, unnecessary coupling should be avoided and it's best to start now.

Instead, think of the movement input as a signal: you press the W key and the signal says "go forward". Our controller doesn't know how the signal was made, simply what to do when it sees the signal. Signals are Observables. In case of movement (a 2D direction), it might be an IObservable<Vector2>. Ultimately the player controller will subscribe to the Observable, creating an Observer which will be notified every time there's a new Vector2 to deal with.

So let's separate concerns by making a script just for input.

public class Inputs : MonoBehaviour {

  public IObservable Movement { get; private set; }

  private void Awake() {
    Movement = this.FixedUpdateAsObservable()
      .Select(_ => {
        var x = Input.GetAxis("Horizontal");
        var y = Input.GetAxis("Vertical");
        return new Vector2(x, y).normalized;
      });
  }
}

First we declare the movement signal as a public property (that only we can set) and initialize it during Awake. One of the best things about Observables is that you can't inject values from the outside. All you can do after creating one is subscribe to get values out of it. This may sound frustrating but it's actually a very powerful property that lends itself to nicely decoupled code. The most common way to create an Observable is by transforming an existing one. And "transform" is used loosely here, because the original Observable still remains—they're immutable—we only create a new one.

When do we want to read input? Every Fixed Update (not Update, because we know movement will involve the physics system). UniRx provides an Observable that "ticks" on Fixed Update: which you access by calling this.FixedUpdateAsObservable(). This gives us an IObservable<Unit>. Without going into too much detail, Unit tells us the signal doesn't have a data payload. The fact that this signal fires at all is the signal. On this we call the special method Select which creates a new Observable that for each input value, outputs a new value according to the function we pass in. The function reads the walk inputs and outputs them as a normalized vector. So Movement is now an Observable which, every Fixed Update, outputs a vector representing the keyboard input.

How do we use it? In another script, we subscribe to this signal and apply movement to the character.

public class PlayerController : MonoBehaviour {
  // ... fields omitted ...

  private void Start() {
    inputs.Movement
      .Where(v => v != Vector2.zero)
      .Subscribe(inputMovement => {
        var inputVelocity = inputMovement * walkSpeed;

        var playerVelocity =
          inputVelocity.x * transform.right +
          inputVelocity.y * transform.forward;

        var distance = playerVelocity * Time.fixedDeltaTime;
        character.Move(distance);
      }).AddTo(this);
  }
}

First, notice that we used Awake to set up the signal, and now Start to subscribe. This is to avoid initialization order issues and is a pattern I follow generally. (Awake for my Observables; Start for yours.)

After getting a reference to the Movement Observable, we have a neat little optimization: the call to Where. Where is another transformation function: in this case we ignore movement vectors that are zero. If the user isn't pressing any keys, we get to break early.

Then we subscribe by providing a function to be called every time there's a new value from the Observable. In the function we simply multiply by the player's walking speed, translate the 2D input into the player's 3D coordinate system, multiply by time to get distance, and use the CharacterController to apply the movement.

Finally, a UniRx detail, we call AddTo(this). It's important to know that Observables and subscriptions (Observers) are living things. As long as the signal continues, they'll all happily keep processing. (Observables can complete or produce an error, but that's outside our scope at the moment.) In order to not leak memory and waste processing power, we need to make sure to clean up when game objects get destroyed. That's what AddTo(this) does for us. Basically when PlayerController's game object is destroyed, it will dispose of the subscription. Note that this will not dispose of the underlying Observable, Movement. That will be disposed when the Inputs game object is destroyed, since Movement started from that object's Fixed Update Observable.

And that's all you need for basic WASD movement. Not so painful right?

Look around you, just look around you

All right, we've got the basic idea; mouse-look is more of the same. The mouse input is just another signal, again a 2D vector. The player controller translates that into rotations and applies pan (left-and-right) to the Player game object and tilt (up-and-down) to the Camera object.

Rather than walk through it step by step, I'll post the final code listing. Notice that we do not alter the movement subscription, but rather start a new subscription which deals only with mouse input. This, again, is decoupling in action. There's no reason for the movement code to depend on the rotation code (at least not for these simple requirements). They could even be in separate scripts if that were more convenient.

You'll find the complete code for Part 1 on GitHub Gist.

Everything else is implementation detail. Clamping the up/down rotation so we can't flip head-over-heels. Locking the cursor into the screen and hiding it. Treating Inputs as a singleton (which is not a sin in the land of immutability). Tweaking input axis settings in the editor, those sorts of things. Remember to check out the video above to see it in action.

And that's it for Part 1! Hopefully it's a gentle-enough introduction to the topic of Observables with a glimpse of how to use them in practice. Next we'll get a little more sophisticated, handling different inputs and adding some special effects.