Custom Pools
The lightweight pools we provide only provides very minimal functionality. Pools in games are often quite specific to the objects being pooled, and the context in which this is being done, so it is difficult to provide a one-size-fits-all pool. However, the implementation we provide can easily be adapted to more specific use cases. Here are two examples to show how the Pool we provide is meant to be used to implement more sophisticated pools.
The pool class is not meant to be extended. Instead, it should be used as a field, with methods delegated to it as needed.
LazyPool
By default, the pool instantiates its objects when it is created. This is not always desirable, especially if the
number of objects in the pool is not known in advance. The LazyPool
class is pool implementation that grows only
as objects are needed.
using System;
namespace Gamelogic.Extensions.DocumentationCode
{
/// <summary>
/// A pool that lazily creates objects when they are requested.
/// </summary>
/// <typeparam name="T">The type of the objects to pool.</typeparam>
/// <remarks>
/// This pool adjust its capacity as objects are requested. If there are no available objects,
/// the pool will increase its capacity by one.
///
/// This class does not implement <see cref="IPool{T}"/> because it does not have a fixed capacity, and so
/// the semantics of methods like <see cref="IPool{T}.HasAvailableObject"/> are not well-defined.
/// </remarks>
public class LazyPool<T>
where T : class
{
private readonly IPool<T> pool;
/// <summary>
/// The total number of objects in the pool (active and inactive).
/// </summary>
public int Count => pool.ActiveCount;
/// <summary>
/// Initializes a new instance of the <see cref="LazyPool{T}"/> class.
/// </summary>
/// <param name="create">The function to create new objects.</param>
/// <param name="kill">The action to destroy objects.</param>
/// <param name="activate">The action to activate objects.</param>
/// <param name="deactovate">The action to deactivate objects.</param>
public LazyPool(Func<T> create, Action<T> kill, Action<T> activate, Action<T> deactovate)
=> pool = new Pool<T>(0, create, kill, activate, deactovate);
/// <summary>
/// Initializes a new instance of the <see cref="LazyPool{T}"/> using the given pool as the underlying pool.
/// </summary>
public LazyPool(IPool<T> pool) => this.pool = pool;
public T Get()
{
if (!pool.HasAvailableObject)
{
pool.IncreaseCapacity(1);
}
return pool.Get();
}
/// <inheritdoc cref="IPool{T}.Release"/>
public void Release(T obj) => pool.Release(obj);
/// <inheritdoc cref="IPool{T}.ReleaseAll"/>
public void ReleaseAll() => pool.ReleaseAll();
/// <inheritdoc cref="PoolExtensions.Clear{T}" />
public void Clear() => pool.Clear();
}
}
RobustPool
Pools support nulls, but this is needed only in unusual cases. A more robust implementation (that does not need to support null elements in the pool) should perform additional checks to prevent null elements from being added to the pool.
This version also maintains an IsActive
boolean that can be used internally and externally to present objects from
being used when they should not.
using System;
using UnityEngine.Assertions;
namespace Gamelogic.Extensions.DocumentationCode
{
/// <summary>
/// A pool that does not allow null objects, and performs additional checks on the state of objects
/// when performing operations on them to ensure integrity of the system.
/// </summary>
/// <remarks>
/// To use this pool, the objects you want to pool should implement the <see cref="IPoolObject"/> interface.
/// </remarks>
public class RobustPool : IPool<RobustPool.IPoolObject>
{
/// <summary>
/// An object managed by a pool.
/// </summary>
public interface IPoolObject
{
/// <summary>
/// Gets and sets whether the object is awake.
/// </summary>
/// <remarks>The <c>set</c> method should only be called by the pool.</remarks>
bool IsActive { get; set; }
/// <summary>
/// Activates the object.
/// </summary>
/// <remarks>
/// Implementors: Any logic that you want to execute when the object is activated should be placed here.
/// </remarks>
void Activate();
/// <summary>
/// Deactivates the object.
/// </summary>
/// <remarks>
/// Implementors: Any logic that you want to execute when the object is deactivated should be placed here.
/// </remarks>
void Deactivate();
}
private readonly IPool<IPoolObject> pool;
private readonly Func<IPoolObject> create;
private readonly Action<IPoolObject> destroy;
/// <inheritdoc cref="Capacity"/>
public int Capacity => pool.Capacity;
/// <inheritdoc cref="ActiveCount"/>
public int ActiveCount => pool.ActiveCount;
/// <inheritdoc cref="HasAvailableObject"/>
public bool HasAvailableObject => pool.HasAvailableObject;
/// <summary>
/// Initializes a new instance of the <see cref="RobustPool"/> class.
/// </summary>
/// <param name="initialCount">The initial number of objects in the pool.</param>
/// <param name="create">The function that creates new objects.</param>
/// <param name="destroy">The action that destroys objects.</param>
public RobustPool(int initialCount, Func<IPoolObject> create, Action<IPoolObject> destroy)
{
initialCount.ThrowIfNegative(nameof(initialCount));
create.ThrowIfNull(nameof(create));
destroy.ThrowIfNull(nameof(destroy));
this.create = create;
this.destroy = destroy;
// Create calls this.create, so it needs to be assigned before the pool is created.
pool = new Pool<IPoolObject>(initialCount, Create, Destroy, Activate, Deactivate);
}
/// <inheritdoc cref="IPool{T}.Get"/>
public IPoolObject Get()
{
var newObject = pool.Get();
Assert.IsNotNull(newObject);
return newObject;
}
/// <inheritdoc cref="IPool{T}.Release"/>
public void Release(IPoolObject obj)
{
obj.ThrowIfNull(nameof(obj));
pool.Release(obj);
}
/// <inheritdoc cref="IPool{T}.IncreaseCapacity"/>
public void IncreaseCapacity(int amount) => pool.IncreaseCapacity(amount);
/// <inheritdoc cref="IPool{T}.DecreaseCapacity"/>
public int DecreaseCapacity(int amount, bool deactivateFirst = false) => pool.DecreaseCapacity(amount, deactivateFirst);
/// <inheritdoc cref="IPool{T}.ReleaseAll"/>
public void ReleaseAll() => pool.ReleaseAll();
private static void Activate(IPoolObject obj)
{
Assert.IsFalse(obj.IsActive);
obj.Activate();
obj.IsActive = true;
}
private static void Deactivate(IPoolObject obj)
{
Assert.IsTrue(obj.IsActive);
obj.Deactivate();
obj.IsActive = false;
}
private IPoolObject Create()
{
var obj = create();
if(obj == null)
{
throw new InvalidOperationException("The create Func provided in the constructor returned null");
}
obj.IsActive = false;
return obj;
}
private void Destroy(IPoolObject obj)
{
Assert.IsNotNull(obj);
destroy(obj);
}
}
}
Encapsulating Pool
Sometimes you want the pool to completely encapsulate the creation and destruction of the objects that it manages.
One way to do this is to make the class of the objects in the pool internal to the pool and private, and provide a public interface that exposes the necessary functionality of the object to the rest of the game. Only the pool can create instances of the objects or destroy them.
Here is an example of how this can be done with a pool of enemies.
using UnityEngine.Assertions;
namespace Gamelogic.Extensions.DocumentationCode
{
/// <summary>
/// A pool for managing enemies.
/// </summary>
/// <remarks>
/// This is an example of how to encapsulate the creation of objects completely within a pool.
/// </remarks>
public class EnemyPool : IPool<EnemyPool.IEnemy>
{
/// <summary>
/// Represents an enemy in the game.
/// </summary>
public interface IEnemy { }
// This class is hidden from users; they cannot construct instances.
// The only way to get instances is through the pool.
private class Enemy : IEnemy
{
/// <inheritdoc cref="IEnemy"/>
public bool IsActive { get; set; }
/// <inheritdoc cref="IEnemy"/>
public void Activate() => IsActive = true;
/// <inheritdoc cref="IEnemy"/>
public void Deactivate() => IsActive = false;
}
/// <inheritdoc />
public int Capacity => pool.Capacity;
/// <inheritdoc />
public int ActiveCount => pool.ActiveCount;
/// <inheritdoc />
public bool HasAvailableObject => pool.HasAvailableObject;
private IPool<IEnemy> pool;
/// <summary>
/// Initializes an instance of <see cref="EnemyPool"/> with the given initial capacity.
/// </summary>
/// <param name="initialCapacity"></param>
public EnemyPool(int initialCapacity)
{
pool = new Pool<IEnemy>(
initialCapacity,
CreateEnemy,
DestroyEnemy,
Activate,
Deactivate);
}
/// <inheritdoc />
public IEnemy Get() => pool.Get();
/// <inheritdoc />
public void Release(IEnemy enemy) => pool.Release(enemy);
/// <inheritdoc />
public void IncreaseCapacity(int increment) => pool.IncreaseCapacity(increment);
/// <inheritdoc />
public int DecreaseCapacity(int decrement, bool deactivateFirst = false)
=> pool.DecreaseCapacity(decrement, deactivateFirst);
/// <inheritdoc />
public void ReleaseAll() => pool.ReleaseAll();
private static void Activate(IEnemy enemy)
{
var enemyImplementation = (Enemy)enemy;
Assert.IsFalse(enemyImplementation.IsActive);
enemyImplementation.Activate();
}
private static void Deactivate(IEnemy enemy)
{
var enemyImplementation = (Enemy)enemy;
Assert.IsTrue(enemyImplementation.IsActive);
enemyImplementation.Deactivate();
}
private static IEnemy CreateEnemy() => new Enemy();
private void DestroyEnemy(IEnemy enemy) { /* do nothing */ }
}
}
Self Releasing Objects
It is sometimes more convenient to call a method on the object itself to release it back to the pool. This can be done by wrapping objects in a class that has a reference to the pool.
Here is an example of this design:
using System;
namespace Gamelogic.Extensions.DocumentationCode
{
/// <summary>
/// A pool with objects that can release themselves.
/// </summary>
public class PoolOfSelfReleasingObjects<T>
{
/// <summary>
/// Represents an object that is managed by a pool.
/// </summary>
/// <remarks>
/// Why an inner class? This prevents clashes, since this namespace defines more than one pooled object.
/// </remarks>
public interface IPoolObject
{
/// <summary>
/// The value of the object.
/// </summary>
T Value { get; }
/// <summary>
/// Releases the object back to the pool.
/// </summary>
void Release();
}
private class PoolObject : IPoolObject
{
private readonly IPool<PoolObject> owner;
/// <inheritdoc />
public T Value { get; }
/// <summary>
/// Initializes a new instance of <see cref="PoolObject"/>.
/// </summary>
/// <param name="value">The value to wrap.</param>
/// <param name="owner">The pool controlling this object.</param>
public PoolObject(T value, IPool<PoolObject> owner)
{
Value = value;
this.owner = owner;
}
/// <summary>
/// Releases this object from the pool it is in.
/// </summary>
public void Release() => owner.Release(this);
}
private readonly IPool<PoolObject> pool;
/// <summary>
/// Initializes a new instance of <see cref="PoolOfSelfReleasingObjects{T}"/>.
/// </summary>
/// <param name="initialCapacity">The initial capacity of the pool.</param>
/// <param name="create">The function that creates new objects.</param>
/// <param name="destroy">The action that destroys objects.</param>
/// <param name="activate">The action that activates objects.</param>
/// <param name="deactivate">The action that deactivates objects.</param>
/// <remarks>See <see cref="IPool{T}"/> for more detail on the parameters.</remarks>
public PoolOfSelfReleasingObjects(
int initialCapacity,
Func<T> create,
Action<T> destroy,
Action<T> activate,
Action<T> deactivate)
{
pool = new Pool<PoolObject>(
initialCapacity,
() => new PoolObject(create(), pool),
po => destroy(po.Value),
po => activate(po.Value),
po => deactivate(po.Value));
}
}
}