3 min read

Implementing Warrior Pool Service

Minifigure soldiers macro shot
Photo by Jaime Spaniol / Unsplash

Content

What is Object Pooling?

Instantiating and destroying objects are heavy on memory allocation and deallocation. Doing that repeatedly is definitely worse. Ideally, initialization and destruction of game objects must be done only at the beginning and at the end of the game. While the game is running, it is advisable to only enable or disable an object depending if they are currently in use or not. Object pooling basically allows us to do that.

Initial Implementation

The core mechanic of the game is whenever you select an answer, a warrior will spawn in front of a base: yours if you answered correctly; on enemy base otherwise. My straightforward solution is to generate the pool of warrior objects and use each base as containers.

public class BaseController : MonoBehaviour
{
	private const int WARRIOR_POOL_CAPACITY = 5;
    
    [SerializeField] private WarriorController _warriorPrefab;
    [SerializeField] private int _teamId
    
    private int _lastWarriorIdx;
    private WarriorController[] _warriorPool;

	private void Awake()
    {
        InitWarriorPool();
    }
    
    private void OnEnable()
    {
    	switch(_teamId)
        {
        	case 0: // your base
            	AnswerService.OnCorrectAnswer += SpawnWarrior;
            	break;
                
            case 1: // enemy base
            	AnswerService.OnWrongAnswer += SpawnWarrior;
            	break;
        }
    }
    
    private void OnDisable()
    {
    	switch(_teamId)
        {
        	case 0: // your base
            	AnswerService.OnCorrectAnswer -= SpawnWarrior;
            	break;
                
            case 1: // enemy base
            	AnswerService.OnWrongAnswer -= SpawnWarrior;
            	break;
        }
    }
    
	private void InitWarriorPool()
    {
        _lastWarriorIdx = 0;
        _warriorPool = new WarriorController[WARRIOR_POOL_CAPACITY];

        for (int idx = 0; idx < WARRIOR_POOL_CAPACITY; ++idx)
        {
            WarriorController warrior = Instantiate(_warriorPrefab, transform);
            warrior.SetTeam(_teamId);
            _warriorPool[idx] = warrior;
        }
    }
    
    private void SpawnWarrior()
    {
        WarriorController warrior = _warriorPool[_lastWarriorIdx];
        warrior.Spawn();
        _lastWarriorIdx = (_lastWarriorIdx + 1) % WARRIOR_POOL_CAPACITY;
    }
}

Better Implementation

Technically, the warriors doesn't really need to use any of the bases as a container. A single pool of warriors is enough and each base can simply share access. This way, we will be able to separate the object pool functionality from the base specific functionalities.

I mentioned the WarriorPoolController and the WarriorPoolService classes in my previous blog about Service Locator. The difference is WarriorPoolController is a MonoBehaviour that has a reference to the warrior prefab. On the other hand, WarriorPoolService is a native C# object class that focuses on pool operations like creating the array of warriors and then enabling or disabling these objects as needed.

public interface IWarriorPoolService
{
	void InitPool(WarriorController warriorPrefab, Transform container);
    void Spawn(int teamId, Vector3 spawnPoint, Quaternion rotation);
}
public class WarriorPoolService : IWarriorPoolService
{
	private const int POOL_CAPACITY = 10;
	private WarriorController[] _warriorPool = new WarriorController[POOL_CAPACITY];
	private int _lastWarriorIndex = 0;
    
    public void InitPool(WarriorController warriorPrefab, Transform container)
    {
        for (int idx = 0; idx < POOL_CAPACITY; ++idx)
        {
            WarriorController warrior = Object.Instantiate(warriorPrefab, container);
            warrior.Init();
            _warriorPool[idx] = warrior;
        }
    }

    public void Spawn(int teamId, Vector3 spawnPoint, Quaternion rotation)
    {
        WarriorController warrior = _warriorPool[_lastWarriorIndex];
        warrior.Spawn(teamId, spawnPoint, rotation);
        _lastWarriorIndex = (_lastWarriorIndex + 1) % POOL_CAPACITY;
    }
}
public class WarriorPoolController : MonoBehaviour
{
    [SerializeField] private WarriorController _warriorPrefab;

    private IWarriorPoolService _warriorPoolService;

    private void Awake()
    {
    	_warriorPoolService = ServiceLocator.Instance.GetService<IWarriorPoolService>();
    }

    private void Start()
    {
    	_warriorPoolService.InitPool(_warriorPrefab, transform);
    }
}

Now BaseController doesn't need to worry about the warrior pooling. It just needs a reference to the IWarriorPoolService to spawn a warrior and the service will handle the rest.

public class BaseController : MonoBehaviour
{
    [SerializeField] private int _teamId
	
    private IWarriorPoolService _warriorPoolService;

	private void Awake()
    {
    	_warriorPoolService = ServiceLocator.Instance.GetService<IWarriorPoolService>();
    }

    private void OnEnable()
    {
    	switch(_teamId)
        {
        	case 0: // your base
            	AnswerService.OnCorrectAnswer += SpawnWarrior;
            	break;
                
            case 1: // enemy base
            	AnswerService.OnWrongAnswer += SpawnWarrior;
            	break;
        }
    }
    
    private void OnDisable()
    {
    	switch(_teamId)
        {
        	case 0: // your base
            	AnswerService.OnCorrectAnswer -= SpawnWarrior;
            	break;
                
            case 1: // enemy base
            	AnswerService.OnWrongAnswer -= SpawnWarrior;
            	break;
        }
    }
    
    private void SpawnWarrior()
    {
        Vector3 spawnPoint = transform.position + (transform.forward * 3f);
        _warriorPoolService.Spawn(_teamId, spawnPoint, transform.rotation);
    }
}

Conclusion

To recap, I explained what and why use object pooling. Then, I mentioned what was my plain implementation based on the game concept. Lastly, I illustrated the improvements done from my previous code.

My changes resulted to additional classes but I still think it's better since each class has a clear purpose. This follows the single responsibility principle in S.O.L.I.D.


Thank you for reading! For updates, follow me on Mastodon. You can also subscribe now and leave a comment below!