Actor Pooling

Intro

Actor pooling is a concept which allows us to swap in and out actors at runtime without creating and destroying them, by caching actors in a list called a pool. When a new actor of a particular type is required, if one exists in the pool we can grab that actor and use it instead of creating a new one, eliminating much of the overhead involved in creating an actor at the expense of using up some memory.

MSquared's actor pooling system is almost entirely standard, and this article will concentrate less on technical details and more on common usages together with the kinds of situations which can catch people out!

Pooled Render Target Actors

The main use of the actor pooling system is in our LOD system for characters. With up to 20k players in a level where only the nearest ~35 are displayed in full fidelity (the rest being represented with Crowd Rendering), we need a way of efficiently assigning and removing render target actors from player MAs as they run around.

Actor pooling fulfills that brief by returning a render target to the pool when a player moves into the crowd, and grabbing a render target from the pool when they move out of the crowd.

NOTE: This only applies to other clients' characters. The authoritative client's render target actor is not pooled, and cannot go into the crowd.

This example has 3 clients, with the max number in LOD0 set to 1. Of the other clients, one character will be a render target actor, and the other will be in the crowd. As you move around, and the other characters move in and out of the crowd, they will use the same BP_Origin_PlayerCharacter. Therefore, there will only ever be 2 actors, your one, and whichever other client is in LOD0.

When Actor Pooling Goes Wrong

Technical Usage

The lifecycle of pooled actors, and the relevant transition events is as follows:

  • To enable actor pooling on an actor or component, implement the IMorpheusPooledActor interface on it.

    • For the interface on the component to work, it will need to be on an actor that also implements the interface.

  • To enable actor pooling on a render target, implement the IMorpheusPooledRenderTarget interface on it.

    • This is a more specialised version of IMorpheusPooledActor, that adds the OnEndPlayToPoolWithOwner event.

And that’s it! Everything else is handled for you. The interfaces provide three functions, and a fourth for the render target one specifically:

  • OnBeginPlayFromPool() can be implemented to perform logic when an actor is taken from the pool. Logic such as making the actor visible again, or setting up the state based on the latest MorpheusActor associated with the actor is done here.

  • OnEndPlayToPool() can be implemented to perform logic when an actor is returned to the pool. Logic such as hiding the actor, or cleaning up any added visuals is done here.

  • IsPoolingEnabled() can be implemented to provide an additional conditional check for pooling. If false, the actor won't be pooled. NOTE: This interface function has no effect when implemented on a component; it will perform its begin/end logic regardless, dependent entirely on whether IsPoolingEnabled() returns true for its owning actor.

  • OnEndPlayToPoolWithOwner() on the render target version only is a version of OnEndPlayToPool() which provides a MorpheusActor owner as an argument. (This is because the association between the render target actor and its old MorpheusActor is cleaned up before it is returned to the pool)

How to Test

Naturally, each usage of actor pooling will require a different testing procedure due to it being specific to particular actors. However, a general solution for testing render targets or components on those render targets is to simulate targets moving into and out of LoD0.

We can do this by overriding the PlayerClient.Rendering.NumInLoD0 live config value in the editor (or in live config, if testing a deployment) to e.g. 1 and then having 3 clients present. This means that one client can observe as the other two run in and out of LoD0.

Characters changing LOD levels can be easily triggered via the Morpheus Inspector - see Rendering related options

"Lingering" delegates

If a render target actor has been returned to the pool, but hasn't unbound any delegates that it bound to on its previous Morpheus Actor (or any components on that Morpheus Actor), you could see strange bugs present themselves, since the pooled actor would still be listening to that old Morpheus Actor's events. If it then re-enters play out of the pool, being assigned a new Morpheus Actor, it could appear to be mostly working, but be listening to events on the old owner, instead of (or as well as) the new one.

We have added some additional warnings that will be printed if this is the case, to help track issues like this down.

We also have automatic logic, gated behind the Pooling.RemoveDelegatesBoundToOldMorpheusActor live config flag, to unbind such delegates. This should at least minimise the damage of not fully implementing actor pooling (the delegates will still need to be bound when the actor is returned to the pool though!)

An example warning if there is a delegate that hasn't been cleaned up. In this example, we automatically unbind it.

Pooling Verbose Logging

If you have verbose logging enabled for LogMorpheusActorPooling category, it will print additional details on the actor returning to the pool, comparing it to the base class's default values.

These logs will print any differences from a reference actor, which hasn't yet begun play. Some of these may not be issues, but worth considering any differences that you don't expect, relevant to components/properties you added!

This can be set by doing e.g.:

  • Using the Log LogMorpheusActorPooling Verbose command

  • Adding the LogMorpheusActorPooling=Verbose line to the [Core.Log] section of your Engine .ini file.

Last updated

Was this helpful?