I am making a shooting game, where I have a countdown as I destroy enemies. I want to implement logic when all the game objects are destroyed within the given time, and the countdown reaches zero.
How do I do that?
You need to find a way of identifying all objects of interest. Do they have a particular script on them? You simply count them
if(GameObject.FindObjectOfType<EnemyScript>()==null)
{
// Everything destroyed
}
else
{
// Something survived
}
EnemyScript
increment a shared counter on Start()
(to say "I am here"), and decrement that counter in OnDestroy()
(to say "I was shot"). Then you always know the number of active EnemyScript
s without searching for them every time (handy if you want to display this number in UI, etc). You can have the decrement method check for zero and fire off the "everything destroyed" event in response.
\$\endgroup\$
As mentioned, you could use UnityEngine.GameObject.FindObjectOfType<>()
to find all available instances of a particular script, ideally the script that defines these game objects as enemies, in your game. This works, but it involves a lot of redundant work. You do not need to check if there is still an enemy alive in every frame. Instead, have your enemies notify us when they are destroyed.
Ideally, your main logic would be aware of the enemies, and the enemies would notify the main logic, when they are destroyed. I usually implement this behavior through a singleton objective manager class.
public class ObjectiveManager
{
public static ObjectiveManager instance;
void Awake()
{
if(instance == null)
{
instance = this;
}
else if(instance != this)
{
if(gameObject == null)
{
Destroy(this);
}
else
{
Destroy(gameObject);
}
}
}
}
By being created in this way, we can always access our ObjectiveManager
as an instance, even when we do not have a direct reference. That is because we have a static reference to our single instance: ObjectiveManager.instance
. As an alternative to using a singleton and having a static reference to the objective manager, you could provide each enemy with a reference to ObjectiveManager
.
You have two fairly easy ways to find all the active enemies in your scene: by tag and by script. They both work fairly similar to eachother;
FindObjectsOfType<Enemy>()
will find all active instances of Enemy
, and will retrieve a list of Object
types. FindObjectsWithTag("Enemy")
will find any active instance of a game object tagged with the string "Enemy". This allows you to specify specific Enemy
instances to add to your count, and will retrieve a list of GameObject
types.Once you have decided how you wish to create your list, doing so is fairly simple. Note that you should only implement the code relating to the means in wish you wish to create your list.
public class ObjectiveManager
{
public static ObjectiveManager instance;
public int enemyCount;
void Awake()
{
/* logic for setting up the instance reference */
Object[] enemyList = GameObject.FindObjectsOfType<Enemy>();
GameObject[] enemyList = GameObject.FindObjectsWithTag("Enemy");
// Since the first element in an array is 0, we will
// increment the initial count by 1 to correct the real count.
enemyCount = (enemyList.Length() + 1);
}
Enemy
inform ObjectiveManager
on Awake()
With a singleton objective manager, it is even easier to have the Enemy
class inform ObjectiveManager
that it needs to be added to the count. This is especially handy if you wish to add more Enemy
game objects mid-game.
First, you would add a public method to ObjectiveManager
to handle incrementing the count.
public class ObjectiveManager
{
public static ObjectiveManager instance;
public int enemyCount;
public void AddEnemy()
{
enemyCount++;
}
}
Then, on your enemy, add logic to the Awake()
method to alert the ObjectiveManager
.
public class Enemy
{
void Awake()
{
ObjectiveManager.instance.AddEnemy();
}
}
ObjectiveManager
when an enemy is destroyedFrom there, you can use the Enemy
objects OnDestroy()
method to inform ObjectiveManager
that it has to decrement enemyCount
by 1. Assuming we have correctly counted the total enemies at the start, we know that when we kill the last enemy, enemyCount == 0
.
public class Enemy
{
void OnDestroy()
{
ObjectiveManager.instance.EnemyDestroyed();
}
}
public class ObjectiveManager
{
public static ObjectiveManager instance;
public int enemyCount;
public void EnemyDestroyed()
{
enemyCount --;
if(enemyCount <= 0)
{
AllEnemiesDestroyed();
}
}
public void AllEnemiesDestroyed()
{
// all enemies destroyed. Continue appropriate logic from here.
}
}
There was a complaint that any alternatives to the currently accepted "Find" answer are "over-engineered," but this is not necessarily the case. We can get both good performance and simple code:
public class CountedObject : MonoBehaviour {
public static int count { get; private set; }
public static event System.Action AllObjectsDestroyed;
void Start () {
// Count every object as it's initialized.
count++;
}
void OnDestroy () {
// Decrement the count when objects are destroyed,
// and optionally fire an event when the last one is gone.
if (--count == 0 && AllObjectsDestroyed != null)
AllObjectsDestroyed();
}
}
Now scripts that are interested in how many objects are left can find out in two ways:
CountedObject.count
whenever it needsCountedObject.AllObjectsDestroyed += OnAllDestroyed;
The cost for this is quite modest:
Constant time to read the current count
(for the Find method, this cost is linear in the number of objects in the scene)
Constant time per subscriber to notify when the last object is destroyed, and this cost is incurred only when the final destruction happens
(for the Find method, this cost is linear in the number of objects in the scene every update tick that we check it)
This is not as full-featured and robust as an object manager that can track multiple separate object counts or expose more inspector control, but it gets a greater degree of both flexibility and performance than the accepted answer.
We should of course be wary of premature optimization leading us to excessive complexity, but that doesn't mean we should use code we already know is inefficient when there are much better-performing options available with similar code simplicity. In this day of mobile gaming, even if an inefficiency doesn't hit your framerate, it's still sapping the player's battery unnecessarily when there could be a simple way to let them play longer. ;)