ReactiveX and Unity3D: part 2

This is the second part of a three-part series in which we re-create the Unity Standard Assets first-person controller using ReactiveX Observables.

We've got walking and mouse-look done, now let's add running and camera bob. In this article we'll start to see some of the payoff for the work we did in Part 1. Here's what we'll have by the end of this part:

My little runaway

"Run by holding Shift" is practically standard practice in first-person games, so we want to support that feature. There are many ways to accomplish this using Observables, of course, but I think it's a great opportunity to introduce Reactive Properties. This is a special feature added to UniRx that's not in ReactiveX proper. Reactive Properties give us the best of both worlds: the flexibility of plain-old properties and the power of Observables. You can set and get the value from a Reactive Property just like a property, but you can also subscribe to it to get update notifications. The run input is a great candidate for this, because I don't need to react exactly when the user presses or releases the key; I just want to know if the key is pressed while I'm calculating movement. So let's add this code to our Inputs script (I'll omit the lines we've already seen):

public ReadOnlyReactiveProperty<bool> Run { get; private set; }
// ...

private void Awake() {
  // ...
  Run = this.UpdateAsObservable()
    .Select(_ => Input.GetButton("Fire3"))
    .ToReadOnlyReactiveProperty();
}

First off, I'm using a ReadOnlyReactiveProperty. If I use the plain ReactiveProperty, anyone with access to it can set its value. It's best practice to limit write-access whenever possible, again, in the interest of de-coupling your code. And anyway, we don't need to set the value of Run ourselves because we can modify another Observable to produce the value for us! And that's what we do in Awake: every Update, select the state of the button "Fire3", and convert that into our property. ("Fire3" is an input axis defined by default in a new Unity project, conveniently mapped to the Shift key.)

Using our new input is simple. After adding a runSpeed field to our PlayerController, we poll the value of Run whenever we're calculating movement to pick the appropriate speed.

