Home > Enterprise >  Destroy all GameObjects but keep spawning new ones - Unity3D C#
Destroy all GameObjects but keep spawning new ones - Unity3D C#

Time:08-31

When my spaceship dies, I move it to a new random position at the top of the screen. When my spaceship dies, I want to destroy all meteoroids but keep spawning new meteoroids.

When I loop and destroy all meteoroids, no new meteoroids are spawned.

I'm trying to replicate the following Scratch game I created in 2015: Homesick Cody

Some suggestions are to keep count of how many meteoroids and spawn one before deleting the last one. I'm a bit lost on how to do this. And maybe there is a better way.

Meteoroid.cs

using System.Collections.Generic;
using UnityEngine;

public class Meteoroid : MonoBehaviour
{
    [SerializeField] 
    private GameObject spaceship;

    [SerializeField] 
    private float speed = 1.0f;

    [SerializeField] 
    private Rigidbody2D rb;

    // Start is called before the first frame update
    void Start()
    {
        rb = this.GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {
        transform.position = Vector2.MoveTowards(transform.position, spaceship.transform.position, speed * Time.deltaTime);
        transform.up = spaceship.transform.position - transform.position;
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        foreach (var meteoroid in GameObject.FindGameObjectsWithTag("meteoroid"))
        {
            Destroy(meteoroid);
        }
    }

}

MeteoroidSpawn.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MeteoroidSpawn : MonoBehaviour
{

    // Start is called before the first frame update
    void Start()
    {
        StartCoroutine(SpawnRoutine());
    }

    // Update is called once per frame
    void Update()
    {

    }

    [SerializeField] 
    private GameObject meteoroid; //prefab

    [SerializeField]
    private bool spawn = true;

    IEnumerator SpawnRoutine()
    {
        Vector2 spawnPos = Vector2.zero;
        
        while (spawn == true)
        {
            yield return new WaitForSeconds(1f);

            // Spawn meteoroid
            spawnPos.x = Random.Range(-Screen.width/40, Screen.width/40);
            spawnPos.y = -Screen.height/40;
            meteoroid = Instantiate(meteoroid, spawnPos, Quaternion.identity);
        }
    }

}

CodePudding user response:

  1. It seems like you're trying to destroy meteoroids on just any collision. It would be great if you could also check that the collision of the meteoroid was with an actual spaceship. This might not be necessary just now, but it might backfire if any 2 meteoroids collide with each other, or with other game objects that you might add later.

Assuming your spaceship has a Tag Player:

private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Player")
        {
            Debug.Log("collision");
            Destroy(gameObject);  // I want to destroy all meteoroids when spaceship dies
        }
    }
  1. I assume you are destroying meteoroids that leave the game area so that they're not wasting resources.

  2. Your meteoroids don't get destroyed all at once because each one holds its own instance of the Meteoroid script and each one will separately run their own OnCollisionEnter2D(). Thus, if a meteoroid hits the player, it checks only for the collision between itself and the other collider.

If you want to destroy all meteoroids at once, you will have to store them somehow and then on any collision between any Meteoroid, invoke some method that can access all stored meteoroids and destroy them all at once. Your MeteoroidSpawn class seems like a good place to do this for now:

public class MeteoroidSpawn : MonoBehaviour
{
    private List<Meteoroid> meteoroids; // Stores all currently spawned Meteoroids.

    [SerializeField] 
    private GameObject meteoroidPrefab;

    private bool spawn = true;

    private void Start()
    {
        meteoroids = new List<Meteoroid>();  // Create and initialize the list.
        StartCoroutine(SpawnRoutine());
    }

    IEnumerator SpawnRoutine()
    {
        Vector2 spawnPos = Vector2.zero;
        
        while (spawn == true)
        {
            yield return new WaitForSeconds(1f);

            // Spawn meteoroid
            spawnPos.x = Random.Range(-Screen.width/40, Screen.width/40);
            spawnPos.y = -Screen.height/40;
            var meteor = Instantiate(meteoroidPrefab, spawnPos, Quaternion.identity);
            
            // Add new meteoroid to the list.
            meteoroids.Add(meteor.GetComponent<Meteoroid>());
        }
    }
}

Now that you have a reference to all spawned meteoroids, you can destroy them all in whatever fashion you like, i.e. write a static method in the MeteoroidSpawn.cs that will destroy meteoroids and call it from a Meteoroid.cs on a collission (remember that you would also have to make the List<Meteoroid> static).

