0
\$\begingroup\$

In my game the player can interact with certain objects like coins, clouds and springs. After these interactions the game objects are destroyed. All of these game objects have their own properties, but their classes inherit from a master "WorldObjects" class that contains respawn information:

protected virtual void OnTriggerEnter2D(Collider2D other)
{
    if (other.CompareTag("Player"))
    {
        if (respawnObject)
        { 
            Respawner.respawnPositions.Add(startPosition);
            Respawner.respawnTimes.Add(respawnTimer);
        }

        PlayerController = other.GetComponent<CharacterController2D>();
    }

    else return;
}

I have been able to respawn one type of object at a time by dragging a prefab to the first position in my objectsToRespawn array, but how do I send information to my Respawner class so that I can dynamically respawn the correct type of game object depending on the object I previously destroyed? Is it possible to compare my destroyed game object's name with its associated prefab, and then load the prefab into the array (or, if needed, a list)?

Here's my respawner class:

public class Respawner : MonoBehaviour
{
public GameObject[] objectsToRespawn;

public List<Vector3> respawnPositions = new List<Vector3>();
public List<float> respawnTimes = new List<float>();

private void Update ()
{
    if (respawnPositions.Count != 0)
    {
        RespawnObjects();
    }
}

private void RespawnObjects()
{
    for (int i = 0; i < respawnPositions.Count; i++)
    {
        respawnTimes[i] -= Time.deltaTime;

        if (respawnTimes[i] <= 0)
        {
            Instantiate(objectsToRespawn[i], respawnPositions[i], Quaternion.Euler(0, 0, 0));
            respawnPositions.RemoveAt(i);
            respawnTimes.RemoveAt(i);
        }
    }
}

Perhaps I should clarify, what I want to have happen is when I destroy a game object via OnTriggerEnter2D (like picking up a coin), that object sends its name or type or some sort of identification to the respawner class, which "queues" up the associated prefab, ultimately respawning it in its original location once the respawn timer for that object is 0.

This is my coin's OnTriggerEnter2D class (where the object is destroyed):

protected override void OnTriggerEnter2D(Collider2D other)
{
    base.OnTriggerEnter2D(other);

    Destroy(this.gameObject);
    Destroy(Instantiate(interactionEffect.gameObject, ObjectTransform.position, ObjectTransform.rotation) as GameObject, 
        interactionEffect.main.duration * 2);
}
\$\endgroup\$
  • \$\begingroup\$ The title of the question, "How do I respawn a prefab that matches the name of the game object I just destroyed?" gave me some terrifying flashbacks ;) \$\endgroup\$ – DMGregory Mar 6 '18 at 0:11
  • \$\begingroup\$ If I understand you correctly though, your question isn't really about matching names, but about ensuring that the prefab you're spawning from is the source prefab that corresponds to the object that requested the respawn, right? \$\endgroup\$ – DMGregory Mar 6 '18 at 9:08
  • \$\begingroup\$ Yes. I have objects that may need to be respawned multiple times, and I want to make sure the prefab of that object is respawned so that the instance of that object has all the necessary traits each time it's spawned. \$\endgroup\$ – Ginger and Lavender Mar 6 '18 at 15:35
2
\$\begingroup\$

Edit: whoops! I had the idea that I wanted the prefab of the object itself to remember which prefab it's spawning from, but neglected a wrinkle of prefab spawning: when you spawn a copy of an object, all references that object has to itself and its own components/children get translated in the copy to point at the copy itself and the copy's components/children.

This is usually a good thing - it means that if I spawn a dozen instances of a gun, each one fires from its own barrel, rather than all of those instances still referencing the original gun's barrel.

But, it does slightly complicate a reference to "my own prefab" which, when the object is a prefab in the Assets folder is "me," but when the object is dropped/spawned in the scene, we no longer want it to be "me" but still point at the same asset in the Assets folder.

Here's how we can get this behaviour....


public class Respawnable : MonoBehaviour {

