This document is about: FUSION 2
SWITCH TO

Fusion Object Pooling

Level 4

Overview

Overview

The Fusion Object Pooling sample aims to showcase how a custom INetworkObjectProvider implementation can be used to pool
objects and avoid having to instantiate a new prefab for every NetworkObject spawned by the NetworkRunner.

With a custom implementation it is possible to keep track and re-use objects, making spawning and despawning more efficient.

Download

Version Release Date Download
2.0.2 Oct 21, 2024 Fusion Object Pooling 2.0.2 Build 707

INetworkObjectProvider Interface

Interface which defines the handlers for NetworkRunner Spawn() and Despawn() actions.
Passing an instance of this interface to NetworkRunner.StartGame(Fusion.StartGameArgs)
as the StartGameArgs.ObjectProvider argument value will assign that instance
as the handler for runner Spawn() and Despawn() actions.

Acquire Prefab Instance

The AcquirePrefabInstance method is called when the NetworkRunner is spawning a NetworkObject, it has a NetworkPrefabAcquireContext
and expects a NetworkObject as output result.

The default implementation behaviour is to simply Instantiate a new GameObject.

Release Instance

The ReleaseInstance method is called when the NetworkRunner is despawning a NetworkObject, the method provides
the runner reference and a NetworkObjectReleaseContext that holds some context information like the NetworkObject, the
NetworkObjectTypeId, if it is a nested object and more.

The default implementation behaviour is to simply Destroy the GameObject.

Pooling Implementation

This sample uses a custom implementation that will store previous released NetworkObjects and re-use these already instantiated prefabs when
a new spawn occur.

How to test the sample

Open and run the scene FusionObjectPooling and press the Start button, a new session will be started using AutoHostOrClient as game mode.

After connecting the host should be able to spawn 2 types of objects using the in-game buttons, a cube and a sphere object. Hit Despawn All and notice how the
objects are disabled instead of destroyed, they are now ready to be used again when a new prefab of their type is spawned.

It is possible to press the Make Pooled Object Transparent to change the behaviour of pooled object, they will now be set as kinematic and will stay transparent on the game view. This has no real use besides making it easier to see that these objects are free/released.

It is also possible to set a maximum amount of pooled object per type by changing the Max Pool Count input field, use 0 for unlimited pooled object and any positive number to limit the amount of stored object per type. For instance setting it to 10 will make any extra NetworkObject released after already having 10 free/pooled/released objects of the same type to be destroyed instead.

Complete Code Snippet

The following is a complete code snippet from this sample's Network Object Provider, but with some few specific logic removed to make it more generic and ready to be used.

C#

public class PoolObjectProvider : Fusion.Behaviour, INetworkObjectProvider
{
    /// <summary>
    /// If enabled, the provider will delay acquiring a prefab instance if the scene manager is busy.
    /// </summary>
    [InlineHelp]
    public bool DelayIfSceneManagerIsBusy = true;
    
    private Dictionary<NetworkPrefabId, Queue<NetworkObject>> _free = new Dictionary<NetworkPrefabId, Queue<NetworkObject>>();
    
    /// <summary>
    /// How many objects are going to be kept on the pools, 0 or negative means to pool all released objects.
    /// </summary>
    private int _maxPoolCount = 0;
    
    /// The base <see cref="NetworkObjectProviderDefault"/> by default simply instantiate a new game object.
    /// Let's create a method to use a custom logic that will pool objects.
    protected NetworkObject InstantiatePrefab(NetworkRunner runner, NetworkObject prefab,
        NetworkPrefabId contextPrefabId)
    {
        var result = default(NetworkObject);
        
        // Found free queue for prefab AND the queue is not empty. Return free object.
        if (_free.TryGetValue(contextPrefabId, out var freeQ))
        {
            if (freeQ.Count > 0)
            {
                result = freeQ.Dequeue();
                result.gameObject.SetActive(true);
                return result;
            }
        }
        else
        {
            _free.Add(contextPrefabId, new Queue<NetworkObject>());
        }
        
        // -- At this point a free queue was not yet created or was empty. Create new object.
        result = Instantiate(prefab);
        
        return result;
    }

    protected void DestroyPrefabInstance(NetworkRunner runner, NetworkPrefabId prefabId, NetworkObject instance)
    {
        if (_free.TryGetValue(prefabId, out var freeQ) == false)
        {
            // No free queue for this prefab. Should be destroyed.
            Destroy(instance.gameObject);
            return;
        } 
        else if (_maxPoolCount > 0 && freeQ.Count >= _maxPoolCount)
        {
            // The pool already have the max amount of object we defined. Should be destroyed.
            Destroy(instance.gameObject);
            return;
        }
        
        // Free queue found. Should cache.
        freeQ.Enqueue(instance);

        // Make objects inactive.
        instance.gameObject.SetActive(false);
    }

    public NetworkObjectAcquireResult AcquirePrefabInstance(NetworkRunner runner, in NetworkPrefabAcquireContext context,
        out NetworkObject instance)
    {
        
        instance = null;

        if (DelayIfSceneManagerIsBusy && runner.SceneManager.IsBusy) {
            return NetworkObjectAcquireResult.Retry;
        }

        NetworkObject prefab;
        try {
            prefab = runner.Prefabs.Load(context.PrefabId, isSynchronous: context.IsSynchronous);
        } catch (Exception ex) {
            Log.Error($"Failed to load prefab: {ex}");
            return NetworkObjectAcquireResult.Failed;
        }

        if (!prefab) {
            // this is ok, as long as Fusion does not require the prefab to be loaded immediately;
            // if an instance for this prefab is still needed, this method will be called again next update
            return NetworkObjectAcquireResult.Retry;
        }

        instance = InstantiatePrefab(runner, prefab, context.PrefabId);
        Assert.Check(instance);

        if (context.DontDestroyOnLoad) {
            runner.MakeDontDestroyOnLoad(instance.gameObject);
        } else {
            runner.MoveToRunnerScene(instance.gameObject);
        }

        runner.Prefabs.AddInstance(context.PrefabId);
        return NetworkObjectAcquireResult.Success;
    }

    public void ReleaseInstance(NetworkRunner runner, in NetworkObjectReleaseContext context)
    {
        var instance = context.Object;

        // Only pool prefabs.
        if (!context.IsBeingDestroyed) {
            if (context.TypeId.IsPrefab) {
                DestroyPrefabInstance(runner, context.TypeId.AsPrefabId, instance);
            }
            else
            {
                Destroy(instance.gameObject);
            }
        }

        if (context.TypeId.IsPrefab) {
            runner.Prefabs.RemoveInstance(context.TypeId.AsPrefabId);
        }
    }

    public void SetMaxPoolCount(int count)
    {
        _maxPoolCount = count;
    }
}
Back to top