6
\$\begingroup\$

I want to create a countdown timer in a Unity game I am creating. Up until now my game was running for a specific time but I was using InvokeRepeating to end the game after the set time.

But now I want to display the time left in a GUI text. One approach could be to use InvokeRepeating again but this time call it every second to decrement the timer. With this approach though it would mean do extra calculations for when a minute passes in order to display time appropriately and not just display the whole time in seconds.

Is there a different approach? Does Unity have any timers built in? Also which is the more efficient method?

\$\endgroup\$
5
\$\begingroup\$

Alternatively, you could use System.Timers.Timer which is probably the most performant of any solution. The following example shows a countdown for 100 seconds.

System.Timers.Timer LeTimer;
int BoomDown = 100;
void Start ()
{
    //Initialize timer with 1 second intervals
    LeTimer = new System.Timers.Timer (1000);
    LeTimer.Elapsed +=
        //This function decreases BoomDown every second
        (object sender, System.Timers.ElapsedEventArgs e) => BoomDown--;
}

void Update ()
{
    //When BoomDown reaches 0, BOOM!
    if (BoomDown <= 0)
        Debug.Log ("Boom!");
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Do you know which of the three approaches is the most efficient? Yours, mine or return trues? \$\endgroup\$ – John Demetriou May 17 '15 at 6:57
  • 1
    \$\begingroup\$ In order from most efficient to least: System.Timers.Timer, Coroutines, then finally InvokeRepeating. InvokeRepeating has the nasty habit of using reflection every time to call the function which is pretty slow. @Return True is right though. This is a premature optimization. I'd use Timers just because I find them easier to use but for something like an animation, coroutines are the way to go. \$\endgroup\$ – JPtheK9 May 17 '15 at 16:30
  • \$\begingroup\$ I guess I will follow the Timer approach then. They different implementations may not differ so much in efficiency at my game but it's important to stay efficient and use the most efficient practices \$\endgroup\$ – John Demetriou May 18 '15 at 14:45
  • 1
    \$\begingroup\$ Oh, in terms of accuracy, Timer is the way to go. Coroutines use update frames and delta times for WaitForSeconds() while Timers use system ticks. If the FPS dropped to 2 FPS, the coroutine would also drop to a rate of 2 ticks per second (maximum) and would have a hard time being more accurate than .5 seconds. \$\endgroup\$ – JPtheK9 May 18 '15 at 17:17
3
\$\begingroup\$

Coroutines are you friend I think on this one...

http://docs.unity3d.com/Manual/Coroutines.html

Here is an example:

void Start () {
    StartCoroutine ("Countdown", 10);
}

private IEnumerator Countdown(int time){
    while(time>0){
        Debug.Log(time--);
        yield return new WaitForSeconds(1);
    }
    Debug.Log("Countdown Complete!");
}
\$\endgroup\$
  • \$\begingroup\$ Do you know which of the three approaches is the most efficient? Yours, mine or JPtheK9s? \$\endgroup\$ – John Demetriou May 17 '15 at 6:56
  • 2
    \$\begingroup\$ The difference would be so negligible it would not be worth the time to even test it. I think this would best fall under premature optimization. I recommend using the one that works best for you, and that you can better understand and that lends itself to what you want it to do with it (for instance, if you wanted to load and wait on data from a website, Coroutine is most definitely the better way to go, whereas in other instances using Timer is a life saver). I honestly think you should learn to use both, and include both methodologies in the future when developing with Unity. Hope that helps \$\endgroup\$ – return true May 17 '15 at 7:17
1
\$\begingroup\$

The code below might look complicated but once you understand it, you will be able to see its power of reusablity. (it's kind of countup but further down you will understand how to make it countdown if you understand how the code works)

public IEnumerator Count(
    float targetTime,
    System.Action<float> receiveTimeElapsed = null,
    System.Func<bool> actionCondition = null,
    System.Action conditionalAction = null,
    System.Action onCountStart = null,
    System.Action onCountEnd = null)
{
    onCountStart?.Invoke();

    float timeElapsed = 0f;
    receiveTimeElapsed?.Invoke(timeElapsed);

    while (timeElapsed < targetTime)
    {
        if (actionCondition != null && actionCondition.Invoke())
            conditionalAction?.Invoke();

        yield return null;

        timeElapsed += Time.deltaTime;

        receiveTimeElapsed?.Invoke(timeElapsed);
    }

    if (actionCondition != null && actionCondition.Invoke())
        conditionalAction?.Invoke();

    onCountEnd?.Invoke();
}

private float _timeElapsed;
private int _count;

private void Awake()
{
    this.StartCoroutine(
        this.Count(
            5f,
            (timeElapsed) =>
            {
                this._timeElapsed = timeElapsed;
                Debug.Log("Time Elapsed: " + this._timeElapsed);
            },
            () => { return this._timeElapsed >= (this._count + 1); },
            () =>
            {
                this._count++;

                Debug.Log("Countdown: " + this._count);
            },
            () => { Debug.Log("Countdown: " + this._count); },
            () => { Debug.Log("On countdown end"); }
        )
    );
}

If you think that it's too much or you may not need all this checking for null. To simplify the method you could create a similar methods that will do the same but without some actions.

But I would optimize only when it hits the performance, though, I like early optimization. This one is not the case as it's not intended to be used 1000000 times per frame. As well as removing null checks and some actions would be a micro-optimization.

Also you can always add more actions, for while loop condition check and for the way time elapsing is calculated.

For example, add getElapsingValue:

public IEnumerator Count(
    float targetTime,
    System.Func<float> getElapsingValue,
    System.Action<float> receiveTimeElapsed = null,
    System.Func<bool> actionCondition = null,
    System.Action conditionalAction = null,
    System.Action onCountStart = null,
    System.Action onCountEnd = null)
{
    onCountStart?.Invoke();

    float timeElapsed = 0f;
    receiveTimeElapsed?.Invoke(timeElapsed);

    while (timeElapsed < targetTime) // You want to add func here to make it more scalable.
    {
        if (actionCondition != null && actionCondition.Invoke())
            conditionalAction?.Invoke();

        yield return null;

        timeElapsed += getElapsingValue.Invoke();

        receiveTimeElapsed?.Invoke(timeElapsed);
    }

    if (actionCondition != null && actionCondition.Invoke())
        conditionalAction?.Invoke();

    onCountEnd?.Invoke();
}

private float _timeElapsed;
private int _count;

private void Awake()
{
    this.StartCoroutine(
        this.Count(
            5f,
            () => { return Time.deltaTime; },
            (timeElapsed) =>
            {
                this._timeElapsed = timeElapsed;
                Debug.Log("Time Elapsed: " + this._timeElapsed);
            },
            () => { return this._timeElapsed >= (this._count + 1); },
            () =>
            {
                this._count++;

                Debug.Log("Countdown: " + this._count);
            },
            () => { Debug.Log("Countdown: " + this._count); },
            () => { Debug.Log("On countdown end"); }
        )
    );
}

To actually take countdown value you need to edit the method. Or you could simply do this this._countdown - this._count which will give you the desired value anyway.

\$\endgroup\$
  • \$\begingroup\$ I steel feel the other two are a lot simpler and mote re-useable \$\endgroup\$ – John Demetriou Jan 14 '19 at 17:20
  • \$\begingroup\$ @JohnDemetriou if they work for you, then go for it. It depends on a case. If you have multiple UI components that behave differently you would have to reimplement the same thing multiple times. While using this I could write calls that fit my needs. Also, get information about time that has elapsed and so on. Though, for this kind of behaviour I use separate classes or structures to expose values and simplify code. But they have a lot of code. \$\endgroup\$ – Candid Moon _Max_ Jan 14 '19 at 17:35
1
\$\begingroup\$

This seemed a simple solution: you can use st.Reset() etc as you need in your own code.

[SerializeField] private TextMeshProUGUI countDownDisplay;
Stopwatch st = new Stopwatch();

void SomeEvent() {
    st.Start();
}

void Update()
{
    if (st.IsRunning)
    {
        int secondsUntilDestructSequence = 5;
        int elaspedSeconds = Mathf.RoundToInt((st.ElapsedMilliseconds / 1000) % 60f);
        int secondsRemaining = secondsUntilDestructSequence - elaspedSeconds;
        countDownDisplay.text = "Countdown remaining: " + secondsRemaining;
        if (secondsRemaining==0) Debug.Log("Game Over");
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.