    public Respawnable prefab;

// When we're setting up the scene in the Unity editor:
// If I drag a reference of this prefab into the scene,
// the OnValidate() method runs and fixes up the reference 
// to point to its prefab instead of to itself.
// This also initializes the reference when we first add
// the script to a new object that is or will become our prefab.
#if UNITY_EDITOR
    void OnValidate() {
        if(sourcePrefab == this || sourcePrefab == null) {
            var prefab = (Respawnable)UnityEditor.PrefabUtility.GetPrefabParent(this);
            sourcePrefab = prefab ?? this;            
        }
    }
#endif

    // When spawning one of these via script, we need to fix up the
    // prefab link after spawning the copy. So call the Spawn()
    // method instead of Instantiate to make sure this happens.
    public GameObject Spawn() {
        var spawned = Instantiate(prefab);
        spawned.prefab = prefab;
        return spawned.gameObject;
    }

    // You can mirror all the overloads of Instantiate() you like.
    public GameObject Spawn(Vector3 position, Quaternion orientation) {
        var spawned = Spawn().transform;
        spawned.localPosition = position;
        spawned.localRotation = orientation;
        return spawned.gameObject;
    }
.
    Vector3 startPosition;
    void Start() {
        startPosition = transform.position;
    }

    void OnTriggerEnter(Collider2D other) {
        if (other.CompareTag("Player") == false)
            return;

        if (enabled) {
            // Find the respawner and tell it to respawn us after we're gone.
            var respawner = Respawner.GetInstance();
            respawner.Add(this, startPosition, respawnTimer);
        }
    }
}

Attach this to anything you want to respawn this way.

To simplify the lists, I'd make a little record type:

public struct RespawnRecord {
    public Respawnable prefab;
    public Vector3 spawnPoint;
    public float time;
}

