- Home /
Communication between a generalized Spawner and spawned objects
I have a simple problem with instance counting that I can't resolve. I'm trying to write a generalized "Spawner" class which can instantiate any number of prefabs dragged into it in the Inspector, based on some conditions (also set in the Inspector). I need the instantiated objects to tell the Spawner when they are destroyed, without caring who the Spawner was (there can be multiple Spawners in the scene). I've defined an interface for the spawners which looks like this:
using UnityEngine;
public interface ICanSpawnEntities {
void NotifyDestroyed(GameObject obj);
}
I also have an interface to be used on the script component of the prefabs to be spawned, which looks like this:
public interface ISpawnable
{
Spawner Spawner { get; set; }
}
The script component can then do this:
private void OnDestroy()
{
Spawner.NotifyDestroyed(this.gameObject);
}
In my Spawner class, I'm using the simplest possible implementation of weighted probabilities for the different prefabs. In order to keep count of the different instantiated prefabs, I'm using a Dictionary. But for the life of me I can't figure out how to write the NotifyDestroyed() method. I don't know how to compare the returned GameObject (or even a ISpawnable object) to my SpawnableObject struct. On a side note, I think the SpawnableObject properties should be static because they only define a "type" of a spawnable object (something which has a shared prefab and some stats), but when I've tried that it got me into a huge reference mess. Please help! There must be an easier way to achieve this (I'd rather not use a central EventManager for this or FindWithTag methods, I'd just like to keep the classes loosely coupled). Thanks a lot.
using System.Collections; using System.Collections.Generic; using UnityEngine;
public class Spawner : MonoBehaviour, ICanSpawnEntities
{
// ---------- Inspector fields ----------
[Range(1, 60)]
public int secondsBetweenSpawns;
[Range(0, 1)]
public float overallSpawnProbability = 1;
public SpawnableObject[] spawnableObjects;
// ---------- Private fields ----------
private int totalProbabilityWeights;
private List<SpawnableObject> spawnableObjectsWeighted;
private Dictionary<SpawnableObject, int> spawnedCount;
private Coroutine spawnCoroutine;
private void Awake()
{
// Initialize the list and dictionary
spawnableObjectsWeighted = new List<SpawnableObject>();
spawnedCount = new Dictionary<SpawnableObject, int>();
// For each spawnable object, add it to the list as many times
// as its weight value, and set its spawned count to zero
foreach (SpawnableObject obj in spawnableObjects)
{
for (int i = 0; i < obj.probabilityWeight; i++)
{
spawnableObjectsWeighted.Add(obj);
spawnedCount[obj] = 0;
}
}
}
public void NotifyDestroyed(GameObject obj)
{
// ??
}
private void Start()
{
StartSpawning();
}
// Other objects might need to ask this to start spawning,
// so it's separate from Start()
public void StartSpawning()
{
Debug.Log("[Spawner/StartSpawning]");
spawnCoroutine = StartCoroutine(SpawnCoroutine());
}
// Stop the spawning coroutine
public void StopSpawning()
{
StopCoroutine(spawnCoroutine);
}
// Main spawning routine
IEnumerator SpawnCoroutine()
{
while (true)
{
// Wait 3 seconds between spawns
yield return new WaitForSeconds(3.0f);
// If the random value is higher than the probability,
// wait some more (secondsBetweenSpawns)
while (Random.value > overallSpawnProbability)
{
yield return new WaitForSeconds(secondsBetweenSpawns);
}
// Get the object to spawn from the weighted list of objects
SpawnableObject obj = spawnableObjectsWeighted[Random.Range(0, spawnableObjectsWeighted.Count)];
// Instantiate the object if the maximum number hasn't been reached
if (spawnedCount[obj] < obj.maxSimultaneousInstances)
{
Instantiate(obj.prefab);
spawnedCount[obj]++;
Debug.Log("[Spawner/SpawnCoroutine] " + obj.prefab.name + " spawned");
}
else
{
Debug.Log("[Spawner/SpawnCoroutine] "
+ obj.prefab.name + " already at maximum ("
+ obj.maxSimultaneousInstances + ")");
}
}
}
}
// A struct holding an object and a weight for its spawning probability.
// Make it serializable so its fields are visible in the Inspector.
[System.Serializable]
public struct SpawnableObject
{
public GameObject prefab;
[Range(0, 10)]
public int probabilityWeight;
[Range(1, 3)]
public int maxSimultaneousInstances;
}
Answer by Bunny83 · Feb 06, 2018 at 04:00 PM
In short: You can't.
An instance of a prefab does not have any information to know from which prefab or object it was cloned from. It's just a clone. If you want to identify a certain type of object you have to store some kind of identifier with the object. One way would be to compare the object names. As long as you ensure that the instantiated clones have the same name as the prefab and you ensure that every prefab has a unique name you can match an instance with a certain prefab type.
However you can go a different route. Just add a non serialized List to your SpawnableObject struct which would be used to track all instantiated objects of this type. You don't even need the callback from the instance since you can simply check if an instance in the list is "fake-null" / destroyed. Of course using a callback would be more efficient otherwise you would have to scan the tracking list regularily for destroyed objects.
An efficient way to implement a tracking list is to iterate the list in reverse. Removing elements at the end of a list is the cheapest operation. Removing elements in between requires moving all following elements one up. Since the order of the elements doesn't matter you can simply do:
[System.Serializable]
public struct SpawnableObject
{
public GameObject prefab;
[Range(0, 10)]
public int probabilityWeight;
[Range(1, 3)]
public int maxSimultaneousInstances;
private List<GameObject> m_Instances;
public int AliveCount { get {
return RemoveDestroyed();
} }
public void Spawn()
{
if (m_Instances == null)
m_Instances = new List<GameObject>();
// spawn your instance here
m_Instance.Add( newInstance );
}
public int RemoveDestroyed()
{
if (m_Instances == null)
return 0;
for(int i = m_Instances.Count-1; i >= 0; i--)
{
if (m_Instances[i] == null)
{
int lastIndex = m_Instances.Count - 1;
if (i < lastIndex)
m_Instances[i] = m_Instances[lastIndex];
m_Instances.RemoveAt(lastIndex);
}
}
return m_Instances.Count;
}
}
This will allow you to keep track of all instances of this prefab type. You would spawn a prefab using the Spawn method. If your spawner is "informed" about a destroyed enemy you can simply call "RemoveDestroyed()" on all of your "SpawnableObjects". In addition it will return the number of objects that are still alive.
When iterating the list to remove null elements we simple replace the null item in the list with the last item in the list and remove the last item. This way we don't move the elements around unnecessarily.
Note: If you use an object pool so the actual objects are never destroyed, you would check against the active state of the object in addition to checking against null:
if (m_Instances[i] == null || m_Instances[i].activeSelf == false)
instead of
if (m_Instances[i] == null)
Note that i initialize the "m_Instances" list inside the Spawn method. We can't initialized it in a field initializer since you use a struct. Structs can't have field initializers since structs and it's variables are always initialized with the default values unless you use a custom constructor. Structs can't have a custom default constructor. When using a class instead of struct this wouldn't be a problem and we could simply do
private List<GameObject> m_Instances = new List<GameObject>();
So we wouldn't need the null check
edit
Another approach would be to turn your SpawnableObject struct into a class. That way we can reference it from the instantiated object. You can change the "ISpawnable" interface to
public interface ISpawnable
{
SpawnableObject Owner { get; set; }
}
and have the actual script instance inform the corresponding "SpawnableObject" where this instance belongs to. If you want everything more abstract you can implement a seperate interface for the SpawnableObject to define the callback.
However it highly depends on what's the actual purpose of your communication between an instance and its spawner.
The purpose of the communication is just for the Spawner to keep track of which and how many instances it spawned, and for the spawned instances to notify the Spawner when they've been destroyed. It seems crazy to me to go into so many interfaces just to do this simple thing. Although the string comparison method you mentioned might be the simplest one. Thanks.
Your answer
![](https://koobas.hobune.stream/wayback/20220612152046im_/https://answers.unity.com/themes/thub/images/avi.jpg)