Shykes G
Shykes G

Reputation: 44

Save and Load Coroutines state

I'm new in Unity. Is it possible to save the current state of coroutine and load it then continue the coroutine? I have a few coroutines in my project and i want to save it all.

Upvotes: 0

Views: 1152

Answers (2)

Jeremy Lakeman
Jeremy Lakeman

Reputation: 11120

When you define a Coroutine, eg;

using System.Collections;
public class C {
    private void before() { }
    private void after() { }
    public IEnumerator MyCoroutine()
    {
        before();
        yield return null;
        after();
    }
}

The compiler defines a new type to track the state of the method, including any local variables. You can see that in action by using a decompiler.

While it's more work, and more complicated, you could implement your own IEnumerable types instead.

public class MyCoroutine : IEnumerator
{
    private int state = 0;
    public object Current { get; private set; }

    private void before() { }
    private void after() { }

    public bool MoveNext()
    {
        switch (state)
        {
            case 0:
                before();
                state++;
                Current = null;
                return true;
            case 1:
                after();
                state++;
                break;
        }
        return false;
    }

    public void Reset() => throw new System.NotImplementedException();
}

Then it's up to you how you wish to save / load and resume your coroutines.

Upvotes: 1

Ruzihm
Ruzihm

Reputation: 20259

Unity doesn't have anything built in to support this. So, let's go over what saving a coroutine entails.

A started & incomplete coroutine has local variables, a yield instruction that they are currently at. And the yield that they are currently at may instantiate an anonymous YieldInstruction.

So, if you would want to save a coroutine, you would need to save these things. So, you would need to save a bunch of locals as well as answer the question of how to resume the couroutine at the correct yield instruction. This is a complicated task, and the outcome would probably be very messy without substantial effort and refactoring.

So, I recommend that you accept the fact you will need to do some refactoring. Rather than keeping the coroutines intact, the more straightforward path here is to "unroll" your coroutines into the various update methods:

  • Update, when using yield return null, WaitForSeconds, WaitUntil, WaitForSecondsRealtime
  • FixedUpdate, if using the uncommonly used WaitForFixedUpdate
  • LateUpdate, when using WaitForEndOfFrame

and move the locals of the coroutine into fields, as well as any additional necessary fields needed to track time passage in place of WaitForSeconds and such. And to make these fields so that they serialize properly.

Since there is no specific example given in the question, I will provide a before/after example that may help:

Using Coroutine (state not serializable):

void Update()
{
    DoStuff();
}

void StartMoveTowards(Transform target, Vector3 destination, float duration)
{
    StartCoroutine(DoMoveTowards(target, destination, duration));
}

IEnumerator DoMoveTowards(Transform target, Vector3 destination, float duration)
{
    float v = Vector3.Distance(target.position, destination) / duration;
    while( target.position != destination)
    {
        yield return null;
        target.position = Vector3.MoveTowards(target.position, destination, v * Time.deltaTime);
    }

    DoFirstThing();
    yield return new WaitForSeconds(3f);
    DoSecondThing();
}

Not using coroutine (serializable state):

enum MoveTowardsState 
{
    NotRunning,
    Starting,
    MovingTowards, 
    DoingFirstThing,
    Waiting
}

// serializable coroutine locals, timers, and instruction state
[SerializeField] MoveTowardsState currentMoveTowardsState;
[SerializeReference] 
[SerializeField] Transform moveTowardsTarget;
[SerializeField] Vector3 moveTowardsDestination;
[SerializeField] float moveTowardsDuration;
[SerializeField] float waitT;
[SerializeField] float v;

void Update()
{
    DoStuff();
    HandleDoMoveTowards();
}

void StartMoveTowards(Transform target, Vector3 destination, float duration)
{
    moveTowardsTarget = target;
    moveTowardsDestination = destination;
    moveTowardsDuration = duration;
    currentMoveTowardsState = MoveTowardsState.Starting;
}

void HandleDoMoveTowards()
{
    bool continuing = true;
    while(continuing)
    {
        continuing = false;
        switch (currentMoveTowardsState)
        {
            case MoveTowardsState.NotRunning: 
                break;

            case MoveTowardsState.Starting:
                v = Vector3.Distance(target.position, destination) / duration;
                if (target.position != destination)
                {
                    currentMoveTowardsState = MoveTowardsState.MovingTowards;
                }
                else 
                {
                    currentMoveTowardsState = MoveTowardsState.DoingFirstThing;
                    continuing = true;
                }
                break;

            case MoveTowardsState.MovingTowards:
                target.position = Vector3.MoveTowards(moveTowardsTarget.position, 
                        moveTowardsDestination, v * Time.deltaTime);

                if (target.position == destination)
                { 
                    currentMoveTowardsState = MoveTowardsState.DoingFirstThing;
                    continuing = true;
                }
                break;
            
            case MoveTowardsState.DoingFirstThing:
                DoFirstThing();
                waitT = 3f;
                currentMoveTowardsState = MoveTowardsState.Waiting;
                break;
  
            case MoveTowardsState.Waiting:
                waitT -= Time.deltaTime;
                if (waitT <= 0f)
                {
                    DoSecondThing();
                    currentMoveTowardsState = MoveTowardsState.NotRunning;
                }
                break;
        }
    }
}

And if multiple parallel instances are needed, You can create a new monobehavior for the coroutine, and create an instance for each running instance.

Note: I can't check syntax at the moment, so please mind any syntax errors. Hopefully the idea gets across. Please let me know in the comments if any parts dont make sense.

Upvotes: 0

Related Questions