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:
- 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
}
}
I assume you are destroying meteoroids that leave the game area so that they're not wasting resources.
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 ownOnCollisionEnter2D()
. 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();