Implementing Warrior Pool Service
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.
Member discussion