如何使用协程来检测玩家是否仍然停留足够长的时间

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

我不太擅长协程,我想在我的游戏中做点什么。我的目标是让它在玩家静止不动一定秒数(在本例中为 3 秒)时调用一个函数。我正在尝试使用协程,但无法使它们工作。到目前为止,这是我的代码:

...
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");
}

我确实设法在我的日志中显示“一切正常”,但不是我想要的那样。即使我在 waitTime 过去之前放开了 E,它也会打印它。速度检查工作正常。

现在,我可能应该说出我想让脚本做什么。我想让脚本检测玩家是否静止并且他们按住 E 键 waitTime 秒。这将如何应用于实际游戏是这样的:我正在制作一个玩家可以提高检查点的系统。从那时起,这个检查点就是生成点,直到创建一个新的检查点。我可以自己做 checkpoint/spawnpoint 事情,只需要协程方面的帮助。

我可能对使用协程的想法有误。我选择在这种情况下使用它们,因为我听说当我的代码随时间运行时我应该使用它们。我也认为我需要更多地练习使用它们以更好地理解它们。


这是我的问题:

(请在你的答案中使用协程,除非绝对不可能没有协程。我更喜欢这个,因为我可以在没有协程的情况下做到这一点,但是,作为上面说了,我需要练习。)

Coroutine 可以在这里工作,但由于您只是想在一定时间后调用函数,Involke 可能会更好。每当我只需要在 X 秒后调用一个函数时,我倾向于使用 Involke,但是当我需要创建一个函数时,我会使用 Coroutine,该函数 运行s 基于一系列先决条件特定方式或特定时间后。

我目前在您的代码中看到的一个问题是您不断调用 StartCoroutine 而没有检查 Coroutine 是否处于活动状态。每当我需要在实际 Coroutine 的生命周期中多次出现的条件下使用 Coroutine 时,我将存储一个引用并检查该引用是否为空。

...
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");
}

通过这个额外的条件检查,打印输出应该按预期工作。但是,我会在这里使用调用,因为您的用例在固定时间后触发单一方法。

这里是使用 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");
}

两个片段都未经测试,但大体方向已经确定。如果您 运行 遇到问题请告诉我,我可以更新代码片段。同样,如果您发现自己只是使用 Coroutine 在特定时间后调用函数,请使用 Invoke。当你在连续布置多个函数调用或 Lerps 时做任何更复杂的事情时,Coroutine 是一个非常有用的工具。

我还要补充一点 Coroutines 非常灵活,您可以出于任何原因使用 yields 暂停它们然后继续它们,而 Invoke 通常在 X 之后调用一次秒,或者如果使用 InvokeRepeating,则在 X 秒后每 Y 秒调用一次。使用 Coroutines 的另一个有用部分是您可以将参数传递给它们,而使用 Invoke 则不能。我想一个过于简单的思考方式是 Invoke 是一个非常简单的一般情况,用于一次性使用 Coroutines 或非常固定的时间重复方法。

你可以修改协程,而不是等待,不断检查玩家是否静止,并在 3 秒后调用,否则中断。

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

如果你有一个 Update 检查,实际上我不会在这里使用协程,而是像

这样的简单计数器
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;
   }
}

在这个用例中,我认为这比协程更容易维护。

如果你真的想使用协程,我会这样做

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();
}