In the previous article we have described the advantages of creational design pattern which can be extended. We will describe our approach in the following text. Every game is composed of innumerable objects which can almost always be divided into several categories. Each category is defined by common properties / behaviour of certain object type. In order to effectively change the visual representation of objects placed in the scene we have decided to design some sort of groups consisting of similar objects.

Overview

Let’s define some simple naming conventions we will use later on. Object pool collection is represented by group of object pools storing instances of similar object type and we will mark it shortly as a “collection“. Collection can contain object pools (just “pools“ for simplicity) containing only instances of type derived from collection’s object type. Every poolable object instance stored in pool will be named as an “instance“.The object requesting instances from the collection will be marked as a “consumer“.

The main reason we decided to use such a design approach was our need of possibility to change the visual representation of objects at runtime. For example in our game Clumzee: Endless Climb the player can choose from various worlds. Each world consists of objects with exactly same behaviour, but the visual representation of objects is always different. Thus we have designed our collections to handle access to required collections at runtime according to the world player has chosen to play.

Each consumer needs references to certain amount of pools with instances of similiar object types. Thus collection represents some sort of mediator between consumer and pools. Every collection can store references only to pools with instances defined by object type derived from the base type.

In the next sections of the article we will describe two types of collections we were using by now. The first one is just simple collection which can be activated / deactivated in respect of the game requirements. For example, there are different visual interpretations of the same object types used in different environments and you need to effectively switch between particular collections to ensure that each environment will be build with correct instances.

The second type is so called “ranked collection“. This collection type allows us to effectively handle a dynamic difficulty of the game. It contains pools with different instance settings, visual styles etc.. These pools may be activated / deactived according to predefined rules and / or conditions.

If there is a lot of collections and consumers this concept can be extended with some sort of collection manager. Consumers will not have direct access to particular collections, instead they will send requests to the collection manager.

Simple Object Pool Collection

Main aim of the simple collection is to provide access to objects of particular pools. Collection can contain defined amount of pool references which can be also added / removed at runtime. The overview of simple collection parts is presented by Fig. 1. Each pool of subtype x (where x = 1, 2, … , n) is implemented via description in the previous article Object Pooling in Unity.

Ranked Pool Collection

The ranked collection is derived from simple collection therefore it has all the functionality of simple collection and it has to handle activation / deactivation of ranked pool groups. When any of ranked pool groups is activated / deactivated the ranked collection itself has to update the reference list so it will not contain any reference to inactive pools. The overview diagram is presented by Fig. 2. Each rank value represents one difficulty step (e.g. based on climbed distance – in case of Clumzee: Endless Climb).

Ranked collections allow us, in various ways, to change game difficulty at runtime. Consumers can either ask for instance randomly from all collection’s active pools (regardless of the pool’s rank value) or they can specify the desired rank which will be taken into account by the collection when choosing the instance to provide. Games usually start with easier obstacles to overcome. Difficulty of the game is increased as player achieves higher and higher score.

Thus, the ranked collections became handy to achieve not even the visual variety but also variety of difficulty during the game session since ranked collection can activate / deactivate its ranked pool groups at runtime. Game designer can specify exact amount of instances for each rank what enables him to (in)directly influence difficulty change of the game just with the predefined amount of particular pools related to each rank.

Implementation

First of all, every collection is initialized immediately when the game starts. As part of the initialization process the collection initializes all pools and in case it is a ranked collection it will also deactivate all pools which rank is not allowed at the time.

After initialization the collection workflow is really simple and consists of only three steps:

  1. consumer request instance of object type
  2. collection receiving request (simple or ranked) returns the instance
  3. when instance is not needed anymore it is returned back to its particular pool

The overview implementation diagram is presented by the Fig. 3. Ranked collections can receive requests to activate / deactivate certain ranked pool groups. Actually, the consumer doesn’t need any information about different ranks in the collection they only request the instance from collection. Locking / unlocking conditions and rules are based on either of following:

  1. player’s progress in current game session
  2. overall player’s progress during the game
  3. player settings

or they can be based on all of them or many other features of the game.

Below you can find our code implementation of BasePoolCollection which is base abstract class for every collection. Each collection implements IPoolList interface. There are few notes we will disscuss so you can easily use the code below.

