Search code examples
c#loopsunity-game-enginecoroutineyield-return

How can I prevent my code from yielding every frame?


I am calling a method that call itself to crawl through terrain and create zones. However when the zones become to large the process ends in a stack overflow. By forcing the code to yield and take its time it finishes to completion successfuly and crawls the 3 zones in my map. However the method I am using is yielding EVERY single frame and I don't know how to make it yield just every 100 frames, causing it to be extremely slow to finish. Here is the pseudo code of what I am doing for readability:


 
public int timer = 0;
 
void Awake(){
 
StartCoroutine(crA);
}
 
public IEnumerator crA(){
//stuff
 
yield return StartCoroutine(crB());
 
//stuff that needs to happen only after crB finishes
}
 
public IEnumerator crB(){
 
timer = 0;
 
yield return StartCoroutine(crC());
 
}
 
public IEnumerator crC(){
//Crawiling code, crawls terrain to create a zone
 
if(x){ yield break;}
 
timer++;
 
//vv I WANTED IT TO YIELD ONLY IN HERE
if (timer ==100){
timer = 0;
yield return null;
}
//^^
 
yield return StartCoroutine(crC());
yield return StartCoroutine(crC());
yield return StartCoroutine(crC());
yield return StartCoroutine(crC());
yield return StartCoroutine(crC());
 
}
 

it seems yield return startcoroutine is causing a yield but I don't know what to use instead. Any help would be appreciated.


Solution

  • As said in general as long as you use yield it will at least yield for one frame like

    yield return null;
    

    does.

    What you could do instead is use the IEnumerator.MoveNext "manually" without yielding it. This is basically what the Unity coroutine is calling once per frame if you used StartCoroutine.

    And then only yield when you want to do so. Given your pseudo code something like e.g.

    private void Awake()
    {
        StartCoroutine(crA);
    }
    
    public IEnumerator crA()
    {
        //stuff
    
        yield return crB();
    
        //stuff that needs to happen only after crB finishes
    }
    
    public IEnumerator crB()
    { 
        timer = 0;
    
        // "Manually" run the routine instead of using Unity Coroutine interface
        var crc = crC();
        while(crc.MoveNext())
        {
            if(timer >= 100)
            {
                yield return null;
                timer = 0;
            }  
        } 
    }
    
    public IEnumerator crC()
    {
        //Crawiling code, crawls terrain to create a zone
    
        if(x) yield break;
    
        timer++;
    
        yield return crC();
        yield return crC();
        yield return crC();
        yield return crC();
        yield return crC();
    }
    

    And then as said you could do it time based instead of using a frame/call counter rather using e.g. a StopWatch

    private const float TARGET_FRAMERATE = 60;
    
    public IEnumerator crB()
    { 
        var targetMilliseconds = 1000f / TARGET_FRAMERATE;
        var sw = new StopWatch();
        sw.Start();
    
        // "Manually" run the routine instead of using Unity Coroutine interface
        var crc = crC();
        while(crc.MoveNext())
        {
            if(sw.ElapsedMilliseconds >= targetMilliseconds)
            {
                yield return null;
                sw.Restart();
            }  
        } 
    }
    

    So whenever the last execution exceeded the target frame-rate it will yield one frame. You'll have to play a bit with the value .. probably something like 30 or even 24 might already be enough depending on your usecase.

    It basically is a tradeoff between frame-rate and actual realtime duration.


    Note: Typed on smartphone but I hope the idea gets clear