Leveraging object pooling

Topics covered

Object pooling | CoherenceSyncConfigRegistry | CoherenceSyncConfig

Network entities need to be created and removed all the time. This can be due to entities getting in and out of a LiveQuery, or simply because gameplay requires so. If that is the case, we can leverage coherence's object pooling system in order to avoid costly calls to Instantiate and Destroy, which are famously expensive operations in Unity.

Our use case

In this project we use pooling for one very clear use case: the tree logs that get spawned when chopping down a tree.

This was a natural choice as players will be chopping trees all the time, but we can also assume that they will burn the logs on the fire almost as often. So by pre-allocating a pool of around 10 logs, we should be covered in most cases.

Prefab setup

To set up the log to behave like this, all we did was to set that option on the log's own CoherenceSync inspector.

Check the Log prefab in 📁Prefabs/Interactive/Burnable/:

A pool configured like this means that coherence will pre-spawn 10 instances of the Prefab at the beginning of the game.

However if we were to need more, we could request more instances and they would be created and added to the pool. The game can even go above 20. If that were to happen, any instance released beyond 20 wouldn't just be returned to the pool, but would be destroyed.

In other words, 10 and 20 represent the lower and upper limit for the amount of memory we are reserving for the logs alone in our game. We are considering anything above 20 as a temporary exception.

When we press Play, coherence instantiates these 10 logs, deactivate them, and put the pool in the DontDestroyOnLoad scene:

Because they are inactive, their CoherenceSync components are not syncing any value.

Spawning a new log

To spawn a new log we only need to call one line of code. However, we don't provide a reference to a regular Prefab like we would with Instantiate. We instead leverage the CoherenceSyncConfig object that represents the log.

This CoherenceSyncConfig contains all the info that coherence needs to handle this particular Prefab over the network. If we inspect it, we will notice that it contains in fact how the object is loaded (Load via) and how it's instantiated (Instantiate via).

You can notice how this is the same info we saw while configuring the CoherenceSync before.

This Sync Config can be found in the 📁coherence/ folder, and is a sub-object of another ScriptableObject: the CoherenceSyncConfigRegistry.

Now that we have a reference to it, we can spawn the log with one line of code. In the ChoppableTree script, we do something like:

CoherenceSync newLog =
    logSyncConfig.GetInstance(transform.position + [...], Quaternion.identity);

This line looks remarkably similar to Unity's own Instantiate in its syntax. The difference is that it gives us back a reference to the CoherenceSync attached to the log instance that will be enabled. From this, we can do all sorts of setup operations by just fetching other components with GetComponent, to prepare the instance.

Burning a log

When we are done with it (in this case, when it's thrown into the campfire), we can dispose of it:

_sync.ReleaseInstance();

(this line is in the Burnable.cs class, inside the GetBurned() method)

The instance is then automatically returned into the pool, and disabled.

Cleaning up

When taking an instance out of the pool or when returning it, coherence doesn't automatically do any particular clean up to its state.

As such, when we reuse a pool instance, it is good practice to think of what values should be reset that might have been messed up by previous usage. We should think about what happens during gameplay, and use OnEnable / OnDisable as needed to ensure that disabled instances are put in a state that makes them ready to be used again.

For this project, since an object can be burned while being carried, we do some cleaning in the OnDisable of the Grabbable.cs class to prepare the wood logs for another round, like so:

private void OnDisable()
{
    isBeingCarried = false;
    _rigidbody.isKinematic = false;
    _collider.enabled = true;
    
    // ... in addition to removing any listener set in OnEnable
}