Such code could look like this (MeteoroidSpawn.cs):

public static void DestroyAllMeteoroids()
{
    foreach (var meteoroid in meteoroids)
        Destroy(meteoroid.gameObject);
}

and could be called like this (Meteoroid.cs):

private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Player")
        {
            Debug.Log("collision");
            MeteoroidSpawn.DestroyAllMeoroids();
        }
    }
Event approach

Another way is to use events. Unity provides an event system of its own (UnityEvent) but I personally prefer to use ordinary C# events for as light work as this:

Meteoroid.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Meteoroid : MonoBehaviour
{
    public event EventHandler OnSpaceshipHit;

    [SerializeField] 
    private GameObject spaceship;

    [SerializeField] 
    private float speed = 1.0f;

    [SerializeField] 
    private Rigidbody2D rb;

    // Start is called before the first frame update
    void Start()
    {
        rb = this.GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {
        transform.position = Vector2.MoveTowards(transform.position, spaceship.transform.position, speed * Time.deltaTime);
        transform.up = spaceship.transform.position - transform.position;
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Player")
        {
            Debug.Log("collision");
            OnSpaceshipHit?.Invoke(this, null); // Raise event.
        }
    }

}

And inside MeteoroidSpawn.cs:

private List<Meteoroid> meteoroids; // Stores all currently spawned Meteoroids.

private void Start()
{
    meteoroids = new List<Meteoroid>();  // Create and initialize the list.
    StartCoroutine(SpawnRoutine());
}
    
// Rest of the code.

IEnumerator SpawnRoutine()
{
    Vector2 spawnPos = Vector2.zero;
    
    while (spawn == true)
    {
        yield return new WaitForSeconds(1f);

        // Spawn meteoroid
        spawnPos.x = Random.Range(-Screen.width/40, Screen.width/40);
        spawnPos.y = -Screen.height/40;
        Meteoroid meteor = Instantiate(meteoroidPrefab, spawnPos, Quaternion.identity).GetComponent<Meteoroid>();
        meteor.OnSpaceshipHit  = DestroyAllMeteoroids; // Subscribe to the event.
            
        // Add new meteoroid to the list.
        meteoroids.Add(meteor);
    }
}

private void DestroyAllMeteoroids(object sender, EventArgs e)
{
    foreach (var meteoroid in meteoroids)
        Destroy(meteoroid.gameObject);
}

With this setup, you subscribe to the OnSpaceshipHit event of every new meteoroid. Now every meteoroid has a way to let the Spawner know that it had hit the player. If that happens, an event is raised and the DestroyAllMeteoroids() method is called, destroying all meteoroids in the process. No static methods or variables needed. There is also no need to unsubscribe from the event, since you're destroying the game object the same frame the collision happens.

Let me know if you need any further explanation of the code.

CodePudding user response:

Update

After you edited your question with apprently the acual code it is clear that this line

meteoroid = Instantiate(meteoroid, spawnPos, Quaternion.identity);

causes the issue that the new spawned meteoroid is a clone of the previous one.

So of course as soon as you destroy the last instantiated instance meteoroid becomes an invalid object which can not be used as a prefab for Instantiate anymore.

I see no reason why you would need to update the field at all

=>

Instantiate(meteoroid, spawnPos, Quaternion.identity);

original before edit of the question

Not an answer to where your error comes from but I would simply keep track of all instances e.g. by having them under a specific parent object:

Instantiate(meteoroid, spawnPos, Quaternion.identity, transform);

then you can simply have a method like

public void DestroAllCurrenMeteroids()
{
    foreach(Transform child in transform)
    {
        Destroy(child.gameObject);
    }
}

As a more fancy alternative you can do it within the type itself in a collection like e.g.

public class Meteoroid : MonoBehaviour
{
    private static readonly HashSet<Meteoroid> instances = new ();

    ...

    private void Awake()
    {
        instances.Add(this);
    }

    private void OnDestroy()
    {
        instances.Remove(this);
    }

    public static void DestroyAllInstances()
    {
        foreach(var instance in instances)
        {
            Destroy(instance.gameObject);
        }
    }
}

so from everywhere you can simply call

Meteoroid.DestroyAllInstances();
  • Related