It is essential that every collection is derived from MonoBehaviour and BaseBehaviour class. Since BaseBehaviour is just our extension of Unity’s MonoBehaviour you can replace it without any consequences. Static class ComponentSystem is our own extension of obtaining game object components. If static methods of Unity’s class GameObject for obtaining components is sufficient in your case you can replace it. You can also use Debug class instead of our extended class Debugger. Every instance in pool has to derive from BasePoolableObject. This base class ensures that every instance implements methods handling leaving the pool and returning back to the pool. It also provides access to the prefab of the instance which it was instantiated in the initialization process from.

Unity callback methods Awake, OnEnable, OnDisable and OnDestroy are implemented to ensure the correct functionality in Unity3D environment. IPoolLost interface requires to implement following methods: InitializePoolList, GetPoolList and GetAvailablePools. Methods AddInstanceToList, RemoveInstacneFromList and CheckPrefab are used exclusively in the derived classes of BasePoolCollection. Consumers access the collection only by calling the method GetAvailablePools which returns references only to active pools of the collection.

public abstract class BasePoolCollection : BaseBehaviour, IPoolList
{
    protected List m_poolList;
    protected virtual void Awake()
    {
       InitializePoolList();
    }

    protected virtual void OnDestroy()
    {
       OnDisable();
    }

    protected virtual void OnEnable()
    {
       InitializePoolList();
    }

    protected abstract void OnDisable();

    #region IPoolList interface implementation

    public abstract void InitializePoolList ();

    /// 
    /// Retrieve all scene layout pools attached to this gameobject 
    /// and store them to protected variable m_layoutList. 
    /// Method is called in the InitializePoolList method.
    /// 
    /// 
    public virtual void GetPoolList () where T : BasePoolableObject
    {
       if (this == null)
          return;

       ObjectPool[] objectPools = ComponentSystem.GetSafeComponentsInChildren (this.gameObject);
       if(objectPools != null)
       {
          m_poolList = new List ();
          for (int i = 0; i < objectPools.Length; i++)
          {
             Transform poolablePrefab = objectPools [i].GetPrefab;
             if(CheckPrefab (poolablePrefab))
             {
                m_poolList.Add (objectPools [i]);
             } else
                Debugger.Log ("Wrong type of pool objects!!! " + poolablePrefab.name, Debugger.DebuggerLevel.Error);
          }
       } else
          Debugger.Log ("LayoutPools: There are no object pools as children of the transform!!! " + transform.name, Debugger.DebuggerLevel.Error);
    }

    #endregion

    /// 
    /// Return all available layout pools.
    /// 
    /// 
    public List GetAvailableLayoutPools ()
    {
       return m_poolList;
    }

    /// 
    /// Add current instance to the static list to handle references. 
    ///Elements in the list are accessible via static method 
    ///Get instance implemented in inherited members.
    /// Method is called in the InitializePoolList method.
    /// 
    /// 
    /// 
    /// 
    /// 
    protected List AddInstanceToList (List instanceList, T instance) where T : BasePoolCollection
    {
       List filledList = instanceList;

       if(filledList == null)
          filledList = new List ();

       if(!filledList.Contains (instance))
          filledList.Add (instance);
       return filledList;
    }

    protected List RemoveInstanceFromList (List instanceList, T instance) where T : BasePoolCollection
    { 
       List filledList = instanceList;
 
       if(filledList != null)
          filledList.Remove (instance);

       if(filledList.Count == 0)
          filledList = null;

       return filledList;
    }
 
    /// Returns true if input transform contain required 
    ///component based on the type of layout pool
    /// 
    /// 
    /// 
    protected virtual bool CheckPrefab (Transform prefab) where T : BasePoolableObject
    {
       bool isValid = false;

       ComponentSystem.GetSafeComponent (prefab.gameObject, out isValid);
       if(!isValid)
          ComponentSystem.GetSafeComponentInChildren (prefab.gameObject, out isValid);
       return isValid;
    }
}

Conclusion

The text above describes basic concept and implementation details related to our approach of object pool collections. Such design approach is easy to implement and use at the same time. But be carefull about dividing the object types into similar categories since this is essential assumption of proper collection implementation. Such an approach simplifies accessibility for procedural generation algorithm because each part of the generator has access only to the collections which are necessary for its proper functionality.

Share This