I'm running a coroutine where a GameObject's children's material.shaders are being manipulated. I just can't seem to figure out for the life of me how to make the coroutine wait until all of the materials are at the desired blend level, before going on to the next step. Everything's working just fine, I just need the coroutine not to continue past line 28 until all the materials are ChangedToWhite. Any thoughts?
public GameObject changeInto;
private bool coroutineStarted = false;
private Renderer[] renderersArray;
private List<Material> materialsList = new List<Material>();
private GameObject newForm;
void Start(){
renderersArray = this.gameObject.GetComponentsInChildren<Renderer>();
}
void Update(){
if(!coroutineStarted){
coroutineStarted = true;
StartCoroutine(Change());
}
}
private IEnumerator Change(){
foreach(Renderer renderer in renderersArray){
for(int i = 0; i < renderer.materials.Length; i++){
if(renderer.materials[i].shader.name == "Toon/Basic Blender"){
materialsList.Add(renderer.materials[i]);
}
}
}
foreach(Material material in materialsList){
StartCoroutine(ChangeToWhite(material));
}
animation.Play("Evolution");
newForm = Instantiate(changeInto, this.transform.position, this.transform.rotation) as GameObject;
yield return null;
}
private IEnumerator ChangeToWhite(Material mat){
float counter = mat.GetFloat("_Blend");
while(counter != 1f){
float increase = mat.GetFloat("_Blend") + 0.01f;
mat.SetFloat("_Blend", increase);
counter += increase;
yield return null;
}
}
this part:
foreach(Material material in materialsList){
StartCoroutine(ChangeToWhite(material));
}
has to contain the wait time of the change to white routine. In your case, it would be variable because you are not using Time.deltaTime which is HIGHLY encouraged by the way.
EDIT: here is the code you may want to use
foreach(Material material in materialsList){
StartCoroutine(ChangeToWhite(material));
yield return new WaitForSeconds(1);
}
private IEnumerator ChangeToWhite(Material mat){
float counter = mat.GetFloat("_Blend");
while(counter != 1f){ //the 1 here is the time to wait
float increase = mat.GetFloat("_Blend") + Time.deltaTime;
mat.SetFloat("_Blend", increase);
counter += increase;
yield return null;
}
}
Related
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GenerateWalls : MonoBehaviour
{
public GameObject gameObjectToRaise;
public float duration;
public Vector3 raiseAmount;
public bool go = false;
public Color[] colors = new Color[4];
public bool randomColors = false;
private GameObject objtoraise;
private GameObject[] walls;
private bool scaleOver = false;
private void Start()
{
Init();
ColorWalls();
// The z Axis must be minimum 1 or any value above 0 could be also 0.1f
// but it's better to keep it minimum as 1 by default.
if (raiseAmount.z < 1)
{
raiseAmount.z = 1f;
}
if (go)
{
StartCoroutine(ScaleOverSeconds(objtoraise, new Vector3(raiseAmount.x, raiseAmount.y,
raiseAmount.z), duration));
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.R))
{
//if (scaleOver)
//{
if (objtoraise != null)
{
if (raiseAmount.z < 1)
{
raiseAmount.z = 1f;
}
Destroy(objtoraise);
Init();
ColorWalls();
StartCoroutine(ScaleOverSeconds(objtoraise, new Vector3(raiseAmount.x, raiseAmount.y,
raiseAmount.z), duration));
scaleOver = false;
//}
}
}
}
private void Init()
{
objtoraise = Instantiate(gameObjectToRaise);
objtoraise.name = "Walls";
walls = GameObject.FindGameObjectsWithTag("Wall");
}
public IEnumerator ScaleOverSeconds(GameObject objectToScale, Vector3 scaleTo, float seconds)
{
float elapsedTime = 0;
Vector3 startingScale = objectToScale.transform.localScale;
while (elapsedTime < seconds)
{
objectToScale.transform.localScale = Vector3.Lerp(startingScale, scaleTo, (elapsedTime / seconds));
elapsedTime += Time.deltaTime;
yield return new WaitForEndOfFrame();
}
objectToScale.transform.localScale = scaleTo;
scaleOver = true;
}
private void ColorWalls()
{
for (int i = 0; i < walls.Length; i++)
{
if (randomColors)
{
walls[i].transform.GetChild(0).GetComponent<Renderer>().material.color
= GetRandomColour32();
}
else
{
walls[i].transform.GetChild(0).GetComponent<Renderer>().material.color = colors[i];
}
}
}
private Color32 GetRandomColour32()
{
//using Color32
return new Color32(
(byte)UnityEngine.Random.Range(0, 255), //Red
(byte)UnityEngine.Random.Range(0, 255), //Green
(byte)UnityEngine.Random.Range(0, 255), //Blue
255 //Alpha (transparency)
);
}
}
Inside the Update() when I press the R key it's destroying the Instantiated object and then Instantiate is again and start the coroutine again. The problem is when I press on the R key many times in a row after two times I'm getting MissingReferenceException exception in the editor :
MissingReferenceException: The object of type 'GameObject' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
GenerateWalls+d__12.MoveNext () (at Assets/Scripts/GenerateWalls.cs:81)
Line 81 is :
objectToScale.transform.localScale = Vector3.Lerp(startingScale, scaleTo, (elapsedTime / seconds));
The goal is to be able to generate the walls each time over again when pressing R it should stop the current coroutine and start over.
Maybe the problem is that it's in the middle of the coroutine and because the old coroutine didn't stop yet then the object is missing in the middle because I destroy it?
How then I should do it to be able to press R over and over again and it will start the coroutine over and over? Not to start multiple coroutines but to start each time the coroutine over again.
The solution is to add : StopCoroutine In the Update()
This is not the solution. I thought stopping the coroutine will stop also the while loop inside but it didn't. It seems that checking for null inside the while loop solved the problem :
public IEnumerator ScaleOverSeconds(GameObject objectToScale, Vector3 scaleTo, float seconds)
{
if (objectToScale != null)
{
float elapsedTime = 0;
Vector3 startingScale = objectToScale.transform.localScale;
while (elapsedTime < seconds)
{
if (objectToScale == null)
{
yield return null;
}
else
{
objectToScale.transform.localScale = Vector3.Lerp(startingScale, scaleTo, (elapsedTime / seconds));
elapsedTime += Time.deltaTime;
yield return new WaitForEndOfFrame();
}
}
objectToScale.transform.localScale = scaleTo;
scaleOver = true;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Rotate : MonoBehaviour
{
public GameObject[] objectsToRotate;
public float duration = 5f;
public static bool desiredAngle = false;
private Vector3 lastFwd;
private bool startRot = true;
private void OnMouseDown()
{
if (startRot == true)
{
startRot = false;
StartCoroutine(StartRotationOfObjects());
}
}
private IEnumerator StartRotationOfObjects()
{
for (int i = 0; i < objectsToRotate.Length; i++)
{
// Random wait period before rotation starts
if (i == 0)
{
yield return new WaitForSeconds(0);
}
else
{
yield return new WaitForSeconds(Random.Range(0, 2f));
}
StartCoroutine(Rotates(objectsToRotate[i].transform, duration));
}
startRot = true;
}
private IEnumerator Rotates(Transform objectToRotate, float duration)
{
Quaternion startRot = objectToRotate.rotation;
float t = 0.0f;
lastFwd = objectToRotate.transform.forward;
while (t < duration)
{
t += Time.deltaTime;
objectToRotate.rotation = startRot * Quaternion.AngleAxis(t / duration * 360f, Vector3.up);
var curFwd = objectToRotate.transform.forward;
// measure the angle rotated since last frame:
var ang = Vector3.Angle(curFwd, lastFwd);
if (myApproximation(ang, 179f, 1f) == true)
{
desiredAngle = true;
}
yield return null;
}
objectToRotate.rotation = startRot;
desiredAngle = false;
}
private bool myApproximation(float a, float b, float tolerance)
{
return (Mathf.Abs(a - b) < tolerance);
}
}
I want to disable the OnMouseDown code so I will not be able to execute the Coroutine nonstop times. And after all the objects finished rotating then to enable the OnMouseDown again. I'm using the startRot flag for that but still it's true all the time and I can keep start the coroutine inside the OnMouseDown nonstop.
Below is a simplified version of your code.
private void OnMouseDown()
{
if (startRot == true)
{
startRot = false;
StartCoroutine(StartRotationOfObjects());
}
}
private IEnumerator StartRotationOfObjects()
{
for (int i = 0; i < objectsToRotate.Length; i++)
{
if (i == 0)
{
yield return new WaitForSeconds(0);
}
else
{
yield return new WaitForSeconds(Random.Range(0, 2f));
}
StartCoroutine(Rotates(objectsToRotate[i].transform, duration));
}
startRot = true;
}
private IEnumerator Rotates(Transform objectToRotate, float duration)
{
while (condition)
{
yield return null;
}
}
First, you have the OnMouseDown checking if true and it is on first run so it enters, sets it to false and start the coroutine.
Moving on with the loop. The first if check is useless since wait for 0 seconds. But it actually impacts the experience. So on first run, it enters the statement and basically does not wait. It then moves on to start the Rotates coroutine.
Entering the coroutine, it will reach a yield statement, place the coroutine in a list of coroutines to run and return. Back in the loop, it runs again, this time it will wait for to 2 seconds and run the next coroutine and so on until last one of the objectsToRotate collection.
Loop is done, startRot is set back to true. This means, even though, your coroutines may not be done, you can trigger new ones.
Your solution, either you want to wait for one rotation to be done before starting a new one with:
if (i == 0)
{
yield return new WaitForSeconds(0);
}
else
{
yield return new WaitForSeconds(Random.Range(0, 2f));
}
yield StartCoroutine(Rotates(objectsToRotate[i].transform, duration));
or you need to keep track of any coroutine running:
private int index = 0;
private IEnumerator StartRotationOfObjects()
{
for (int i = 0; i < objectsToRotate.Length; i++)
{
if (i == 0)
{
yield return new WaitForSeconds(0);
}
else
{
yield return new WaitForSeconds(Random.Range(0, 2f));
}
StartCoroutine(Rotates(objectsToRotate[i].transform, duration);
}
while(index > 0){ yield return null; }
startRot = true;
}
private IEnumerator Rotates(Transform objectToRotate, float duration)
{
index++;
while (condition)
{
yield return null;
}
index--;
}
Now anytime you start a coroutine, index is increased, at the end of the coroutine, it is decreased, in the main coroutine, you yield until index is back to 0.
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.
This is the part i'm trying to use the StartCoroutine:
//StartCoroutine(movement());
}
IEnumerator movement()
{
player.localPosition += selectedDirection;
FindDirections();
yield return new WaitForSeconds(0.5f);
}
Now i'm not using it but when i do use it i'm getting this error and the program is close:
Then i have to select or Debug or Close Program
What i want is to make the player to change position each 0.5 seconds.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System.IO;
public class PathFinder : MonoBehaviour
{
public Transform player;
public float playerMoveSpeed = 1f;
public float playerRotationSpeed = 0.5f;
public float distanceToTravel = 1f;
public bool randomPath = true;
public List<Vector3> possibleDirections = new List<Vector3>();
public Vector3 selectedDirection;
private Transform start;
private Transform end;
private GridGenerator gridgenerator;
private float m_distanceTraveled = 0f;
private List<Vector3> visitedList = new List<Vector3>();
private List<Vector3> toBeVisitedList = new List<Vector3>();
private Vector3 playerPosition;
private const float margin = 0.001f;
public void FindPath()
{
gridgenerator = GetComponent<GridGenerator>();
GenerateStartEnd();
FindDirections();
m_distanceTraveled = 0;
}
private void FindDirections()
{
possibleDirections = new List<Vector3>();
playerPosition = player.localPosition;
m_distanceTraveled = 0;
if (playerPosition.x > 1)
{
// can go left
possibleDirections.Add(Vector3.left);
}
if (playerPosition.x + gridgenerator.spaceBetweenBlocks < gridgenerator.gridWidth * gridgenerator.spaceBetweenBlocks)
{
// can go right
possibleDirections.Add(Vector3.right);
}
if (playerPosition.z > 1)
{
// can go backward
possibleDirections.Add(Vector3.back);
}
if (playerPosition.z + gridgenerator.spaceBetweenBlocks < gridgenerator.gridHeight * gridgenerator.spaceBetweenBlocks)
{
// can go forward
possibleDirections.Add(Vector3.forward);
}
if (randomPath == true)
{
selectedDirection = possibleDirections[Random.Range(0, possibleDirections.Count)];
}
player.forward = selectedDirection;
//StartCoroutine(movement());
}
IEnumerator movement()
{
player.localPosition += selectedDirection;
FindDirections();
yield return new WaitForSeconds(0.5f);
}
private void Update()
{
/*if (m_distanceTraveled < distanceToTravel)
{
Vector3 oldPosition = player.localPosition;
player.localPosition += selectedDirection * Time.deltaTime * playerMoveSpeed;
m_distanceTraveled += Vector3.Distance(oldPosition, player.localPosition);
}
if (m_distanceTraveled > distanceToTravel)
{
FindDirections();
}*/
}
private List<Vector3> GenerateStartEnd()
{
GameObject walls = GameObject.Find("Walls");
List<Transform> wallsParents = new List<Transform>();
List<Vector3> startEndPos = new List<Vector3>();
foreach (Transform child in walls.transform)
{
wallsParents.Add(child);
}
for (int i = 0; i < 2; i++)
{
wallsParents.Remove(wallsParents[Random.Range(0, wallsParents.Count)]);
}
var childsWall0 = wallsParents[0].GetComponentsInChildren<Transform>().ToList();
var childsWall1 = wallsParents[1].GetComponentsInChildren<Transform>().ToList();
childsWall0.RemoveAt(0);
childsWall1.RemoveAt(0);
start = childsWall0[Random.Range(0, childsWall0.Count)];
player.position = start.position;
end = childsWall1[Random.Range(0, childsWall1.Count)];
end.tag = "End";
startEndPos.Add(start.position);
startEndPos.Add(end.position);
start.GetComponent<Renderer>().material.color = Color.red;
end.GetComponent<Renderer>().material.color = Color.blue;
return startEndPos;
}
}
Inside the coroutine movement() you call FindDirections(). Inside FindDirections() you start movement(), now; Inside the coroutine movement() you call FindDirections()... and so on..
Also, you are invoking FindDirections() inside the Update() method. The method Update is called every frame (so if your game is running at 30FPS this Update will be executed 30 times every second), so, you are calling every frame a method A that will call another method B, that will call method A, etc.. I recommend you to be careful about what are you invoking inside Update().
So, probably you are getting a StackOverFlowException (yes, the same name that this website has). If for any reason Unity crash, a way to understand what happened is check the logs.
Mayo's answer explained your issue well. This shows you how to accomplish what you wanted to do.
I want is to make the player to change position each 0.5 seconds.
Instead of calling the movement function every time in the FindDirections function, call it once then execute your code in a while loop. This should fix your issue. Just move StartCoroutine(movement()); from the FindDirections function to the Start function or somewhere that calls it once. Below is your new movement code.
IEnumerator movement()
{
while (true)
{
player.localPosition += selectedDirection;
FindDirections();
yield return new WaitForSeconds(0.5f);
}
}
The while loop above will execute every 0.5f second.
I am a bit of a noob to programming and I am trying to make a GameObject , deactivate and reactivate over a set amount of seconds.For example I want my star to slowly blink before it goes away, to create a cool looking effect. If there is a better way of using this method done with out using SetActive(false) and what not , please feel free to give me your method - This is my code , Sorry if its messy i gotta get better at this but i will in due time
Thanks guys
//Timers
public float ScoreTimer;
public float[] TimeMarkStamp;
//Scoring
public int totalCollStars;
[Space]
public int maxStars = 5;
public GameObject[] StarImages;
// Use this for initialization
void Start()
{
}
// Update is called once per frame
void Update()
{
ScoreTimer += Time.deltaTime;
if (ScoreTimer <= TimeMarkStamp[0])
{
Debug.Log("It works");
StarImages[0].SetActive(false);
}
else if (ScoreTimer <= TimeMarkStamp[1])
{
Debug.Log("It workds" + TimeMarkStamp[1]);
StarImages[1].SetActive(false);
}
else if (ScoreTimer <= TimeMarkStamp[2])
{
Debug.Log("It works" + TimeMarkStamp[2]);
StarImages[2].SetActive(false);
}
else if (ScoreTimer <= TimeMarkStamp[3])
{
//This is not working
InvokeRepeating("flickerEffect", 3f, 1f);
}
}
void flickerEffect()
{
bool flickCheck = false;
if (flickCheck == false)
{
StarImages[3].SetActive(true);
flickCheck = true;
}
else if (flickCheck == true)
{
StarImages[3].SetActive(false);
flickCheck = false;
}
}
}
If there is a better way of using this method done with out using
SetActive(false) and what not
Yes, there is a better way, other than using the SetActive function. You should change the alpha color of the GameObject from 0 to 1 back and forth. After that you can then disable the GameObject with SetActive. This saves how much garbage would have been generated when repeatedly calling the SetActive function.
If this is a 3D GameObject, change the Rendering Mode from Opaque(default) to Fade or Transparent.
A simple function that can do this:
void blink(GameObject obj, float blinkSpeed, float duration)
{
StartCoroutine(_blinkCOR(obj, blinkSpeed, duration));
}
IEnumerator _blinkCOR(GameObject obj, float blinkSpeed, float duration)
{
obj.SetActive(true);
Color defualtColor = obj.GetComponent<MeshRenderer>().material.color;
float counter = 0;
float innerCounter = 0;
bool visible = false;
while (counter < duration)
{
counter += Time.deltaTime;
innerCounter += Time.deltaTime;
//Toggle and reset if innerCounter > blinkSpeed
if (innerCounter > blinkSpeed)
{
visible = !visible;
innerCounter = 0f;
}
if (visible)
{
//Show
show(obj);
}
else
{
//Hide
hide(obj);
}
//Wait for a frame
yield return null;
}
//Done Blinking, Restore default color then Disable the GameObject
obj.GetComponent<MeshRenderer>().material.color = defualtColor;
obj.SetActive(false);
}
void show(GameObject obj)
{
Color currentColor = obj.GetComponent<MeshRenderer>().material.color;
currentColor.a = 1;
obj.GetComponent<MeshRenderer>().material.color = currentColor;
}
void hide(GameObject obj)
{
Color currentColor = obj.GetComponent<MeshRenderer>().material.color;
currentColor.a = 0;
obj.GetComponent<MeshRenderer>().material.color = currentColor;
}
Usage:
void Start()
{
blink(gameObject, 0.2f, 5f);
}
If this is a SpriteRender, you have to replace all the obj.GetComponent<MeshRenderer>().material.color code with obj.GetComponent<SpriteRenderer>().color.