One of the scripts on my scene involves creating a grid of cubes. The process does take a fair amount of time and I've been trying to implement it without causing Unity to become unresponsive.
The code at present - works to effect - you can watch as the grid is generated. However, the other scripts in the scene are doing their tasks at the same time and is causing exceptions (they're accessing tiles on the grid which are not yet created).
The script below is the only script that has an 'Awake' function - the other scripts are executing 'Update' and 'OnGUI' before the first script has finished its awake.
GridGenerator.cs
public GameObject[,] tiles = new GameObject[Constants.BOARD_WIDTH, Constants.BOARD_HEIGHT];
// Run a double loop to create each tile.
void Awake ()
{
StartCoroutine(doSetup());
}
IEnumerator doSetup()
{
yield return StartCoroutine (loadLevel());
DoOtherStuff();
}
IEnumerator loadLevel()
{
for (int i = 0; i < Constants.BOARD_WIDTH; i++)
{
for (int j = 0; j < Constants.BOARD_HEIGHT; j++)
{
tiles[i,j] = newTile();
yield return(0);
}
}
}
How would I make the doOtherStuff() method wait until the co-routine is finished?
I've tried a boolean toggle (which froze unity)
I've tried using lock (which didn't work)
Or is there a more efficient way of doing this?
EDIT: The code successfully completes OtherStuff() after the the grid is generated. Apart of the doOtherStuff() is to pass a reference of the grid to a separate class.
The separate class is throwing exceptions when trying to access the grid in the Update method. Which suggests, the update method is being called before the Awake in the GridGenerator.cs is completed.
The easiest way would be to kick off your coroutine within another coroutine, so that you can yield to it:
void Awake ()
{
StartCoroutine(doSetup());
}
IEnumerator doSetup ()
{
yield return StartCoroutine(createGrid()); // will yield until createGrid is finished
doOtherStuff();
}
IEnumerator createGrid()
{
for (int i = 0; i < Constants.BOARD_WIDTH; i++)
{
for (int j = 0; j < Constants.BOARD_HEIGHT; j++)
{
// Instantiate a new tile
yield return(0);
}
}
}
}
Related
I have a basic loot table with weighted drop rarities. I am trying to make it so that when the game starts, it will re-roll if the item already exists in a duplicated list.
I've created an empty list in shopManagerScript and am adding each instantiated item to that list. Then I would like to check against that list to see if the item exists. If it does, I want to re-roll again. If it doesn't then go ahead and instantiate the item.
This current code is executing endlessly however, and is crashing my game.
public GameObject shopManager;
public ShopManager shopManagerScript;
[System.Serializable]
public class DropItem
{
public string name;
public GameObject item;
public int dropRarity;
}
public List<DropItem> ShopItemPool = new List<DropItem>();
private void Start()
{
shopManager = GameObject.FindGameObjectWithTag("ShopManager");
shopManagerScript = shopManager.GetComponent<ShopManager>();
SpawnItem();
}
void SpawnItem()
{
int itemWeight = 0;
for (int i = 0; i < ShopItemPool.Count; i++)
{
itemWeight += ShopItemPool[i].dropRarity;
}
int randomValue = Random.Range(0, itemWeight);
for (int i = 0; i < ShopItemPool.Count; i++)
{
if (randomValue <= ShopItemPool[i].dropRarity && !shopManagerScript.shopItems.Contains(ShopItemPool[i].item.ToString()))
{
Instantiate(ShopItemPool[i].item, transform.position, Quaternion.identity);
shopManagerScript.shopItems.Add(ShopItemPool[i].item.ToString());
return;
}
else
{
SpawnItem();
}
randomValue -= ShopItemPool[i].dropRarity;
}
}
The problem here is that SpawnItem method calls SpawnItem inside the for, which results in having more running SpawnItem. Then these running SpawnItem call more SpawnItem. The process continues until stack is overflowed and it falls with StackOverflowException.
In order to fix this you can use continue as mentioned before, but be careful with calling SpawnItem, because if the random keeps generating inappropriate values the method can still be called too many times and the error will be the same.
Another way to fix it is to remove recursive call of it and make another method that loops calling SpawnItem. Just make sure that the logic of looping doesn't fully rely on random, otherwise it's still possible to call the method too many times
I have written a function on a script that activates when the player loses all his lives. That calls a CoRoutine in a script attached to my main character that makes a simple death animation and then moves to the game over screen. Debug.Log shows that the function calls, and when I use non-CoRoutine functions attached to the main character, those functions call to. However, the CoRoutine itself never calls, not even showing the log of it ever activating? Does anyone know what is up?
Code included below:
if (GameObject.Find("Heart 1") == null)
{
Debug.Log("Naw");
player.DeathAnimation(20);
Debug.Log("still not working");
}
public IEnumerator DeathAnimation(int i)
{
int k = i;
Debug.Log("Numerator works");
transform.Rotate(Vector3.forward * 9);
yield return new WaitForSeconds(.08f);
k--;
Debug.Log(k);
if (k <= 0)
{
SceneManager.LoadScene("Game Over");
yield break;
}
StartCoroutine(DeathAnimation(k));
}
There doesn’t seem to be a reason to make this a recursive coroutine. I’d suggest removing the recursion which might also solve the issue you’re having or at least make it simpler to identify.
if (GameObject.Find("Heart 1") == null)
{
Debug.Log("Hmm");
StartCoroutine( player.DeathAnimation(20) );
Debug.Log("maybe working");
}
…
public IEnumerator DeathAnimation(int i)
{
Debug.Log("Numerator works");
for( ; i>0; i-- ) {
transform.Rotate(Vector3.forward * 9);
yield return new WaitForSeconds(.08f);
Debug.Log(i);
}
SceneManager.LoadScene("Game Over");
}
I have a game I am developing in Unity where AI is doing large calculations when it is its turn. It searches the position to depth 1, then 2, then 3 etc. Between each depth I want to instantiate a Gameobject with info about the depth to UI. The problem is that nothing happens until the AI is completely finished, then all items are added at once. Here is some code to explain better:
private void AIMakeMove()
{
for (int currentDepth = 1; currentDepth < maxDepth + 1; currentDepth++)
{
SearchPosition(currentDepth);
}
}
private void SearchPosition(int _currentDepth)
{
// Search the position to the given depth
score = Negamax(_currentDepth);
// Print things PROBLEM HERE
GameObject printItem = Instantiate(printItemPrefab, printItemParent.transform);
Debug.Log(_currentDepth);
}
I also tried just a simple Debug.Log instead of Instantiate but same thing happens then, all prints to console happens after the AI is done with its thinking process.
Why is my UI not updating with information? I tell it to create some things after it run the first iteration with depth 0 but it skips this step and goes on depth 2 instead. Can someone please let me know how to get information out between each depth?
The problem is that nothing happens until the AI is completely finished
well the UI is only updated if the Unity main-thread is allowed to finish a frame.
You, however, block the main thread until all iterations are finished.
If it is okey for you to block between each instantiation then you could simply use a Coroutine and do something like
private void AIMakeMove()
{
StartCoroutine(AIMakeMoveRoutine());
}
private IEnuemrator AIMakeMoveRoutine()
{
for (int currentDepth = 1; currentDepth < maxDepth + 1; currentDepth++)
{
SearchPosition(currentDepth);
// This now tells Unity to "interrupt" this routine here
// render the current frame and continue from here in the next frame
yield return null;
}
}
private void SearchPosition(int _currentDepth)
{
score = Negamax(_currentDepth);
GameObject printItem = Instantiate(printItemPrefab, printItemParent.transform);
Debug.Log(_currentDepth);
}
This will finish a frame and start a new one (thus refresh the UI) after each finished iteration.
However, if this still blocks the rest of your application too much you should additionally actually run the calculation async e.g. using a Task like
private void AIMakeMove()
{
StartCoroutine(AIMakeMoveRoutine());
}
private IEnuemrator AIMakeMoveRoutine()
{
for (int currentDepth = 1; currentDepth < maxDepth + 1; currentDepth++)
{
// you can yield another IEnuemrator -> executes this and waits for it to finish
yield return SearchPosition(currentDepth);
// This now tells Unity to "interrupt" this routine here
// render the current frame and continue from here in the next frame
yield return null;
}
}
private IEnumerator SearchPosition(int _currentDepth)
{
// run the NegamaxTask asynchronously in the background
var task = Task.Run(() => Negamax(_currentDepth));
// wait for the task to finish
while(!task.IsCompleted)
{
// do nothing but skip frames to allow the rest of the application to run smoothly
yield return null;
}
// If you do nothing else inside the loop this could also be written as
//yield return new WaitWhile(() => !task.IsComoleted);
// or
//yield return new WaitUntil(() => task.IsCompleted);
// since the task is already finished it is save / non-blocking to access the result now
score = task.Result;
var printItem = Instantiate(printItemPrefab, printItemParent.transform);
Debug.Log(_currentDepth);
}
Now this allows your application to continue with a normal frame-rate while in the background you do the heavy calculations and once in a while get a result back when an iteration is finished.
Try using a thread:
private void AIMakeMove()
{
new System.Threading.Thread(() =>
{
for (int currentDepth = 1; currentDepth < maxDepth + 1; currentDepth++)
{
SearchPosition(currentDepth);
}
}).Start();
}
private void SearchPosition(int _currentDepth)
{
// Search the position to the given depth
score = Negamax(_currentDepth);
// Print things PROBLEM HERE
GameObject printItem = Instantiate(printItemPrefab, printItemParent.transform);
Debug.Log(_currentDepth);
}
If I use one-layer yield return, StopCoroutine() can successfully stop my coroutine. See the code example below...
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestStopcoroutine : MonoBehaviour {
IEnumerator co = null;
// Use this for initialization
void Start () {
co = FunA();
StartCoroutine(co);
}
private IEnumerator FunA() {
Debug.Log("Enter FunA...");
yield return RepeatPrint();
Debug.Log("FunA end...");
}
private IEnumerator RepeatPrint() {
for (int i = 0; i < 5; i++) {
Debug.Log(i);
yield return new WaitForSeconds(1);
}
}
/// <summary>
/// Set this function to a button on UI Canvas
/// </summary>
public void OnCancelButtonClick() {
if (co != null) {
StopCoroutine(co);
Debug.Log("Stop Coroutine...");
co = null;
}
}
}
this output is...
// Enter FunA...
// 0
// 1
// 2
// 3
// Stop Coroutine...
However, if I add one layer (i.e.FunB()), FunA() will be stopped but the inside coroutine(FunB()) will not be stopped. See the example code below:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestStopcoroutine : MonoBehaviour {
IEnumerator co = null;
void Start () {
co = FunA();
StartCoroutine(co);
}
private IEnumerator FunA() {
Debug.Log("Enter FunA...");
yield return FunB();
Debug.Log("FunA end...");
}
private IEnumerator FunB () {
Debug.Log("Enter FunB...");
yield return RepeatPrint();
Debug.Log("FunB end...");
}
private IEnumerator RepeatPrint() {
for (int i = 0; i < 5; i++) {
Debug.Log(i);
yield return new WaitForSeconds(1);
}
}
/// <summary>
/// Set this function to a button on UI Canvas
/// </summary>
public void OnCancelButtonClick() {
if (co != null) {
StopCoroutine(co);
Debug.Log("Stop Coroutine...");
co = null;
}
}
}
this output is...
// Enter FunA...
// Enter FunB...
// 0
// 1
// 2
// Stop Coroutine...
// 3
// 4
// FunB end...
Therefore, I am wondering why StopCoroutine() cannot successfully stop multi-layer yield return coroutine??
For your second code, the log should indeed end at Stop Coroutine.....
There are three possibilities to why it is showing the current output: (Very likely in order why this is happening)
1. You are setting Time.timeScale to 0. Search in your whole project and make sure that you're not doing this: Time.timeScale = 0;. This can pause or upause a coroutine function that's waiting with WaitForSeconds. If you did, temporary remove or comment it out and see if it's the issue.
2. Your project is corrupted and there is now a bug in it. Sometimes, a bug can randomly happen in a Unity project and the only way to fix that is to create a new project and manually move the resources from the old project to the new one.
Create a new project and test your modified code below.
3. A bug with Unity itself. Since you're using Unity 5.6.4p3, there is chance this is bug with Unity. If doing what's in #1 and #2 did not solve your issue then simply update Unity to the latest version (Unity 2018.xxx). This is more likely to fix your issue and don't forget to test with a new project instead of importing the old one.
Use the modified code from your question below to test #2 and #3. It uses the Invoke function to stop the coroutine after one second.
IEnumerator co = null;
void Start()
{
co = FunA();
Invoke("OnCancelButtonClick", 1f);
StartCoroutine(co);
}
private IEnumerator FunA()
{
Debug.Log("Enter FunA...");
yield return FunB();
Debug.Log("FunA end...");
}
private IEnumerator FunB()
{
Debug.Log("Enter FunB...");
yield return RepeatPrint();
Debug.Log("FunB end...");
}
private IEnumerator RepeatPrint()
{
for (int i = 0; i < 5; i++)
{
Debug.Log(i);
yield return new WaitForSeconds(1);
}
}
/// <summary>
/// Set this function to a button on UI Canvas
/// </summary>
public void OnCancelButtonClick()
{
if (co != null)
{
StopCoroutine(co);
Debug.Log("Stop Coroutine...");
co = null;
}
}
The expected output:
Enter FunA...
Enter FunB...
0
Stop Coroutine...
To address KYL3R's statement in his answer that stopping the main Coroutine won't affect any other coroutine, that has already been started. This is partially true but totally depends on how and where the coroutine was started.
There are two ways to start a coroutine function:
1. Start a coroutine with the StartCoroutine function from any function like a function with a void return type.
void Start()
{
StartCoroutine(RepeatPrint());
}
or
IEnumerator Start()
{
yield return StartCoroutine(RepeatPrint());
}
2. Start a coroutine function without the StartCoroutine function by yielding the coroutine function you want to start. This must be done inside a coroutine function or a function with IEnumerator return type. It can't be done in a normal function like a void function.
IEnumerator Start()
{
yield return RepeatPrint();
}
When you start a coroutine with StartCoroutine then start children coroutines after with StartCoroutine but then killed the parent coroutine, the children coroutine functions will and should continue to run untouched.
Now, when you start a coroutine with StartCoroutine then start children coroutines after with yield return YourCoroutine() without using the StartCoroutine function but then killed the parent coroutine, the children coroutine functions will and should terminate or stop immediately so as the parent one.
It's not surprising that most Unity users don't know because it is not documented but it's something that's very important to know when using coroutine in Unity.
Coroutines run independent from each other. So stopping the main Coroutine won't affect any other, that has already been started.
It seems you have 2 options:
A: Stop the nested Coroutine from inside the first level
Store a Reference to RepeatPrint and use that to stop it using StopCoroutine.
edit: actually "from inside the first level" is not necessary, just use the correct Reference.
B: Stop ALL Coroutines in your MonoBehaviour
May work if you have no other Coroutines running
I've been messing around trying to create an analog style scoring system for my game that I am building with Unity3D and I've noticed a huge lag when I call AnalogScoreProcessing() from the updateScore() function:
/******************************** Update Score ********************************/
public void updateScore (int newScore)
{
// Send the old score/new score to AnalogScoreProcessing()
if (oldScore != newScore)
{
AnalogScoreProcessing(oldScore, newScore);
}
// Display GUI score
oldScore = newScore;
guiText.text = "" + oldScore;
}
/*************************** Analog Scoring System ****************************/
void AnalogScoreProcessing(int oldScore, int newScore){
for(int i = oldScore; i <= newScore; i++){
print (i);
}
}
Do I have to create a new thread or a co-routine to complete the looping task while the remaining parts of updateScore() are carried out? I've never done any threading but I have used co-routines. I'm just not sure what I should be using here.
The easiest solution is to use coroutines here.
You basically want the players to see the effect of counting up right?
So say you want to display a count up to the user being +1 to the user ever few frames or so so the count up will be perceivable.
Try...
I would do it like this
int current =0;
int target = 0;
float percentateOfTarget = 0;
float step = 0.1f;
IEnumerable AnalogScoreProcessing()
{
while(current <= target)
{
current = (int)Math.Ceiling(Mathf.Lerp(current, target, percentateOfTarget));
return yield new WaitForSeconds(step);
percentageOfTarget += step;
// Display GUI score
guiText.text = "" + current;
}
}
public void updateScore (int newScore)
{
// Send the old score/new score to AnalogScoreProcessing()
if (oldScore != newScore)
{
StopCoroutine("AnalogScoreProcessing");
current = oldScore;
target = newScore;
StartCoroutine("AnalogScoreProcessing");
}
}
Please be aware this code is pulled directly out of my head so youll probably have to tweek some things here and there but should give you something close to what you desire.
You could/should even scale the GUIText up while the coroutine running for an even more dramatic effect. Let me know how it goes.
As I side note coroutines are not separate threads in unity they are ran on unity's main thread.
Well i dont understandt the Logic behind the function, but cant you just print the score once it changes?
if (oldScore != newScore)
{
//AnalogScoreProcessing(oldScore, newScore);
Debug.log(newScore);
}
Also you have to set the GUI calls inside the
OnGUI()
function inside your file.
If there is a large difference between oldScore and newScore then the print function in AnalogScoreProcessing will be run many times.
You might want to look at using a tween to change the displayed value, as you cannot display more than one value per frame anyway. Or if you're printing to the console then... why are you doing that?
edit: have moved quick and dirty solution a more appropriate question/answer