View Gist Page
C#, Unity
Object Pooling system I created for Dot Matrix Dodger. The game takes heavy inspiration from bullet-hell and shmup type games where there are dozens, if not hundreds of game objects on the screen at once. Object Pooling is a common solution for these games and I developed a pooling system that spawns enemies where I need them for the game.
View the Gist below or go to the Gist page linked above for a breakdown
/* | |
* Tomer Braff | |
* May 10, 2017 | |
* ObjectPool.cs | |
* | |
* | |
* This is an ObjectPool class/system for "Dot Matrix Dodger", a mobile game for Android. | |
* Object Pooling systems are fairly ubiquitos in video games as they allow for recycling | |
* and reuse of commonly used objects in games, especially with games in more memory focuesed | |
* languages like C++. For example, instead of instantiating and destroying an enemy from | |
* memory over and over again, the game could intantiate a decent number of enemies, say 100 | |
* or so, and simply activate and deactivate them as needed rather than needing to do costly | |
* instantiating/destroying processes. | |
* | |
* They're practically required for certain types of games like bullet-hell or shmups, which | |
* this game takes heavy inspiration from. | |
* | |
* The way this specific object pool works: | |
* | |
* SETTING UP: | |
* Every Enemy inherets from an "Enemy" class and has its own EnemyType to identify itself. | |
* In the Unity scene, there is a gameobject with the ObjectPool script attached to it. The | |
* Object Pool is then filled with premade enemy prefabs for each of the enemy types I have. | |
* | |
* Unfortunately because of how Unity handles static classes and whatnot, I had to make this | |
* a Singleton object. I'd like to figure out a way to get this to work without having to | |
* instantiate this in the game scene, have it be a static, universal object pool, but for now | |
* it works. | |
* | |
* ON START: | |
* | |
* When initialized, the Object Pool goes through the list of enemy prefabs, looks at each | |
* of the prefab's attached Enemy script, finds that prefab's EnemyType, and create an entry | |
* in the enemyDictionary with the Key being the EnemyType and it's value being a newly instantiated | |
* Queue that holds Enemies. Because all enemies inheret from "Enemy", doing a bunch of Queue<[EnemyScript]> | |
* would be impractical. This way I can create as many different enemy types as I want without having | |
* to create a new Queue type each time. | |
* | |
* It will also place the GameObjects in its own dictionary with the key being the EnemyType. | |
* This is used to decouple certain things from the enemyPrefabs array, like accidentally having | |
* duplicates and lets me place prefabs in regardless of order. It is als used for instantiating | |
* if there isn't enough of a specified enemy to spawn. | |
* | |
* DURING GAMEPLAY: | |
* | |
* Using a wave script/manager system I made, when an enemy needs to be spawned the wave calls | |
* the ObjectPool "SpawnEnemy". There are several functions to spawn an enemy for different | |
* situations, but regardless they all eventually call the original SpawnEnemy function that | |
* calls the Depool function to dequeue the specified EnemyType from its specialized queue in | |
* the dictionary (or instantiates a new enemy if it didn't find one), places the enemy in the | |
* specified location found at a certain angle, and enables the enemy object to view in the game. | |
* | |
* To find the positions the enemy should spawn at, there are several "FindPosition" functions | |
* that take in various parameters to find a point around an origin at a certain radius (since | |
* this game has a lot to do with circles). | |
* | |
* FUTURE CONSIDERATIONS: | |
* | |
* I should probably decouple the FindPosition functions into its own class away from the ObjectPool | |
* since that's not really its particular job. Maybe make that a static class of its own, or | |
* extend one of Unity's like Transform or something. | |
* | |
* SpawnEnemy might also be something that should be decoupled from the Object Pool. This would | |
* help with generalizing the Object Pool instead of having this be specialized to just enemies. | |
* | |
* Generalizing the ObjectPool even more would be great, instead of having this Object Pool be | |
* specifically only for enemies. Earlier in development, for example, I had little particles | |
* that I would spawn behind an enemy like a trail, but I had to create an entirely new queue | |
* for that particle and integrate that everywhere. If I could maybe make an overall object pool, | |
* and then have a certain "section" of the pool be dedicated for Enemies that might help with | |
* those kinds of situaations. | |
* | |
*/ | |
using UnityEngine; | |
using System.Collections.Generic; | |
public class ObjectPool : Singleton<ObjectPool> | |
{ | |
public GameObject[] enemyPrefabs; // Public array to place the different Enemy prefabs | |
public uint enemyPool = 500; // Initial size of each | |
public float radius = 10; // Default radius around a point that an enemy will be spawned at | |
public Sprite[] enemySprites; // Array for enemy sprites. All sprites are the same shape, just different colors | |
public int spriteIndex = 0; // So depending on the theme, enemies can be circles, squares, randomized, whatever | |
#region Dictionaries | |
// Dictionaries relating to the EnemyType and its Queue, GameObject, | |
// A dictionary of Queues that contain specific enemies for the pool | |
private Dictionary<EnemyType, Queue<Enemy>> enemyDictionary = new Dictionary<EnemyType, Queue<Enemy>>(); | |
// A dictionary with EnemyType as keys to the Enemy Prefabs | |
private Dictionary<EnemyType, GameObject> enemyTypeToObject = new Dictionary<EnemyType, GameObject>(); | |
// A Dictionary with EnemyType as keys to the current number of active enemies of that type in the scene | |
// Not really used for anything right now, mostly for future consideration. | |
// Probably could get rid of it though… | |
private Dictionary<EnemyType, int> activeEnemyNum = new Dictionary<EnemyType, int>(); | |
#endregion | |
#region Delegates and Events | |
public delegate void dlgt_Despawn(); // Delegate for the Despawn functions | |
public static event dlgt_Despawn evnt_DespawnEnemies; // Static Event that all enemies subscribe to for despawning | |
// Enemy subscribes its "Despawn" function on enable, unsubscribes when disabled // | |
public delegate void dlgt_ChangeSprite(int spriteIndex); // Delegate for the ChangeSprite function | |
public static event dlgt_ChangeSprite evnt_ChangeSprite; // Static Event that all enemies subscribe to for changing their sprites | |
#endregion | |
// Initialize the Object Pool | |
private void Awake() | |
{ | |
InitializeEnemies(); | |
} | |
private void Start() | |
{ | |
// Change the enemy sprites to the specified sprite in the enemySprites array | |
evnt_ChangeSprite(spriteIndex); | |
// subscribe the "DisableAllEnemies" function here to the GameOver event from the GameState script to handle. | |
GameState.evnt_OnGameOver += DisableAllEnemies; | |
} | |
/// <summary> | |
/// Depool a specified enemy type from a queue to do as you wish and set it to active | |
/// Will Instantiate a new GameObject of the enemy type if it can't find one already. | |
/// </summary> | |
/// <param name="enemType">The enemy type you wish to spawn</param> | |
/// <returns>A GameObject from the specified EnemyType queue (or initialized if none found)</returns> | |
private GameObject DepoolEnemy(EnemyType enemType) | |
{ | |
activeEnemyNum[enemType]++; | |
if(enemyDictionary[enemType].Count > 0) | |
return enemyDictionary[enemType].Dequeue().gameObject; | |
return Instantiate(enemyTypeToObject[enemType]); | |
} | |
/// <summary> | |
/// Pools the passed enemy back into its specified queue. | |
/// </summary> | |
/// <param name="e">The enemy to pool</param> | |
public void PoolEnemy(Enemy e) | |
{ | |
if (enemyDictionary.ContainsKey(e.type)) | |
{ | |
enemyDictionary[e.type].Enqueue(e); | |
activeEnemyNum[e.type]—; | |
} | |
} | |
/// <summary> | |
/// Initializes the enemy queues/dictionaries for the object pool. | |
/// Instantiates the enemies in the enemyPrefabs array by the specified enemyPool amount | |
/// </summary> | |
private void InitializeEnemies() | |
{ | |
Enemy e; | |
EnemyType eType; | |
GameObject o; | |
// Go through the enemyPrefabs array one by one | |
for (int x = 0; x < enemyPrefabs.Length; x++) | |
{ | |
// Take note of the prefab game object, enemyType | |
o = enemyPrefabs[x]; | |
eType = o.GetComponent<Enemy>().type; | |
enemyDictionary.Add(eType, new Queue<Enemy>()); // Add a queue to the enemy dictionary with the specified key of eType | |
enemyTypeToObject.Add(eType, o); // Add the gameObject to the TypeToObject dictionary | |
activeEnemyNum.Add(eType, 0); // Initialize the active enemy number for the type | |
// Instantiate an enemyPool amount of the specified enemy type | |
for (int a = 0; a < enemyPool; a++) | |
{ | |
e = Instantiate(o).GetComponent<Enemy>(); // Probably unecessary to GetComponent<Enemy>() | |
e.gameObject.SetActive(false); | |
} | |
} | |
} | |
/// <summary> | |
/// What it says on the tin. Disables all the enemies subscribed to the Despawn Event. | |
/// This function is subscribed to the onGameOver event from the GameState class. | |
/// </summary> | |
private void DisableAllEnemies() | |
{ | |
evnt_DespawnEnemies(); | |
} | |
/// <summary> | |
/// Sets the speed of the enemies | |
/// </summary> | |
/// <param name="modifier">The amount to modify the speed of the enemies</param> | |
public static void SetEnemySpeeds(float modifier) | |
{ | |
Enemy.speedModifier = modifier; | |
} | |
#region Find Position functions | |
/// <summary> | |
/// Find position from a SPECIFIED ANGLE around a SPECIFIED ORIGIN at a SPECIFIED RADIUS. | |
/// </summary> | |
/// <param name="angle">Angle the point is going to be on in relation to the origin point</param> | |
/// <param name="origin">Vector2 that we want to find the position around</param> | |
/// <param name="distToOrg">Distance from the origin point.</param> | |
/// <returns>Vector2 of the point the given parameters output</returns> | |
private Vector2 FindPosition(float angle, Vector2 origin, float distToOrg) | |
{ | |
/* SOH CAH TOA */ | |
float x = (distToOrg * Mathf.Cos(angle * Mathf.Deg2Rad)) + origin.x; // h * cos(angle) = A, plus the X coordinate offset | |
float y = (distToOrg * Mathf.Sin(angle * Mathf.Deg2Rad)) + origin.y; // h * sin(angle) = O, plus the Y coordinate offset | |
return new Vector2(x, y); | |
} | |
/// <summary> | |
/// Find position from a RANDOM ANGLE around the origin (0,0). DEFAULT RADIUS of the spawn radius | |
/// </summary> | |
/// <returns>Vector2 of the point the given parameters output</returns> | |
private Vector2 FindPosition() | |
{ | |
return FindPosition(Random.Range(0, 360), Vector2.zero, radius); | |
} | |
/// <summary> | |
/// Find position from a SPECIFIED ANGLE around the origin (0,0). DEFAULT RADIUS of the spawn radius | |
/// </summary> | |
/// <param name="angle">Angle the point is going to be on in relation to the origin point</param> | |
/// <returns>Vector2 of the point the given parameters output</returns> | |
private Vector2 FindPosition(float angle) | |
{ | |
return FindPosition(angle, Vector2.zero, radius); | |
} | |
/// <summary> | |
/// Find position from a RANDOM ANGLE around a SPECIFIED ORIGIN. DEFAULT RADIUS of the spawn radius | |
/// </summary> | |
/// <param name="origin">Vector2 that we want to find the position around</param> | |
/// <returns>Vector2 of the point the given parameters output</returns> | |
private Vector2 FindPosition(Vector2 origin) | |
{ | |
return FindPosition(Random.Range(0,360), origin, radius); | |
} | |
/// <summary> | |
/// Find position from a SPECIFIED ANGLE around the origin (0,0) point at a SPECIFIED RADIUS | |
/// </summary> | |
/// <param name="angle">Angle the point is going to be on in relation to the origin point</param> | |
/// <param name="distToOrg">Distance from the origin point.</param> | |
/// <returns>Vector2 of the point the given parameters output</returns> | |
private Vector2 FindPosition(float angle, float distToOrg) | |
{ | |
return FindPosition(angle, Vector2.zero, distToOrg); | |
} | |
/// <summary> | |
/// Find position from a SPECIFIED ANGLE around a SPECIFIED ORIGIN. DEFAULT RADIUS of the spawn radius | |
/// </summary> | |
/// <param name="angle">Angle the point is going to be on in relation to the origin point</param> | |
/// <param name="origin">Vector2 that we want to find the position around</param> | |
/// <returns>Vector2 of the point the given parameters output</returns> | |
private Vector2 FindPosition(float angle, Vector2 origin) | |
{ | |
return FindPosition(angle, origin, radius); | |
} | |
/// <summary> | |
/// Find position from a RANDOM ANGLE around a SPECIFIED ORIGIN at a SPECIFIED DISTANCE. | |
/// </summary> | |
/// <param name="origin">Vector2 that we want to find the position around</param> | |
/// <param name="distToOrg">Distance from the origin point.</param> | |
/// <returns>Vector2 of the point the given parameters output</returns> | |
private Vector2 FindPosition(Vector2 origin, float distToOrg) | |
{ | |
return FindPosition(Random.Range(0,360), origin, distToOrg); | |
} | |
#endregion | |
#region Spawn Enemy functions | |
/// <summary> | |
/// Spawn an enemy at a SPECIFIED POSITION, facing a SPECIFIED ANGLE. | |
/// </summary> | |
/// <param name="enemyType">The type of enemy to spawn</param> | |
/// <param name="enemyPos">The position the enemy should be spawned in</param> | |
/// <param name="facingAngle">What angle the enemy will face</param> | |
/// <returns>The Game Object of the enemy that spawned</returns> | |
private GameObject SpawnEnemy(EnemyType enemyType, Vector2 enemyPos, float facingAngle) | |
{ | |
GameObject obj = DepoolEnemy(enemyType); | |
obj.transform.position = enemyPos; | |
obj.transform.rotation = Quaternion.AngleAxis(facingAngle, Vector3.forward); | |
obj.SetActive(true); | |
return obj; | |
} | |
/// <summary> | |
/// Spawn an enemy at a SPECIFIED ANGLE. This will spawn at the DEFAULT RADIUS around the DEFAULT ORIGIN (0,0) | |
/// assumes the enemy will face towards the origin. | |
/// </summary> | |
/// <param name="enemyType">The type of enemy to spawn</param> | |
/// <param name="spawnAngle">What angle around the origin point the enemy spawns in</param> | |
/// <returns>The Game Object of the enemy that spawned</returns> | |
public GameObject SpawnEnemy(EnemyType enemyType, float spawnAngle) | |
{ | |
return SpawnEnemy(enemyType, FindPosition(spawnAngle, Vector2.zero, radius), spawnAngle + 180); | |
} | |
/// <summary> | |
/// Spawn an enemy at and facing a SPECIFIED ANGLE. This will spawn at the DEFAULT RADIUS around the DEFAULT origin (0,0). | |
/// </summary> | |
/// <param name="enemyType">The type of enemy to spawn</param> | |
/// <param name="spawnAngle">What angle around the origin point the enemy spawns in</param> | |
/// <param name="facingAngle">What angle the enemy will face</param> | |
/// <returns>The Game Object of the enemy that spawned</returns> | |
public GameObject SpawnEnemy(EnemyType enemyType, float spawnAngle, float facingAngle) | |
{ | |
return SpawnEnemy(enemyType, FindPosition(spawnAngle, Vector2.zero, radius), facingAngle); | |
} | |
/// <summary> | |
/// Spawn an enemy at and facing a SPECIFIED ANGLE, at a SPECIFIED RADIUS around the DEFAULT origin (0,0). | |
/// </summary> | |
/// <param name="enemyType">Type of enemy to spawn</param> | |
/// <param name="spawnAngle">What angle around the origin point the enemy spawns in</param> | |
/// <param name="facingAngle">What angle the enemy will face</param> | |
/// <param name="distToOrig">Radius distance to spawn from the origin point</param> | |
/// <returns>The Game Object of the enemy that spawned</returns> | |
public GameObject SpawnEnemy(EnemyType enemyType, float spawnAngle, float facingAngle, float distToOrig) | |
{ | |
return SpawnEnemy(enemyType, FindPosition(spawnAngle, Vector2.zero, distToOrig), facingAngle); | |
} | |
/// <summary> | |
/// Spawn an enemy at and facing a SPECIFIED ANGLE, around a DEFAULT RADIUS around a SPECIFIED ORIGIN. | |
/// </summary> | |
/// <param name="enemyType">Type of enemy to spawn</param> | |
/// <param name="spawnAngle">What angle around the origin point the enemy spawns in</param> | |
/// <param name="orig">The origin point around which to spawn</param> | |
/// <param name="facingAngle">What angle the enemy will face</param> | |
/// <returns>The Game Object of the enemy that spawned</returns> | |
public GameObject SpawnEnemy(EnemyType enemyType, Vector2 orig, float spawnAngle, float facingAngle) | |
{ | |
return SpawnEnemy(enemyType, FindPosition(spawnAngle, orig, radius), facingAngle); | |
} | |
/// <summary> | |
/// Spawn an enemy at and facing a SPECIFIED ANGLE at a SPECIFIED RADIUS around a SPECIFIED ORIGIN. | |
/// </summary> | |
/// <param name="enemyType">Type of enemy to spawn</param> | |
/// <param name="orig">The origin point around which to spawn</param> | |
/// <param name="spawnAngle">What angle around the origin point the enemy spawns in</param> | |
/// <param name="facingAngle">What angle the enemy will face</param> | |
/// <param name="distToOrig">Radius distance to spawn from the origin point</param> | |
/// <returns>The Game Object of the enemy that spawned</returns> | |
public GameObject SpawnEnemy(EnemyType enemyType, Vector2 orig, float spawnAngle, float facingAngle, float distToOrig) | |
{ | |
return SpawnEnemy(enemyType, FindPosition(spawnAngle, orig, distToOrig), facingAngle); | |
} | |
#endregion | |
} |