And your respawn logic now knows exactly what to spawn (and doesn't need to juggle multiple lists)

List<RespawnRecord> respawnList = new List<RespawnRecord>();

public void Add(Respawnable prefab, Vector3 spawnPoint, float time) {
    respawnList.Add(
            new RespawnRecord(){
                 prefab = prefab,
                 spawnPoint = spawnPoint,
                 time = time
            });
}

private void RespawnObjects()
{
    for (int i = 0; i < respawnList.Count; i++)
    {
        var record = respawnList[i];
        record.time -= Time.deltaTime;

        if (record.time <= 0)
        {
            // Note we use the Spawn method instead of Instantiate directly.
            record.prefab.Spawn(record.spawnPoint, Quaternion.identity);

            // Don't forget to decrement i after RemoveAt! 
            // Otherwise our next iteration checks slot [i+1]
            // which means we skip whichever object just moved
            // down to fill slot [i] when we emptied it.                  
            respawnList.RemoveAt(i--);
        } else {
            // Write back the changed record struct.
            respawnList[i] = record;
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ I've been reviewing both posts this evening to learn how you got to this solution, but I don't understand why we have to write i--. I don't think I've ever done this in a 'for' loop before...would you mind explaining this to me? Another peculiar issue I've found is that the respawn only works one time. After that the spawned object tries to respawn its clone instead of the prefab I assigned in the inspector. From what I've researched it seems one needs to load resources from a created Resources folder in the project in order to continue to dynamically spawn objects using code like this. \$\endgroup\$ – Ginger and Lavender Mar 6 '18 at 5:52
  • 1
    \$\begingroup\$ For the for loop decrement, imagine you remove the item at i = 3. That means the item that was at i = 4 moves down into the slot i = 3. If we don't decrement i, then our next loop iteration looks at the item that's in slot i = 4 now (ie. the item that used to be in slot i = 5), effectively jumping by 2 and skipping over the item that had been in the slot i = 4 originally. By decrementing after the remove, we ensure our next iteration checks slot i = 3 again, so the item we just moved down to fill the removed space doesn't get skipped. \$\endgroup\$ – DMGregory Mar 6 '18 at 7:52
  • 1
    \$\begingroup\$ I fixed the error where it only worked once - sorry about that, I'd overlooked the way self references are updated when Instantiating. It's a bit less minimal than my first thought, but it does avoid having to treat the prefab as a dynamically-loaded resource. If your coins are only going away for a little while though, you might want to consider instead just hiding them by disabling their renderer & collider, and letting them reset themselves sometime later. This avoids the churn of deleting & respawning and keeps the behaviour more self-contained. \$\endgroup\$ – DMGregory Mar 6 '18 at 8:57
  • \$\begingroup\$ Thanks for the input. I actually wanted to try creating this respawn system as a learning tool more than anything (especially to help myself better understand how to incorporate it into AI later), and I did consider just turning the object off briefly, but for some reason that felt "cheap" to me even though it's perfectly viable. (Another reason is that I have objects moving around and rotating, so I want them to reappear as they were upon first spawn every time they are respawned.) I'll check out your updated solution after work today. I very much appreciate all of the effort in your posts. \$\endgroup\$ – Ginger and Lavender Mar 6 '18 at 15:38
  • \$\begingroup\$ I finally got around to implementing this today, and it works great. I did not have to use var respawner = Respawner.GetInstance(); in OnTriggerEnter2D though, but it's very interesting to see how the cloned objects now point to the prefabs as their respawn objects. \$\endgroup\$ – Ginger and Lavender Mar 8 '18 at 21:18
2
\$\begingroup\$

First, you should combine this information:

public GameObject[] objectsToRespawn;
public List<Vector3> respawnPositions = new List<Vector3>();
public List<float> respawnTimes = new List<float>();

Nothing about these lists says that the indexes for each refers to the same index in the other (not to mention that one of them is a static sized array and the other two are lists!). You should instead create a custom class object that holds these three pieces of information and have a single list of that class.

public class RespawnObjectInformation {
    public GameObject objectToRespawn;
    public Vector3 respawnPosition;
    public float respawnTime;
}

public class Respawner : MonoBehaviour {
    public List<RespawnObjectInformation> objectsToRespawn;
    // ...

This insures that any information you need is available and correct. You can't inadvertently reverse() one of the arrays and screw everything up.

You need the object name, too? Add that information to the class:

public class RespawnObjectInformation {
    public GameObject objectToRespawn;
    public Vector3 respawnPosition;
    public float respawnTime;
    public string objectName;
}

And your respawn code:

private void RespawnObjects()
{
    for (int i = 0; i < objectsToRespawn.Count(); i++)
    {
        objectsToRespawn[i].respawnTime -= Time.deltaTime;

        if (objectsToRespawn[i].respawnTime <= 0)
        {
            GameObject go = Instantiate(objectsToRespawn[i].objectToRespawn, objectsToRespawn[i].respawnPosition, Quaternion.Euler(0, 0, 0));
            go.name = objectsToRespawn[i].name;
        }
    }
    //remove objects from outside the loop to avoid a concurrent modification exception
    objectsToRespawn.RemoveAll(x => x.respawnTime <= 0);
}

Edit: SO MUCH LINQ

We can replace that entire function with this, instead, utilizing linq and lambda functions.

List<RespawnObjectInformation> respawns = objectsToRespawn.FindAll(x => {
    x.respawnTime -= Time.deltaTime;
    x.respawnTime <= 0
}); //yes, we can do this!
respawns.ForEach(x => {
    GameObject go = Instantiate(x.objectToRespawn, x.respawnPosition, Quaternion.Euler(0, 0, 0));
    go.name = x.name;
});
objectsToRespawn.RemoveAll(respawns);
\$\endgroup\$
  • \$\begingroup\$ I appreciate the help, and clarified what I want to have happen in the original post. \$\endgroup\$ – Ginger and Lavender Mar 5 '18 at 23:52
  • \$\begingroup\$ @Dalsia Edited! \$\endgroup\$ – Draco18s Mar 6 '18 at 0:06
  • \$\begingroup\$ Ahh, looks like we were thinking in similar veins! Your RemoveAll is a much better way to do it than what I put in my answer - it lets us cut down on unnecessary iterations when removing multiple entries in a long list. \$\endgroup\$ – DMGregory Mar 6 '18 at 0:09
  • \$\begingroup\$ @DMGregory The predicate methods (LINQ) are awesome. I had to look up the correct syntax in a project I have to make sure I was doing it right though. All sorts of things you can do with LINQ expressions: Find (one, many), sort, remove... Also means I could have used a ForEach instead of a raw For. \$\endgroup\$ – Draco18s Mar 6 '18 at 0:10
  • \$\begingroup\$ @DMGregory Added a very linq-y alternative, if you're interested. \$\endgroup\$ – Draco18s Mar 6 '18 at 0:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.