Ramblings of General Geekery

UE5 Gameplay Cameras: Node Evaluators

This is the fifth article in my GPC developer diary. Gameplay Cameras (GPC) is a camera system for UE5 that started shipping with the engine as part of version 5.5, but is still in its “experimental” phase. We plan to have it in “beta” with 5.6, and “production ready” with 5.7 or 5.8 depending on how things go.

There was no article last week since it was Family Day here in British Columbia, but this week I’ll go back to one of the very first decisions I had to make, which was the basic design for camera nodes.

This is the second time I build this camera system (originally designed with Thomas Roy of Bioware, for EA’s Frostbite engine), but I didn’t want to make any assumptions about how to (re)implement it inside Unreal. Different engines have different designs, philosophies, and infrastructure after all. So I built three prototypes:

  1. An “instantiated UObject” design.
  2. A “stateless evaluation” design.
  3. An “evaluator tree” design.

Instantiated UObject Design

The first design used the same approach as several other systems in the engine, such as the actor-component framework. The idea is that each camera node would be a UObject that combines data, logic, and state. So just like with an AActor or UActorComponent, a UCameraNode would have:

  • Data: the properties for the node, exposed for the user to tweak. For instance, an offset node has an offset vector property, and a field of view node has an angle value property.
  • Logic: this would be a good ol’ virtual Update() method of sorts, in which the node executes its purpose. For instance, the offset node moves the camera, the boom arm node rotates its, and so on.
  • State: this is information specific to this instance of the node. For instance, a position damping node would keep the previous dampened and undampened positions, along with the current state of its mass/spring system.

Because of the last point, “state”, this requires instantiating these camera nodes, in the same way that actors and components are spawned. You may tweak the properties of a specific Actor Blueprint (its “data”), but then you can spawn a dozen of them, and each will maintain its own state.

In this design, the camera rig that the user creates and edits is a sort of “template” which is then basically duplicated when it’s time to run it. Again, that’s what actors and components do — spawning an Actor Blueprint is pretty much about cloning it.

The advantage of this design is that it’s familiar for UE developers, and very straightforward. You make a subclass of the UCameraNode class, add whatever properties you need, and implement the Update() method.

The big disadvantage is that performance isn’t ideal. In 95% of cases, instantiating a camera rig made up of a dozen camera nodes is quite fast and everything is great… but in 5% of cases, there’s a weird spike. Somehow, the UObject cloning stalls and the time spent creating that camera rig goes up to between 0.2 and 0.4 milliseconds. The frequency of these spikes can be lowered by using object pooling (i.e. re-using an identical, previously instantiated camera node hierarchy that was discarded), and I implemented that in my prototype, but it certainly doesn’t make it disappear since you can have cache misses.

Most of the games I’ve worked on don’t want their cameras to take more than about 0.3 milliseconds, and they get upset when it goes past 0.5 milliseconds. Given that 80% of the camera system’s update is generally physics queries (collision tests, raycasts, etc.), you can see how that kind of spike is making me very nervous.

Another severe disadvantage is that live-editing becomes cumbersome to implement. Tweaking values in the editor while PIE is running means that modifications on the “template” objects must be reflected on the “instantiated” objects. This can be done (and I did it for this prototype) but this was a second aspect of the design that I felt wasn’t ideal.

Stateless Evaluation Design

Next was the “stateless evaluation” design. The idea was to take the first design, but move the third aspect, “state”, away from the UCameraNode. This is the sort of design adopted by State Tree, for instance.

With this design, the virtual Update() method on the UCameraNode class becomes a const method. It can read the properties of the node (such as the offset vector, field of view angle, or damping factor), but any editable state it needs to keep across frames goes into an externally provided structure (such as a structure with the mass/spring system’s state for the position damping node).

The advantage of this design is that, well, it works and fits most of my requirements. Live editing works out of the box because the original data objects can be used directly without duplication. You’re in control of how and where all the state structures are allocated, so you can optimize cache coherency for a given camera rig’s update.

The disadvantage, compared to the “instantiated UObject” design, is of course that some boilerplate code is required. You need some code to declare what kind of state structure you want, and you need to cast whatever base state pointer you’re given in Update() down to your specific state. Of course, you can reduce that overhead quite a lot with templates and macros, but it’s an overhead nonetheless compared to, well, no overhead at all.

I ultimately opted against this design, but it was a close call with the design I adopted, which is presented below. I can totally imagine someone else going with this stateless design, and that would probably work about as well as the evaluator tree design. For what it’s worth, my reasoning boiled down to the following arguments, some of which involving a lot of subjectivity:

  1. I personally found it awkward to code with an external state structure. As I expanded my prototype to slightly more complicated nodes, there was a lot of State.Foo and State.Bar everywhere. You could avoid some of that by making the node’s Update() method call an Update() method on the state itself but that felt like a cop-out.
  2. In my humble opinion, the stateless method approach works well when you use more data from the node (i.e. from this) than from the external state structure. But with dynamic parameters and nodes with complicated state, there’s actually little data read from the UCameraNode object past initialization. So with that in mind, I started wondering why would the Update() method be on that object?
  3. There were a couple of edge cases I needed to handle (see below) where the UCameraNode instance could be unloaded.

Evaluator Tree Design

The third design is the one I used the last time I implemented this camera system. One could say that even though I did my due diligence testing a couple of other designs, I may have been biased towards that one…

The “evaluator tree” design is popular in animation, AI, and rendering, among others. The general idea is that your main objects create “evaluator” objects, and those are the ones that actually do the work. If you’re old enough to remember Windows 95, you might know that this design is to blame for robbing us of one of our childhood’s best games: painting over frozen windows!

In this design, the UCameraNode objects create FCameraNodeEvaluator objects which contain both “logic” and “state”. The UCameraNode object is only there for the “data”, and accessed through a const pointer by the evaluator.

Just like the previous stateless design, live editing just works. You’re also again in control of how and where the evaluators are allocated, so you can also pack them nicely and optimize for cache coherency. And while I complained that the stateless design has lots of code snippets like State.Foo, in theory you trade it here with lots of ConstNodeData->Foo snippets… but as I said before, most of the node’s data is in practice cached as state anyway for handling dynamic parameters.

In the end, I liked the code better in this case, and that’s how GPC works.

Other Considerations

There were a couple of secondary considerations when I picked the “evaluator tree” design:

  • You can in theory run camera logic without any data. That is: entirely programmatic logic that doesn’t expose any UCameraNode to the user. To be honest, I haven’t done a pass for that yet, so I’m sure there would be some crashes if an evaluator had no underlying camera node, but I intend to make it possible eventually.
  • The UCameraNode objects, being data, belong to pack files and can, therefore, be streamed in and out. For instance, a camera rig defined inside a DLC weapon’s data, or a world camera defined in a specific part of a dungeon, may stream in and out of memory as the player changes their equipment load-out or fast-travels back to their homebase. The camera system may have to handle these camera nodes being yanked from under it, and still somehow gracefully transition to whatever’s next. So whatever camera logic is being run is better not being tied to the data classes. This may seem like an edge case (and it is!) but I’ve had to deal with it in the past.

Well, there you go, this is why GPC uses node evaluators!