Gusto
Gusto

Reputation: 1513

Calling a Coroutine from UI button gets unpredictable behavior Unity

SOLVED: Updated Unity to version 2017.2

UI button On Click manager

When adding more than 1 task to the execution list of UI button On Click() manager, the execution of those tasks gets unpredictable behavior. My countdown script counts twice as fast when there is more than one task added to that list (se the attached image). When I remove one task, the code runs as it should (prints the countdown to the screen every second 5..4..3..2..1 etc). Unity Bug?

public IEnumerator Countdown()
{
    while (timeLeft > 0)
    {
        yield return new WaitForSeconds(1.0f);
        countText.text = timeLeft.ToString("f0");
        timeLeft--;          
    }
}

public void StartCountDown()
{
    StartCoroutine("Countdown");
}

Upvotes: 0

Views: 1403

Answers (2)

Benjamin BERTRAND
Benjamin BERTRAND

Reputation: 1

You can add a "yeld return null;" at the end of the function to prevent strange behaviors

Upvotes: 0

Programmer
Programmer

Reputation: 125435

Each time the coroutine function is called, timeLeft variable is being modified. If you call that coroutine function again while the first one is running then multiple coroutine functions will be modifying the timeLeft variable at the-same time resulting to your current issue.

One solution would be to use a boolean variable to detect when coroutine is already running and start is again but you mentioned that you want to create and multiple timers at the-same time. You have two options:

1.Move the timeLeft variable inside the Countdown function so that each function call has a timeLeft variable for it. Also do the-same for the countText variable so that a different Text component is used to modify the Text in each function call.

public IEnumerator Countdown()
{
    float timeLeft = 5f;
    Text countText = GameObject.Find("TextForThisCounter").GetComponent<Text>();

    while (timeLeft > 0)
    {
        yield return new WaitForSeconds(1.0f);
        countText.text = timeLeft.ToString("f0");
        timeLeft--;
    }
}

2.Move the whole coroutine function to another class then use a callback Action to notify you each second there is a tick in the timer and when the timer is done. I recommend this method since it is more portable and reusable. You can also implement an ID to determine which timer this is when it finish running.

Timer moved to another script:

public struct CountDownTimer
{
    private static int sTimerID = 0;
    private MonoBehaviour monoBehaviour;

    public int timer { get { return localTimer; } }
    private int localTimer;

    public int timerID { get { return localID; } }
    private int localID;

    public CountDownTimer(MonoBehaviour monoBehaviour)
    {
        this.monoBehaviour = monoBehaviour;
        localTimer = 0;

        //Assign timer ID
        sTimerID++;
        localID = sTimerID;
    }

    public void Start(int interval, Action<int> tickCallBack, Action<int> finshedCallBack)
    {
        localTimer = interval;
        monoBehaviour.StartCoroutine(beginCountDown(tickCallBack, finshedCallBack));
    }

    private IEnumerator beginCountDown(Action<int> tickCallBack, Action<int> finshedCallBack)
    {
        while (localTimer > 0)
        {
            yield return new WaitForSeconds(1.0f);
            localTimer--;
            //Notify tickCallBack in each clock tick
            tickCallBack(localTimer);
        }

        //Notify finshedCallBack after timer is done
        finshedCallBack(localID);
    }
}

Usage:

Start timer 4 times each with different ID.

void Start()
{
    createAndStartNewTimer();

    createAndStartNewTimer();

    createAndStartNewTimer();

    createAndStartNewTimer();
}


public void createAndStartNewTimer()
{
    //Create new Timer
    CountDownTimer timer = new CountDownTimer(this);

    //What to do each second time tick in the timer
    Action<int> tickCallBack = (timeLeft) =>
    {
        Debug.Log(timeLeft.ToString("f0"));
    };


    //What to do each second time tick in the timer
    Action<int> finshedCallBack = (timeriD) =>
       {
           Debug.Log("Count Down Timer Done! ID: " + timeriD);
       };

    //Start Countdown Timer from 5
    timer.Start(5, tickCallBack, finshedCallBack);
}

Upvotes: 2

Related Questions