How to use coroutines to detect if the player is still for long enough

Viewed 146

I am not very good with coroutines and am trying to make something in my game. My goal is to make it so if the player stays still for a certain amount of seconds (in this case, 3), it will call a function. I am trying to use coroutines, but I have not been able to make them work. Here is my code so far:

...
public float waitTime;

void Update()
{
   if (Input.GetKey(KeyCode.E) && rb.velocity.x <= stillVelocity.x && rb.velocity.y <= stillVelocity.y)
   {
       StartCoroutine(coroutine());
   }
   else
   {
       StopCoroutine(coroutine());
   }
}
IEnumerator coroutine()
{
   yield return new WaitForSeconds(waitTime);
   method();
}
void method()
{
   Debug.Log("It all worked");
}

I did manage to get the "It all worked" in my log, but not the way I intended it. It would print it even if I had let go of E before the waitTime had passed. The velocity checking works fine.

Now, I should probably say what I wanted the script to do. I wanted the script to detect if the player is still and they are holding the E key down for waitTime seconds. How this will apply to an actual game is this: I am making a system where the player can raise a checkpoint. This checkpoint is the spawnpoint from then on until a new checkpoint is created. I can do the checkpoint/spawnpoint thing on my own, and only need help with coroutines.

I may be wrong with the idea of using coroutines. I chose to use them in this case because I heard that I should use them when my code works with time. I also think I need to practice using them more to get a better understanding of them.


Here are my questions:

  • Am I right in use coroutines here?
  • If so, how would I use the coroutines?
  • If not, when should I use them?

(Please use coroutines in your answer unless it is absolutely impossible to do it without them. I would prefer this, because I can do it without the coroutines, but, as said above, I need the practice.)

3 Answers

A Coroutine can work here, but as you are just wanting to call a function after a certain amount of time, an Involke might work better. I tend to use Involke whenever I just need a function called after X seconds, but use a Coroutine when I need to create a function that runs based on a series of preconditions in a specific way or after a specific amount of time.

The one issue I currently see with your code is you are continually calling StartCoroutine without checking if the Coroutine is active. Whenever I need to use a Coroutine with conditions that can occur multiple times in the lifetime of the actual Coroutine, I will store a reference and check if the reference is null.

...
public float waitTime;

private Coroutine YourCoroutineReference = null;

void Update()
{
   if (Input.GetKey(KeyCode.E) && rb.velocity.x <= stillVelocity.x && rb.velocity.y <= stillVelocity.y)
   {
       if(YourCoroutineReference == null)
           YourCoroutineReference = StartCoroutine(coroutine());
   }
   else
   {
       if(YourCoroutineReference != null)
            StopCoroutine(YourCoroutineReference);
   }
}
IEnumerator coroutine()
{
   yield return new WaitForSeconds(waitTime);
   method();
   YourCoroutineReference = null;
}
void method()
{
   Debug.Log("It all worked");
}

With this additional conditional check, the printout should work as intended. I would however use an invoke here instead as your use case is triggering a singular method after a fixed amount of time.

Here is the example using Invoke

...
public float waitTime;

void Update()
{
   if (!IsInvoking("method") && Input.GetKey(KeyCode.E) && rb.velocity.x <= stillVelocity.x && rb.velocity.y <= stillVelocity.y)
   {
       Invoke("method", waitTime);
   }
   else
   {
       if(IsInvoking("method"))
       {
           CancelInvoke("method");
       }
   }
}

private void method()
{
   Debug.Log("It all worked");
}

Both snippets are untested, but the general direction is there. If you run into an issue let me know and I can update the snippets. Again, if you find yourself just using Coroutine to call a function after a certain time, use an Invoke. When doing anything more complex where you are laying out multiple function calls or Lerps in succession, a Coroutine is a very useful tool for that.

I will also add that Coroutines are very flexible in that you can pause them for any reason using yields then continue them, whereas an Invoke is generally called once after X seconds, or called every Y seconds after X seconds if you use InvokeRepeating. Another useful part of using Coroutines is you can pass parameters to them where with Invoke, you can not. I suppose an oversimplified way to think about it is Invoke is a very simple general case for single use Coroutines or extremely fixed time repeating methods.

You can modify the coroutine to instead of waiting, constantly check if player is still and call after 3 seconds, else break.

IEnumerator coroutine()
{
    float limit = 3;
    float elapsed = 0;
    while(elapsed < limit)
    {
        if(!IsPlayerStill())
        {
            yield break;
        }
        elapsed += Time.deltaTime;
        yield return null;
    }
   
    method();
}

If you have an Update check anyway actually I wouldn't use a Coroutine here but rather a simple counter like

float timer;
bool alreadyFired;

void Update()
{
   if (Input.GetKey(KeyCode.E) && rb.velocity.x <= stillVelocity.x && rb.velocity.y <= stillVelocity.y)
   {
       timer -= Time.deltaTime;

       if(!alreadyFired && timer <= 0)
       {
           alreadyFired = true;
           method();
       }
   }
   else
   {
       alreadyFired = false;
       timer = waitTime;
   }
}

In this use case I think this would be way easier to maintain than a coroutine.

If you really want to go for a Coroutine I would do it like

private Coroutine _routine;

void Update()
{
   if (Input.GetKey(KeyCode.E) && rb.velocity.x <= stillVelocity.x && rb.velocity.y <= stillVelocity.y)
   {
       if(_routine == null) _routine = StartCoroutine(DoAfterSeconds(waitTime, method));
   }
   else
   {
       if(_routine != null) StopCoroutine(_routine);

        _routine = null;
   }
}

private IEnumerator DoAfterSeconds (float duration, Action callback)
{
    yield return new WaitForSeconds (duration);

    _routine = null;

    callback?.Invoke();
}
Related