I've got a class. It has a method that does a lot of work. I would like to not have the program hang while it does the work. I understand yield will do this for me.
void Start() {
DoWork(10,10);
}
void DoWork (int x, int y) {
for (int i=0; i < x; i++) {
for (int j=0; j < y; j++) {
// Stuff
}
}
}
If I add a yield like this
void Start() {
DoWork(10, 10);
}
IEnumerable DoWork (int x, int y) {
for (int i=0; i < x; i++) {
for (int j=0; j < y; j++) {
// Stuff
}
Debug.Log (1);
yield return null;
}
}
None of the work gets done and on top of that I see no log statements at all.
How do I yield my code so the program doesn't freeze?
This is Unity3D engine so your coroutine needs to return IEnumerator to be valid:
void Start() {
StartCoroutine(DoWork(10, 10));
}
IEnumerator DoWork (int x, int y) {
for (int i=0; i < x; i++) {
for (int j=0; j < y; j++) {
// Stuff
}
Debug.Log (1);
yield return null;
}
}
This is in no way multithreading. It is run just like an update once per frame between the Update and the LateUpdate except if you use
yield return new WaitForEndOfFrame();
then it is postponed until after the rendering process. What it does is create a new object of type Coroutine and place it on the calling MonoBehaviour stack of coroutines.
This works as a method that performs some repetitive action but always return to the main program when hitting a yield. It will then catch back from there on the next frame.
You need to use the StartCoroutine method:
void Start() {
StartCoroutine(DoWork(10, 10));
}
IEnumerator DoWork (int x, int y) {
// (A)
yield return null;
// (B)
for (int i=0; i < x; i++) {
for (int j=0; j < y; j++) {
// Stuff
}
Debug.Log (1);
yield return null;
// (C)
}
}
Yur code is executed piece by piece where delimiter of steps is the yield operator, i.e. when Framework calls MoveNext() the first time - the code (A) will be executed, when it calls MoveNext() second time - the code (B) will be executed, then code (C), and so on and so forth.
When you add a yield statement, the compiler actually generates a private class that acts as a state machine that implements IEnumerable. As such none of the code wrapped up from the original method will be called unless you enumerate the result of the method - in your example, you're throwing away the return value, so nothing would happen.
Yield keyword is used for lazy loading/computation support in C#.
Try doing:
var result = DoWork().ToList();
This forces an evaluation of the DoWork() method and you will see the logging taking place.
C# yield in Unity works just like C# yield always does. Unity does not influence this in any way.
yield is a keyword that is used to allow enumeration over a set of return values.
IEnumerator<int> MyEnumerationMethod()
{
yield return 5;
yield return 1;
yield return 9;
yield return 4;
}
void UserMethod1()
{
foreach (int retVal in MyEnumerationMethod())
Console.Write(retVal + ", ");
// this does print out 5, 1, 9, 4,
}
void UserMethod2()
{
IEnumerator<int> myEnumerator = MyEnumerationMethod();
while (myEnumerator.MoveNext())
Console.Write(myEnumerator.Current + ", ");
// this does print out 5, 1, 9, 4,
}
UserMethod1() and UserMethod2() are pretty much the same. UserMethod1() is just the C# syntactic sugar version of UserMethod2().
Unity uses this language feature to implement Coroutines:
When you call StartCoroutine() and pass it an IEnumerator, Unity stores this enumerator and calls MoveNext() for the first time. This will cause MyEnumerationMethod() to be called and executed up until the first yield return. At this point, MoveNext() returns and the first result (5) can be retrieved by looking at the Current property of the enumerator.
Now, Unity regularly checks the Current property and - depending on its value - decides whether the time has come to call MoveNext() again. The value of Current might be an instance of WaitForEndOfFrame, an instance of WWW or whatever, and depending on that the time, MoveNext() is called is decided.
Once MoveNext() is called again, execution of MyEnumerationMethod() will be continued at the point where it was interrupted last time, and executes until the next yield return is executed. And so on.
That's all there is to yield, and to Coroutines in Unity.
Related
I am working on a game, that has a keypad puzzle. A specific key combination lights up one by one, which the player must repeat to solve that puzzle. I am going to let the player know what the combination is by activating and deactivating some GameObjects systematically, one by one. As it suggests, there is some time delay between the deactivation of one GameObject and the activation of another. The problem is, in my code, all the GameObjects activate simultaneously instead of one by one, after a delay.
Here is the code:
public string Generate(int length, float delay)
{
// Variables for logic
string combination = "";
int prev = -1; int current = 0;
int rnd = 0;
for (int i = 0; i < length; i++)
{
rnd = Random.Range(0, BUTTONS);
while (rnd == prev)
{
rnd = Random.Range(0, BUTTONS);
}
prev = current;
current = rnd;
combination += current.ToString();
// Activation and Deactivation
StartCoroutine(GenerateDelay(delay, current));
}
return combination;
}
IEnumerator GenerateDelay(float delay, int index)
{
ButtonClicks[index].SetActive(true);
yield return new WaitForSeconds(delay);
ButtonClicks[index].SetActive(false);
}
The loop counter specifies the length of the combination. I believe there is something wrong with the Coroutine I made? Since all the objects activate simultaneously.
Here is the result in the game as well:
We can see, only one button should turn green at a time, but all of them do in this case. Any solutions?
You start all your Coroutines parallel so things happen at the same time.
StartCoroutine does not delay the method which calls it (unless it is yielded as well.
You would need to run the entire loop within a Coroutine in order to delay it as well.
You could simply split up the creation of the combination and while you already return it you start the visualization in the background in parallel
public string Generate(int length, float delay, Action<string> onCombination)
{
// Variables for logic
var combination = List<int>();
var prev = -1;
for (var i = 0; i < length; i++)
{
int rnd;
do
{
rnd = Random.Range(0, BUTTONS);
}
while (rnd == prev);
prev = rnd;
combination.Add(rnd);
}
StartCorouine (ShowCombination(combination, delay));
return string.Join("", combination);
}
private IEnumerator ShowCombination (IEnumerable<int> combination, float delay)
{
foreach(var i in combination)
{
ButtonClicks[i].SetActive(true);
yield return new WaitForSeconds(delay);
ButtonClicks[i].SetActive(false);
}
}
something alot easier then a coroutine is invoking a function, basically create a function to activate/deactivate the wanted object and whenever you want to call it do:
Invoke("FUNCTIONNAME", TIME);
and it will run the function specified after the TIME.
Hey I'm in a situation where I have a for loop that does some stuff and I want to make a line of code either call a function passing in an array indexed by the for loops index, or run a single (not array) variable for every call of that function, I know I could do that by putting an if statement inside the for loop but i'd be repeating the same if statement over and over getting the same result. So is there a way good way I can run the if statement before the for loop and the result of that if statement run the same for loop but that one call passes in the array or the variable?
Code Example
for (int i = 0; i < CurrentVerticalList.Count; i++)
{
GuiGeneral CGroup = CurrentVerticalList[i];
CGroup.ResizeUsingStandard(ForcedResize[i]); //I want the condition before the for
//loop to have ForcedResize[i] here if
//true and another variable here of the
//same type but not an array if false.
for (int j = 0; j < 2; j++)
{
GlobalListIndex[j]++;
}
CGroup.MoveElementTo(CCoord, false);
CCoord.y += CGroup.ElementRect.WidthHeight.y;
}
Here you go, moving your condition check out of your for loop:
Func<int, double> GetResizeFromForcedResize = (index => ForcedResize[index]);
Func<int, double> GetResizeFromVariable = (index => fixVariable);
var GetResizeValue = condition? GetResizeFromForcedResize : GetResizeFromVariable;
for (int i = 0; i < CurrentVerticalList.Count; i++)
{
GuiGeneral CGroup = CurrentVerticalList[i];
CGroup.ResizeUsingStandard(GetResizeValue(i));
for (int j = 0; j < 2; j++)
{
GlobalListIndex[j]++;
}
CGroup.MoveElementTo(CCoord, false);
CCoord.y += CGroup.ElementRect.WidthHeight.y;
}
Edit: Wanted to let you know that the other answers here are still doing a check at every iteration, but not this one.
Eh, hard to understand the question to me but i recon what you want is something along these lines, could've helped with more types supplied, but you could make the intend achievable using a local function:
ForcedResizeArrayType other = new object(); //TODO: Define return type
bool condition = ResolveCondition(); //TODO: Define condition to be true or false
ForcedResizeArrayType GetOneOr(int i, bool condition,
ForcedResizeArrayType[] forcedResizeArray)
{
return condition ? forcedResizeArray[i] : other;
}
for (int i = 0; i < CurrentVerticalList.Count; i++)
{
CGroup.ResizeUsingStandard(GetOneOr(i, condition, ForcedResize));
}
It varies from week to week if i love or hate those local functions, but they have uses
I tried to use delegate chain like below, trying to make animation in unity:
public class Class1
{
class Dele {
delegate void MyDelegate();
private MyDelegate dele;
private int count = 0;
public void Animate() {
dele = new MyDelegate(DoIe);
}
IEnumerator Ie() {
Debug.Log(count);
count += 1;
yield return new WaitForSeconds(5f);
}
private void DoIe() {
StartCouroutine(Ie());
for (int i=0; i<10; i++) {
dele += DoIe;
}
dele();
}
}
//call new Dele().Animate() here
}
I thought the Log will go like
1
(5 secs)
2
(5 secs)
...
10
but instead,
1
2
..
10
was logged at the same time.
If I want to callback another Ie after 5 second,
what should I do??
With coroutines it's the code inside the routine (the IEnumerator method) that runs later. The code after StartCoroutine() in your void-returning method above will run synchronously (straight away), like you saw.
You don't need a delegate here at all. All you need is this:
IEnumerator Ie() {
for (int i=0; i<10; i++) {
Debug.Log(count);
count += 1;
yield return new WaitForSeconds(5f);
}
}
private void DoIe() {
StartCoroutine(Ie());
}
First of all, your class needs to inherit from MonoBehavious for StartCoroutine to work.
Then in regards of your question: you need to start the coroutine with a delay, just adding them to a multicast delegate is simply not doing what you think you are
This is using Unity3D. I have three coroutines: GetJSONFromSelectedSubreddit(), LoadMoreMemes(), and a function in a separate script that needs to be able to access the array of memes through the GetNewMemes() function (must return type Meme[]). LoadNewMemes() produces. The thing is, LoadMoreMemes() requires the json to work, so they have to run in the mentioned order. If you need the functions, here they are:
public void GetNewMemes(string subReddit, int count)
{
SetSelectedSubreddit(subReddit);
memesAtATime = count;
subJSON = null;
StartCoroutine(GetJSONFromSelectedSubreddit());
StartCoroutine(LoadMoreMemes());
}
IEnumerator GetJSONFromSelectedSubreddit()
{
gettingJSON = true;
WWW requester = new WWW("https://www.reddit.com/r/" + selectedSub + "/new.json?sort=new&count=25&after=" + postIndex);
yield return requester;
subJSON = requester.text;
json = new JSONObject(subJSON);
gettingJSON = false;
}
IEnumerator LoadMoreMemes()
{
while (gettingJSON)
yield return new WaitForSeconds(0.1f);
for (int i = 0; i < memesAtATime; i++)
{
yield return StartCoroutine(GetUserPostKarma(json["data"]["children"][i]["data"]["author"].str));
string sourceURL = json["data"]["children"][i]["data"]["preview"]["images"][0]["source"]["url"].str;
sourceURL = sourceURL.Replace("&", "&");
yield return StartCoroutine(GrabImage(sourceURL));
Meme currentMeme = new Meme(
json["data"]["children"][i]["data"]["preview"]["images"][0]["source"]["url"].str,
authorPostKarma,
(int) json["data"]["children"][i]["data"]["score"].i,
json["data"]["children"][i]["data"]["permalink"].str,
json["data"]["children"][i]["data"]["title"].str,
currentBitmap
);
Debug.Log(currentMeme.cost);
memes[i] = currentMeme;
}
}
Here's the other script:
void Start ()
{
RedditCommunicator redditCommunicator = GetComponent<RedditCommunicator>();
redditCommunicator.GetNewMemes("me_irl", 1);
Meme[] memes = redditCommunicator.GetCurrentMemes();
Debug.Log(memes[0].currentScore);
redditCommunicator.SpawnOneMeme(memes[0]);
}
Each function works fine on its own, but they need to wait for each other to finish, as well as run in the correct order to work. I'd like the functions to stay separate so I can call them individually in the future. memes is a private variable, and the one I'd like to pass to the other script calling these functions. If you don't think I've tried my options Googling and solving this on my own, just believe me, I've done my best. Thanks for your help in advance. If you need more information, just ask me for it. The current state of this code is it returns memes to early, before the coroutines can finish, resulting in empty memes.
You can yield a Coroutine in an IEnumerator which will halt the progression of that Coroutine until that Coroutine is done. Like this:
void Start()
{
StartCoroutine(DoThings((text) => {
Debug.Log("Dothings told me: " + text);
}));
}
IEnumerator DoThings(Action<string>() callback)
{
yield return StartCoroutine(DoThisFirst());
callback("Returning a value mid-method!");
yield return StartCoroutine(ThenThis());
Debug.Log(3);
}
IEnumerator DoThisFirst()
{
yield return new WaitForSeconds(1);
Debug.Log(1);
}
IEnumerator ThenThis()
{
yield return new WaitForSeconds(1);
Debug.Log(2);
}
Problem is that GetJSONFromSelectedSubreddit and LoadNewMemes methods are called as two "parallel" coroutines in GetNewMemes method.
If you do not need to run a coroutine "asynchronously", you can just enumerate through it:
public void GetNewMemes(string subReddit, int count)
{
SetSelectedSubreddit(subReddit);
memesAtATime = count;
subJSON = null;
var enumerator = GetJSONFromSelectedSubreddit();
while (enumerator.MoveNext());
enumerator = LoadNewMemes();
while (enumerator.MoveNext());
}
If I step through the following code the call to ReturnOne() is skipped.
static IEnumerable<int> OneThroughFive()
{
ReturnOne();
yield return 2;
yield return 3;
yield return 4;
yield return 5;
}
static IEnumerator<int> ReturnOne()
{
yield return 1;
}
I can only assume the compiler is stripping it out because what I'm doing is not valid. I'd like the ability to isolate my enumeration into various methods. Is this possible?
You're not actually using the result of ReturnOne. You're calling the method, and ignoring the return value... which means you'd never actually see any of your code being run. You can do it like this:
static IEnumerable<int> OneThroughFive()
{
foreach (int x in ReturnOne())
{
yield x;
}
yield return 2;
yield return 3;
yield return 4;
yield return 5;
}
C# doesn't (currently at least :) have a sort of "yield all" construct.
The fact that you're not getting to step into it has nothing to do with the fact that you've got a call within an iterator block - it's just that until you start using the result of an iterator block, none of the code runs. That's why you need to separate out argument validation from yielding. For example, consider this code:
public IEnumerator<string> ReturnSubstrings(string x)
{
if (x == null)
{
throw ArgumentNullException();
}
for (int i = 0; i < x.Length; i++)
{
yield return x.Substring(i);
}
}
...
ReturnSubstring(null); // No exception thrown
You need to write it like this:
public IEnumerator<string> ReturnSubstrings(string x)
{
if (x == null)
{
throw ArgumentNullException();
}
return ReturnSubstringsImpl(x);
}
private IEnumerator<string> ReturnSubstringsImpl(string x)
{
for (int i = 0; i < x.Length; i++)
{
yield return x.Substring(i);
}
}
For more details, read chapter 6 of C# in Depth - which happens to be a free chapter in the first edition :) Grab it here.