I'm making a rhythm game and and working on a script that spawns an object on every beat of a song. Thus, it spawns a single object, waits for the length of the beat, and then spawns the next one. It's supposed to do this until the song finishes. The issue is that the script seems to be spawning everyone of the objects at once, rather than waiting for the specified amount of time. The script compiles fine and I can't figure out what the issue is. Here's my script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class spawnState : MonoBehaviour
{
public float waitTime = 0.483870968f;
public bool running = true;
public int beats = 504;
private int count = 0;
public GameObject spawn;
// Update is called once per frame
void Update()
{
//Generate object
while (running == true)
{
//Wait
StartCoroutine(Wait());
//Count & Stop
count += 1;
if (count >= beats)
{
running = false;
}
}
}
//Wait function
IEnumerator Wait()
{
//Wait
yield return new WaitForSeconds(waitTime);
//Spawn
Instantiate(spawn, transform.position, spawn.transform.rotation);
}
}
Thank you!
For starters you don't want any possibly blocking while loop within Update!
Update is called once every frame.
Do you really want to instantiate 504 objects within one frame?
And then StartCoroutine does not delay the method which calls it. It rather registers a Coroutine to be running and continues immediately with the rest of the code. So you are starting 504 "parallel" running Coroutines and after 0.48.. seconds you suddenly get all 504 objects instantiated at the same time.
What you rather want to do would e.g. be either directly using a timer field without a Coroutine
private float timer;
void Update()
{
if(!running) return;
timer += Time.deltaTime;
if(timer >= waitTime)
{
timer = 0;
Instantiate(spawn, transform.position, spawn.transform.rotation);
count += 1;
if (count >= beats)
{
running = false;
}
}
}
Or if you want to use a Coroutine you could e.g. simply do
private void Start ()
{
StartCorouine (SpawnRoutine());
}
private IEnumerator SpawnRoutine ()
{
for(var count = 0; count < beats; count++)
{
yield return new WaitForSeconds (waitTime);
Instantiate(spawn, transform.position, spawn.transform.rotation);
}
}
or if Start is anyway the only place where you trigger this it can be that routine itself
private IEnumerator Start ()
{
for(var count = 0; count < beats; count++)
{
yield return new WaitForSeconds (waitTime);
Instantiate(spawn, transform.position, spawn.transform.rotation);
}
}
Related
I tried to make a mob spawner which does followings:
1) spawns pre-defined amount of mobs with time interval of choice
2) checks if spawned gameobject destroyed if so spawns new ones till it reaches maximum amount again
Code works but i still think there can be improvements i want it to be mmo like slot spawner with pre defined maximum mob amount and intervals between every spawn
Issues im having:
1) at start works properly by 5 sec intervals between spawns but sometimes after you delete gameobject next spawn in line spawns instantly or very quickly
private void Start()
{
spawnedCount = gameObject.transform.childCount;
if (spawnedCount != 6)
isSpawning = false;
}
private void Update()
{
spawnedCount = gameObject.transform.childCount;
if (!isSpawning && spawnedCount < maxSpawnCount + 1) // check if should start spawn status and if coroutine currently working already isSpawning = false > yes you can can if you want to spawn or not isSpawning = True > no it is already spawning you cant check anymore and send requests
{
isSpawning = true; // set spawning status to true
StartCoroutine(DelayedSpawn(delayInterval));
}
}
IEnumerator DelayedSpawn(float delay)
{
yield return new WaitForSecondsRealtime(delay);
if (spawnedCount <= maxSpawnCount)
{
GetRandomZombie(); // gets random zombie prefab to instantiate
spawnedObj = Instantiate(PrefabToSpawn, new Vector3(gameObject.transform.position.x + Random.Range(-5f, 5), 0, gameObject.transform.position.z + Random.Range(-5f, 5)), transform.rotation);
spawnedObj.transform.parent = gameObject.transform;
}
if (spawnedCount <= maxSpawnCount )
StartCoroutine(DelayedSpawn(delayInterval));
else if (spawnedCount == 6)
{
isSpawning = false;
yield break;
}
}
I would do this like this. It seems cleaner.
Also I would suggest GetRandomZombie() method to return a prefab. That would be cleaner as well.
A reminder: The game object with script that has the coroutine method is disabled/deleted somehow, unity gives an error and coroutine stops. You can solve this like this:
private void OnDisable() //To prevent the error
{
StopAllCoroutines();
}
private void OnEnable() //To start the coroutine again whenever the spawner object is enabled again.
{
StartCoroutine(DelayedSpawn());
}
private IEnumerator DelayedSpawn()
{
while (true)
{
yield return new WaitForSecondsRealtime(delayInterval);
if (gameObject.transform.childCount <= maxSpawnCount)
{
GetRandomZombie(); // gets random zombie prefab to instantiate
spawnedObj = Instantiate(PrefabToSpawn, new Vector3(gameObject.transform.position.x + Random.Range(-5f, 5), 0, gameObject.transform.position.z + Random.Range(-5f, 5)), transform.rotation);
spawnedObj.transform.parent = gameObject.transform;
}
}
}
I am spawning objects on startup,(maxObj = 75) then destroying Obj's on event and disabling spawner Obj. When player wants they can re enable spawner. I need count to start at 0 on enable. Another 75 obj's spawn and are then destroyed. etc. Appreciate any help thanks.
enter code here
private static readonly float _spawnTime = 0.125f;
[SerializeField]
private GameObject _asteroidObject = null;
[SerializeField]
private int _maxObjects = 0;
private int _spawnedObjects = 0;
private float _time = 0;
private void Update()
{
if(_spawnedObjects < _maxObjects)
{
if(_time > _spawnTime)
{
Instantiate(_asteroidObject, transform.position, Quaternion.identity);
++_spawnedObjects;
_time = 0;
}
_time += Time.smoothDeltaTime;
}
}
Touch it is quite unclear how a User shall be able to start the spawn again I would recommend to rather use a Coroutine in general. They are like little temporary Update blocks but easier to control and maintain. It is also more efficient since it doesn't call the Update method every frame also when the maxObjects amount is already reached and nothing is going to happen anyway
[SerializeField]
private GameObject _asteroidPrefab;
[SerializeField]
private float _spawnInterval = 0.125f;
[SerializeField]
private int _maxObjects;
private bool _canStart = true;
// However your player calls this method
public void StartSpawn()
{
// Only start spawning if there is no other spawn routine already running
if(_canStart) StartCoroutine(Spawn());
}
private IEnumerator Spawn()
{
// Just in case ignore if another routine is already running
if(!_canStart) yield break;
// block concurrent routines
_canStart = false;
var interval = new WaitForSeconds(_spawnInterval);
for(var i = 0; i < _maxObjects; i++)
{
Instantiate(_asteroidObject, transform.position, Quaternion.identity);
// yield makes Unity pause this routine and render the frame
// and continue from here in the next frame
// Then since we yield return another IEnumerator in thi case WaitForSconds
// it results exactly in this: Holds here for given seconds and then goes to the next iteration
yield return interval;
}
// re-enable the StartSpawn
_canStart = true;
}
Then in case you additionally also want to automatically start spawning in the beginning you can simply call it in
private void Start()
{
StartSpawn();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpawnObj : MonoBehaviour
{
[SerializeField]
private GameObject _asteroidPrefab;
[SerializeField]
private float _spawnInterval = 0.125f;
[SerializeField]
private int _maxObjects;
private bool _canStart = true;
private void Start()
{
StartSpawn();
}
// However your player calls this method
public void StartSpawn()
{
// Only start spawning if there is no other spawn routine already running
if (_canStart) StartCoroutine(Spawn());
}
private IEnumerator Spawn()
{
// Just in case ignore if another routine is already running
if (!_canStart) yield break;
// block concurrent routines
_canStart = false;
var interval = new WaitForSeconds(_spawnInterval);
for (var i = 0; i < _maxObjects; i++)
{
Instantiate(_asteroidPrefab, transform.position, Quaternion.identity);
// yield makes Unity pause this routine and render the frame
// and continue from here in the next frame
// Then since we yield return another IEnumerator in thi case WaitForSconds
// it results exactly in this: Holds here for given seconds and then goes to the next iteration
yield return interval;
}
// re-enable the StartSpawn
_canStart = true;
}
}
I'm trying to create a wave spawner for a top down game that I'm creating. I have created the wave spawner script but when I hit play, nothing happens. The countdown doesn't begin. Ideally it should start from 2, once it reaches 0, the first wave should spawn with one enemy. Once that enemy is killed, the countdown should begin from 5 and once 0 is reached, the next wave with 2 enemies should begin and so on. The new wave should not begin until all the current enemies are destroyed.
public enum SpawnState { SPAWNING, WAITING, COUNTING };
public SpawnState state = SpawnState.COUNTING;
public Transform enemy;
public float timeBetweenWaves = 5f;
public float countDown = 2f;
private int waveIndex = 0;
public float searchCountdown = 1f;
void Update()
{
if (state == SpawnState.WAITING)
{
if (!EnemyisAlive())
{
WaveCompleted();
}
else
{
return;
}
}
if (countDown <= 0f)
{
if (state != SpawnState.SPAWNING)
{
StartCoroutine(SpawnWave());
countDown = timeBetweenWaves;
}
else
{
countDown -= Time.deltaTime;
}
}
}
void WaveCompleted()
{
state = SpawnState.COUNTING;
countDown = timeBetweenWaves;
SpawnWave();
}
bool EnemyisAlive()
{
searchCountdown -= Time.deltaTime;
if (searchCountdown <= 0)
{
searchCountdown = 1f;
if (GameObject.FindGameObjectsWithTag("Enemy").Length == 0)
{
return false;
}
}
return true;
}
IEnumerator SpawnWave()
{
state = SpawnState.SPAWNING;
waveIndex++;
for (int i = 0; i < waveIndex; i++)
{
SpawnEnemy();
yield return new WaitForSeconds(0.5f);
}
state = SpawnState.WAITING;
yield break;
}
void SpawnEnemy()
{
Instantiate(enemy, transform.position, transform.rotation);
}
I would recommend you to use a Coroutine for all of it. That makes some things easier. You can e.g. simply wait until another Ienumertaor is finished. Then I would simply add the spawned enemies to a list, filter it for null entries and use the count. Using Find or in your case FindGameObjectsWithTag each frame is highly inefficient!
using System.Linq;
using System.Collections.Generic;
...
public Transform enemy;
public float timeBetweenWaves = 5f;
public float countDown = 2f;
//public float searchCountdown = 1f;
private List<Transform> enemies = new List<Transform>();
private int waveIndex = 0;
private void Start()
{
StartCoroutine(RunSpawner());
}
// this replaces your Update method
private IEnumerator RunSpawner()
{
// first time wait 2 seconds
yield return new WaitForSeconds(countDown);
// run this routine infinite
while(true)
{
state = SpawnState.SPAWNING;
// do the spawning and at the same time wait until it's finished
yield return SpawnWave();
state = SpawnState.WAITING;
// wait until all enemies died (are destroyed)
yield return new WaitWhile(EnemyisAlive);
state = SpawnState.COUNTING
// wait 5 seconds
yield return new WaitForSeconds(timeBetweenWaves);
}
}
private bool EnemyisAlive()
{
// uses Linq to filter out null (previously detroyed) entries
enemies = enemies.Where(e => e != null).ToList();
return enemies.Count > 0;
}
private IEnumerator SpawnWave()
{
waveIndex++;
for (int i = 0; i < waveIndex; i++)
{
SpawnEnemy();
yield return new WaitForSeconds(0.5f);
}
}
private void SpawnEnemy()
{
enemies.Add(Instantiate(enemy, transform.position, transform.rotation));
}
To be slightly more efficient you could also avoid instantiating and destroying but rather use Object Pooling - only enabling and disabling the objects and eventually spawn new ones only when needed.
in my project im trying to count the diferent objects and simulate a little animation, for example i have stars in my game, and i want to count the number of stars in the final of the game from 0 trough the number of stars the user got, so i did this:
public void youWin()
{
audio.Stop ();
StartCoroutine (activatePanel ());
}
IEnumerator activatePanel()
{
yield return new WaitForSeconds (3f);
pausePanel2.SetActive (true);
for (int i = 0; i <= stars; i++) {
yield return new WaitForSeconds (0.2f);
starText2.text = i + "";
}
}
my code worked well for 0.3f on the for loop wait for seconds, but it is too slow, i want it for 0.2f, but something strange happen sometimes it get like a bug and the first number seems to go back, it doesn't count right, someone know what is happening?
It very likely that the activatePanel function is being called from another place while it is already running or the script that contains this code is attached to multiple GameObjects and the activatePanel is again, being called by another function. You can use flag to stop this from happening.
If the coroutine function is already running, use yield break; to break out of it.
bool isRunning = false;
IEnumerator activatePanel()
{
//Exit if already running
if (isRunning)
{
yield break;
}
//Not running, now set isRunning to true then run
isRunning = true;
yield return new WaitForSeconds(3f);
pausePanel2.SetActive(true);
WaitForSeconds waitTime = new WaitForSeconds(0.2f);
for (int i = 0; i <= stars; i++)
{
yield return waitTime;
starText2.text = i.ToString();
}
//Done running, set isRunning to false
isRunning = false;
}
Well i solved it with all of you guys help, actually you all where right, i thaught i was calling the youWin function just 1 time, but i forgot this is unity and i called the youWin inside a trigerEnter function, that means that the object keep enter the triger function and called the youWin function, thank you all here is what i mean with that
Solved it with the bool entered
public class Victory : MonoBehaviour {
Manager gameManager;
// Use this for initialization
public AudioClip clip;
private AudioSource audio;
public Animator girl;
private bool entered;
void OnTriggerEnter(Collider c)
{
if (c.gameObject.tag == "Player" && !entered) {
gameManager.win = true;
audio.clip = clip;
audio.Play ();
gameManager.Ball.GetComponent<MoveBall> ().enabled = true;
girl.SetBool ("win",true);
entered = true;
gameManager.youWin ();
}
}
void Start () {
gameManager = GameObject.Find ("GameController").GetComponent<Manager> ();
audio = GetComponent<AudioSource> ();
entered = false;
}
// Update is called once per frame
void Update () {
}
}
I'm still working on the game and i ran into another problem, I'm trying to make an infinite loop which waits for a couple seconds every execute, i currently have:
StartScript.cs
using UnityEngine;
using System.Collections;
using ProgressBar;
public class StartScript : MonoBehaviour {
private ProgressBarBehaviour hunger;
private ProgressBarBehaviour stamina;
private ProgressRadialBehaviour health;
private ProgressBarBehaviour thirst;
public void OnGUI(){
this.hunger = GameObject.Find("hungerBar").GetComponent<ProgressBarBehaviour>();
this.stamina = GameObject.Find("staminaBar").GetComponent<ProgressBarBehaviour>();
this.health = GameObject.Find("healthBar").GetComponent<ProgressRadialBehaviour>();
this.thirst = GameObject.Find("thirstBar").GetComponent<ProgressBarBehaviour>();
this.health.Value = 100f;
this.stamina.Value = 100f;
StartCoroutine ("runHunger");
}
bool addBar(ProgressBarBehaviour target, float percentage){
Debug.Log (percentage);
if ((target.Value + percentage) <= 100f) {
target.IncrementValue (percentage);
return true;
}
return false;
}
bool damageBar(ProgressBarBehaviour target, float percentage){
if ((target.Value - percentage) >= 0f) {
target.DecrementValue (percentage);
return true;
}
return false;
}
bool addRadial(ProgressRadialBehaviour target, float percentage){
if ((target.Value + percentage) <= 100f) {
target.IncrementValue (percentage);
return true;
}
return false;
}
bool damageRadial(ProgressRadialBehaviour target, float percentage){
if ((target.Value - percentage) >= 0f) {
target.DecrementValue (percentage);
return true;
}
return false;
}
IEnumerator runHunger(){
while (true) {
yield return new WaitForSeconds(10f);
/*if (!this.addBar(this.hunger,5f)) {
this.damageRadial(this.health,3f);
}*/
Debug.Log("Time: "+Time.time);
}
}
IEnumerator runHealth(){
while (true) {
}
}
/*
IEnumerator runThirst(){
while (true) {
if (!this.thirst.Add (2)) {
this.health.Damage(8);
}
}
}*/
}
As you guys can see I'm trying to make a while(true){} loop with an yield return new WaitForSeconds(), the idea is that it runs the function in the loop every (in this test case) 10 seconds.
The first time executing works like a charm, it waits for 10 seconds, but after that it only waits for like 0.1 seconds and executes again.
I hope someone can help me with this problem.
There are too many wrong things I currently see in your code.
This:
IEnumerator runHealth(){
while (true) {
}
}
Change it to
IEnumerator runHealth(){
while (true) {
yield return null;
}
}
Also This:
IEnumerator runThirst(){
while (true) {
if (!this.thirst.Add (2)) {
this.health.Damage(8);
}
}
}
Change it to
IEnumerator runThirst(){
while (true) {
if (!this.thirst.Add (2)) {
this.health.Damage(8);
}
yield return null;
}
}
If you are gonna have while(true) in a coroutine, you must have yield return something or it will lock your program. Fix these problems first. It is good to have it just before the closing bracket of the while loop.
I am sure you did this in other scripts too.
Go over the rest of other scripts and do the same thing. Put yield return null inside each while loop in each coroutine function.
ANOTHER BIG MISTAKE:
Remove every code from OnGUI function and put it in Start function like below. The code is being called too several times per frame and that will lead to slow down as you will be creating many many coroutines that never stops due to your while loop in the coroutine function.Putting it in the Start function will call it once.
void Start()
{
this.hunger = GameObject.Find("hungerBar").GetComponent<ProgressBarBehaviour>();
this.stamina = GameObject.Find("staminaBar").GetComponent<ProgressBarBehaviour>();
this.health = GameObject.Find("healthBar").GetComponent<ProgressRadialBehaviour>();
this.thirst = GameObject.Find("thirstBar").GetComponent<ProgressBarBehaviour>();
this.health.Value = 100f;
this.stamina.Value = 100f;
StartCoroutine ("runHunger");
}
Call the StartCoroutine ("runHunger");
somewhere other than in OnGUI(). It will work then. Since everytime OnGUI is called it starts a new coroutine
Call the StartCoroutine in Start() or someplace else