Spawn system that doesn't instantiated enemies on top of player or each other (C#)
Hello my fellow great Unites. I have an enemy spawn system that spawns out enemies using Ienumerator inside a set of borders. Now the only problem is that the enemies will sometimes spawn out on the player, or another object...
How can I fix this problem? Using a for loop? Any code example?
Here's the code:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class EnemySpawn : MonoBehaviour
{
// EnemyPrefabs
public GameObject EnemyBouncingPrefab;
public GameObject EnemyGrowerPrefab;
public GameObject EnemyMinePrefab;
//Show where spawn is prefab
public GameObject showEnemySpawn;
public GameObject showEnemyFlyBySpawn;
public GameObject showEnemyGrowerSpawn;
public GameObject showEnemyMineSpawn;
// Array of spawn points
public Transform[] spawnPointsEnemyFollow;
public Transform[] spawnPointsEnemyExplode;
public Transform[] spawnPointsEnemyFlyBy;
// Borders
public Transform borderTop;
public Transform borderBottom;
public Transform borderLeft;
public Transform borderRight;
//Used to put active enemies in a list
public static List<GameObject> activeEnemies;
void Start ()
{
// Starts a coroutine function for spawning bouncing enemies and fly by enemy
StartCoroutine(SpawnBouncingEnemy());
StartCoroutine(SpawnGrowerEnemy());
StartCoroutine(SpawnEnemyFlyBy());
StartCoroutine(SpawnMineEnemy());
}
void Awake()
{
// Create the list
activeEnemies = new List<GameObject>();
}
// Spawn a Bouncing enemy
IEnumerator SpawnBouncingEnemy()
{
//Wait 3 to 5 seconds when game starts to spawn a ball
yield return new WaitForSeconds(Random.Range(1, 3));
while(true)
{
//Calls the function to set random position
Vector2 spawnPoint = RandomPointWithinBorders();
// Show spawn location for one second
Object marker = Instantiate(showEnemySpawn, spawnPoint, Quaternion.identity);
yield return new WaitForSeconds(1);
Destroy(marker);
// Spawn enemy
GameObject newEnemy = (GameObject) Instantiate(EnemyBouncingPrefab, spawnPoint, Quaternion.identity);
activeEnemies.Add(newEnemy);
yield return new WaitForSeconds(Random.Range(4, 6));
}
}
// Spawn a Grower enemy
IEnumerator SpawnGrowerEnemy()
{
//Wait 3 to 5 seconds when game starts to spawn a "grower"
yield return new WaitForSeconds(Random.Range(20, 40));
while(true)
{
//Calls the function to set random position
Vector2 spawnPoint = RandomPointWithinBorders();
// Show spawn location for one second
Object marker = Instantiate(showEnemyGrowerSpawn, spawnPoint, Quaternion.identity);
yield return new WaitForSeconds(1);
Destroy(marker);
// Spawn enemy
GameObject newEnemy = (GameObject) Instantiate(EnemyGrowerPrefab, spawnPoint, Quaternion.identity);
activeEnemies.Add(newEnemy);
yield return new WaitForSeconds(Random.Range(20, 50));
}
}
// Spawn a Mine enemy
IEnumerator SpawnMineEnemy()
{
//Wait 40 to 60 seconds when game starts to spawn a "Mine_enemy"
yield return new WaitForSeconds(Random.Range(45, 60));
while(true)
{
//Calls the function to set random position
Vector2 spawnPoint = RandomPointWithinBorders();
// Show spawn location for one second
Object marker = Instantiate(showEnemyMineSpawn, spawnPoint, Quaternion.identity);
yield return new WaitForSeconds(2);
Destroy(marker);
// Spawn enemy
GameObject newEnemy = (GameObject) Instantiate(EnemyMinePrefab, spawnPoint, Quaternion.identity);
activeEnemies.Add(newEnemy);
yield return new WaitForSeconds(Random.Range(50, 60));
}
}
public Vector2 RandomPointWithinBorders()
{
//Code that will spawn a bouncing ball and ShowSpawn at a random position inside the borders
Vector2 random = new Vector2();
random.x = (int)Random.Range(borderLeft.position.x, borderRight.position.x);
random.y = (int)Random.Range(borderBottom.position.y, borderTop.position.y);
return random;
}
}
Answer by TBruce · May 25, 2016 at 08:12 PM
Try this modified version of your code. This will get a valid spawn point before the first Instantiate() because it is calculated in RandomPointWithinBorders().
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class EnemySpawn : MonoBehaviour
{
// EnemyPrefabs
public GameObject EnemyBouncingPrefab;
public GameObject EnemyGrowerPrefab;
public GameObject EnemyMinePrefab;
//Show where spawn is prefab
public GameObject showEnemySpawn;
public GameObject showEnemyFlyBySpawn;
public GameObject showEnemyGrowerSpawn;
public GameObject showEnemyMineSpawn;
// Array of spawn points
public Transform[] spawnPointsEnemyFollow;
public Transform[] spawnPointsEnemyExplode;
public Transform[] spawnPointsEnemyFlyBy;
// Borders
public Transform borderTop;
public Transform borderBottom;
public Transform borderLeft;
public Transform borderRight;
//Used to put active enemies in a list
public static List<GameObject> activeEnemies;
public Transform player; // player transform
public float minDistance; // minimum distance from spawnpoint to object
void Start()
{
// if the player has not been set in the inspector log it
if (player == null)
{
Debug.LogWarning("EnemySpawn.Start(): There is no player set in the inspector. Please set this.");
}
// if the player has been set in the inspector but the Tag is not set to "Player" log it
if ((player != null) && (player.tag != "Player"))
{
Debug.LogWarning("EnemySpawn.Start(): The Player does not have its Tag set to 'Player'.");
}
// if the player has not been set in the inspector and can find by Tag set it
if ((player == null) && (GameObject.FindGameObjectsWithTag("Player") != null))
{
Debug.LogWarning("EnemySpawn.Start(): Setting the missing player.");
player = GameObject.FindGameObjectsWithTag("Player")[0].transform;
}
// Starts a coroutine function for spawning bouncing enemies and fly by enemy
StartCoroutine(SpawnBouncingEnemy());
StartCoroutine(SpawnGrowerEnemy());
// StartCoroutine(SpawnEnemyFlyBy()); // this does not exist yet
StartCoroutine(SpawnMineEnemy());
}
void Awake()
{
// Create the list
activeEnemies = new List<GameObject>();
}
// Spawn a Bouncing enemy
IEnumerator SpawnBouncingEnemy()
{
//Wait 3 to 5 seconds when game starts to spawn a ball
yield return new WaitForSeconds(Random.Range(1, 3));
while(true)
{
//Calls the function to set random position
Vector3 spawnPoint = RandomPointWithinBorders();
// Show spawn location for one second
Object marker = Instantiate(showEnemySpawn, spawnPoint, Quaternion.identity);
yield return new WaitForSeconds(1);
Destroy(marker);
// Spawn enemy
GameObject newEnemy = (GameObject) Instantiate(EnemyBouncingPrefab, spawnPoint, Quaternion.identity);
activeEnemies.Add(newEnemy);
yield return new WaitForSeconds(Random.Range(4, 6));
}
}
// Spawn a Grower enemy
IEnumerator SpawnGrowerEnemy()
{
//Wait 3 to 5 seconds when game starts to spawn a "grower"
yield return new WaitForSeconds(Random.Range(20, 40));
while(true)
{
//Calls the function to set random position
Vector3 spawnPoint = RandomPointWithinBorders();
// Show spawn location for one second
Object marker = Instantiate(showEnemyGrowerSpawn, spawnPoint, Quaternion.identity);
yield return new WaitForSeconds(1);
Destroy(marker);
// Spawn enemy
GameObject newEnemy = (GameObject) Instantiate(EnemyGrowerPrefab, spawnPoint, Quaternion.identity);
activeEnemies.Add(newEnemy);
yield return new WaitForSeconds(Random.Range(20, 50));
}
}
// Spawn a Mine enemy
IEnumerator SpawnMineEnemy()
{
//Wait 40 to 60 seconds when game starts to spawn a "Mine_enemy"
yield return new WaitForSeconds(Random.Range(45, 60));
while(true)
{
//Calls the function to set random position
Vector3 spawnPoint = RandomPointWithinBorders();
// Show spawn location for one second
Object marker = Instantiate(showEnemyMineSpawn, spawnPoint, Quaternion.identity);
yield return new WaitForSeconds(2);
Destroy(marker);
// Spawn enemy
GameObject newEnemy = (GameObject) Instantiate(EnemyMinePrefab, spawnPoint, Quaternion.identity);
activeEnemies.Add(newEnemy);
yield return new WaitForSeconds(Random.Range(50, 60));
}
}
public Vector3 RandomPointWithinBorders()
{
bool done = false;
Vector3 randomPosition = new Vector3();
while (!done)
{
//Code that will spawn a bouncing ball and ShowSpawn at a random position inside the borders
randomPosition.x = (int)UnityEngine.Random.Range(borderLeft.position.x, borderRight.position.x);
randomPosition.y = (int)UnityEngine.Random.Range(borderBottom.position.y, borderTop.position.y);
randomPosition.z = 0;
done = ((minDistance == 0) || ValidMinimumDistance(randomPosition));
}
return randomPosition;
}
bool ValidMinimumDistance(Vector3 enemyPosition)
{
bool isValid = true;
minDistance = Mathf.Abs(minDistance);
if (player != null)
{
isValid = (Vector3.Distance(player.position, enemyPosition) > minDistance);
}
if (isValid && (activeEnemies.Count > 0))
{
for (int i = 0; i < activeEnemies.Count; i++)
{
if (Vector3.Distance(activeEnemies[i].transform.position, enemyPosition) < minDistance)
{
isValid = false;
break;
}
}
}
return isValid;
}
}
You do not need the modified Start() as long as you set Player in the inspector.
@$$anonymous$$avina Thank you so much for your brilliant code! But I just can't get around to it, the enemies still can spawn on the player, I wrote everything down just as you wrote but it still won't work, any idéas?
Here is a modified version of Valid$$anonymous$$inimumDistance() with a couple of enhancements
bool Valid$$anonymous$$inimumDistance(Vector3 enemyPosition)
{
bool isValid = true;
$$anonymous$$Distance = $$anonymous$$athf.Abs($$anonymous$$Distance);
if (player != null)
{
isValid = ($$anonymous$$athf.Abs(Vector3.Distance(player.position, enemyPosition)) > $$anonymous$$Distance);
}
if (isValid && (activeEnemies.Count > 0))
{
for (int i = 0; i < activeEnemies.Count; i++)
{
if ($$anonymous$$athf.Abs(Vector3.Distance(activeEnemies[i].transform.position, enemyPosition)) < $$anonymous$$Distance)
{
isValid = false;
break;
}
}
}
return isValid;
}
Try this first, and even though you probably have tried this already you can also try playing with the $$anonymous$$Distance value in the inspector with the modified code if necessary.
If you are still having issues here is a version of Valid$$anonymous$$inimumDistance() with debug statements (Depending on how many enemies you have there will be a lot. You might want to start by adding only a few at a time)
bool Valid$$anonymous$$inimumDistance(Vector3 enemyPosition)
{
bool isValid = true;
$$anonymous$$Distance = $$anonymous$$athf.Abs($$anonymous$$Distance);
float dist;
if (player != null)
{
dist = $$anonymous$$athf.Abs(Vector3.Distance(player.position, enemyPosition));
isValid = (dist > $$anonymous$$Distance);
Debug.Log("player.position = " + player.position + ", spawned position = " + enemyPosition + ", calculated distance = " + dist + ", isValid = " + (isValid ? "True" : False"));
}
if (isValid && (activeEnemies.Count > 0))
{
for (int i = 0; i < activeEnemies.Count; i++)
{
dist = $$anonymous$$athf.Abs(Vector3.Distance(activeEnemies[i].transform.position, enemyPosition));
Debug.Log("activeEnemies[" + i + "].position = " + activeEnemies[i].transform.position + ", spawned position = " + enemyPosition + ", calculated distance = " + dist + ", isValid = " + (isValid ? "True" : False"));
if (dist < $$anonymous$$Distance)
{
isValid = false;
break;
}
}
}
Debug.Log("");
return isValid;
}
Also change this
public static List<GameObject> activeEnemies;
to this
public static List<GameObject> activeEnemies = new List<GameObject>();
@$$anonymous$$avina Brilliant program$$anonymous$$g my friend! I got it to work, I can't believe I didn't notice the error beforehand, I had set the $$anonymous$$Distance to a too low number!
BUT I still have a little problem. :( I have another script for a power up that the player can use, this means that if the power up is activated the player can "kill" the enemies by colliding with them. For now I just simply use the Destroy function to delete the enemies and this is where the problem starts with the new spawn code. This is where the error happens:
bool Valid$$anonymous$$inimumDistance(Vector3 enemyPosition)
{
bool isValid = true;
$$anonymous$$Distance = $$anonymous$$athf.Abs($$anonymous$$Distance);
if (player != null)
{
isValid = ($$anonymous$$athf.Abs(Vector3.Distance(player.position, enemyPosition)) > $$anonymous$$Distance);
}
if (isValid && (activeEnemies.Count > 0))
{
for (int i = 0; i < activeEnemies.Count; i++)
{
**if ($$anonymous$$athf.Abs(Vector3.Distance(activeEnemies[i].transform.position, enemyPosition)) < $$anonymous$$Distance)** // HERE IS THE ERROR
{
isValid = false;
break;
}
}
}
return isValid;
I get the usual message: "The object of type 'GameObject' has been destroyed but you are still trying to access it."
What could be the best way to fix this? Set the object to null?
Also, here is a little test editor script that you can use which I have already tested and it works fine. You need to place it in your Editor folder. If you do not have one create one. It allows you to test without running the game. There will be a Spawn Enemty button in the inspector. Just press the button to spawn an enemy.
using UnityEngine;
using System;
using UnityEditor;
[CustomEditor(typeof(EnemySpawn))]
[System.Serializable]
public class EnemySpawnCustomEditor: Editor
{
public override void OnInspectorGUI()
{
var target_cs = (EnemySpawn)target;
DrawDefaultInspector();
EnemySpawn enemySpawnScript = target_cs;
if(!Application.isPlaying)
{
if(GUILayout.Button("Spawn Enemy"))
{
enemySpawnScript.DoSpawn$$anonymous$$ineEnemy();
}
}
}
}
$$anonymous$$ake sure that everything is set in EnemySpawn and your player is visible I found large numbers can hang you up after too many spawns.
@$$anonymous$$avina This is genius, I don't know what to say, thank you so freaking much for all your help my friend, everything works perfectly now thanks to you! Take my "follow", this is such an inspiration, makes me want to become a better programmer, thank you once again!
Answer by DiGiaCom-Tech · May 25, 2016 at 07:39 PM
@Internetman ... Just shooting from the hip here ;)
First ... You might try adding a collider to the spawner object and then move it to the intended spawn point every iteration. If the collider is triggered (e.g. it is in a collision with something) then you can test if that is OK ... if not then regenerate the intended spawn point ... rinse and repeat until a spawn point is generated that is not in collision with anything undesirable.
OR ... If you have a small set of spawned objects, you could test the distance from the generated spawn point to each object in the collection of spawned objects and if it's too small/close then step out and regenerate another spawn point.
OR ... you could cast a ray down from some point directly above the intended spawn point ...
Vector3 RayCastPoint = new Vector3(SpawnPoint.x, SpawnPoint.y + 100f, SpawnPoint.z);
... if the ray collides with something undesirable then regenerate the intended spawn point and test again. I do this in my spawner to test for collisions with water layers and other world objects. Only problem with this is the ray is infinitely narrow and could miss collision with something by an infinitesimally small amount.
Finally ... you could leave the spawner code alone and add logic to the spawned object in it's Awake/Start methods to check where it spawned in at. If the location is undesirable then it will destroy itself and decrement any counters. If it's OK then it will initialize as needed and go about its business. Note that the spawner should keep track of how many it can and has spawned (e.g. 3 of 5 have spawned in). As the spawner still has more items to spawn things move on without incident.