// In PlayerController.Start()
inputs.Movement
  .Where(v => v != Vector2.zero)
  .Subscribe(inputMovement => {
    var inputVelocity = inputMovement *
      (inputs.Run.Value ? runSpeed : walkSpeed);
    // ... etc.

This is probably the simplest way to implement this feature. But is it perfect? There's a bit of subtlety here which is the price paid for using Observables (or any asynchronous code): we basically can't guarantee execution order except by making signals directly dependent on each other. In other words: I may not know for certain that Run has been updated by the time I'm accessing its value. With this code, I should be prepared that the value of Run from the perspective of the movement calculation may lag behind by one frame. Now there are ways to guarantee proper execution order when it's needed (and we'll see that in Part 3), but it complicates the code a little bit. Ask yourself if the cost is worth it. Does it matter if we start running one frame late? Probably not. But perhaps in your game it will. This is, of course, your job to figure out. For now, we're going to stick with the simpler approach.

Bob and un-weave

Implementing camera bob will be a bit more involved, but we start to see some of the promise of Observables to nicely decouple our code. We want to bounce the camera up and down slightly to emulate walking, and to do so we need to know the distance the player walks (or runs) each frame. In Standard Assets, this is implemented by having the player controller directly update a class responsible for the camera bob effect. Naturally this couples our player controller code to the camera bob code, which we want to avoid. Instead we're going to use Observables to form the interface between these two classes. And it starts with an actual interface (okay, it's actually an abstract class, but only for easy compatibility with Unity's inspector).

public abstract class PlayerSignals : MonoBehaviour {
  public abstract float StrideLength { get; }
  public abstract IObservable<Vector3> Walked { get; }
}

Our PlayerController will extend and implement this interface so that the camera bob script doesn't need a direct dependency on it. StrideLength will be a simple configurable field, but how do we produce the Walked Observable?

Unity's CharacterController component actually calculates this value for us (since this is the actual distance moved after accounting for wall collisions, etc.), we just have to export the value. Consider the movement code:

inputs.Movement
  .Where(v => v != Vector2.zero)
  .Subscribe(inputMovement => {
    // ...
    var distance = playerVelocity * Time.fixedDeltaTime;
    character.Move(distance);
    var distanceActuallyWalked =
      character.velocity * Time.fixedDeltaTime;
}).AddTo(this);

We want to put distanceActuallyWalked into an Observable. But I've already said you can't just inject values into an Observable from the outside, right?

One option is to introduce another concept: the Subject. A Subject in ReactiveX combines the Observer and Observable interfaces, but you can think of it as essentially a read-write Observable. How is that different from a Reactive Property? Well a Subject doesn't have the concept of a "current value" the way a Reactive Property does. You can subscribe to a Subject to be notified when it changes, but you can't just ask for the current value. So you could think of Subjects as just a little less powerful than Reactive Properties, and you should prefer the least-powerful option that will work in any situation. In practice you'll make this decision based on how you intend your signals to be used down the line.

So we add a Subject<Vector3> to our PlayerController script as a field. Because this is "our" signal, we need to initialize it in Awake.

walked = new Subject<Vector3>().AddTo(this);

This is a new signal so we need to constrain its lifecycle to our game object with AddTo(this).

And now we can replace that distanceActuallyWalked line above with this:

walked.OnNext(character.velocity * Time.fixedDeltaTime);

The OnNext method is used to provide a new value to the signal. Everyone who is subscribed to the signal will get a notification with the new value.

We haven't even reached the camera bob portion yet, but I think we've done something remarkable. Our script not only "consumes" signals, it also produces them. This is the fundamental glue that lets you integrate systems that aren't based on Observables, like Unity's physics and scene graph.

There's one last bit of book-keeping. So far I've been very careful to limit who can write values to things. If we make our Subject public, anyone could potentially write values there and break our code. (Odds are it would be you at 3 AM the night before launch day.) Not to worry: since a Subject is an Observable, we simply restrict the public visibility of our fields by defining them like this:

private Subject<Vector3> walked;
public override IObservable<Vector3> Walked {
  get { return walked;  }
}

Now we see a Subject and everyone else sees an IObservable. Nice and clean!

Okay, onto the actual camera bob script! This script will sit on the Camera game object, and control its motion. We'll subscribe to the Walked signal and accumulate the distance modulo the player's stride length. We'll use that to evaluate a sine curve defined as a Unity AnimationCurve which will finally give us the current adjustment to the camera's position.

public class CameraBob : MonoBehaviour {
  // IPlayerSignals reference configured in the Unity Inspector, since we can
  // reasonably expect these game objects to be in the same hierarchy
  public PlayerSignals player;
  public float walkBobMagnitude = 0.05f;
  public float runBobMagnitude = 0.10f;

  public AnimationCurve bob;

  private Camera view;
  private Vector3 initialPosition;

  private void Awake() {
    view = GetComponent<Camera>();
    initialPosition = view.transform.localPosition;
  }

  private void Start() {
    var distance = 0f;
    player.Walked.Subscribe(w => {
      // Accumulate distance walked (modulo stride length).
      distance += w.magnitude;
      distance %= player.StrideLength;
      // Use distance to evaluate the bob curve.
      var magnitude = InputsV2.Instance.Run.Value ? runBobMagnitude : walkBobMagnitude;
      var deltaPos = magnitude * bob.Evaluate(distance / player.StrideLength) * Vector3.up;
      // Adjust camera position.
      view.transform.localPosition = initialPosition + deltaPos;
    }).AddTo(this);
  }
}

Note that the distance variable is state necessary for and internal to the subscription. Here we've incorporated that state via closure. Previously we've incorporated state via class fields (e.g., the CharacterController instance).

To heighten the perceived realism, we also increase the magnitude of the bob based on whether or not the player is running. An example of how clean it can be to re-use signals: we weren't thinking about camera bob when we made Run, after all.

So we've accomplished Part 2! We added running and a camera special effect all without needlessly convoluting our code. We could trivially remove camera bob if the game requirements changed, because it's not closely coupled to anything. Stay tuned for Part 3 where we'll add jumping and a few more effects.

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