Unity 协程组合延迟比预期的要长,有多个 WaitForSeconds

Unity Coroutine combined delay is longer than expected with multiple WaitForSeconds

提问人:Kureyş Alp Kılıç 提问时间:9/12/2023 最后编辑:derHugoKureyş Alp Kılıç 更新时间:9/12/2023 访问量:65

问:

我正在启动一个协程,该协程迭代循环多次,并在每次迭代之间等待一定时间。但是循环的压缩时间比预期的要长得多。代码如下:

private IEnumerator CO_AddBladesInTime(int count, Blade blade)
    {
        var _spawnPos = blade.transform.position;
        var _counter = 0;

        var _firstTime = Time.time;
        var _calculatedTime = 0f;

        Debug.Log("entered coroutine at " + _firstTime);

        while (_counter < count)
        {
            _counter++;
            // AddNewBlade(_spawnPos, blade.Level);
            var _waitTime = bladeSpawnFreq / count;
            _calculatedTime += _waitTime;
            yield return new WaitForSeconds(_waitTime);
        }

        var _secondTime = Time.time;
        Debug.Log("exited coroutine at " + _secondTime);
        Debug.Log($"waited {_secondTime - _firstTime}");
        Debug.Log($"expected wait time {_calculatedTime}");
    }

结果如下:

Log

我错过了什么吗?

C# 循环 unity-game-engine ienumerator

评论

2赞 Retired Ninja 9/12/2023
docs.unity3d.com/ScriptReference/WaitForSeconds.html 有一些解释为什么时间不准确。协程在一帧中仅运行一次,因此它将取决于帧速率。
0赞 Kureyş Alp Kılıç 9/12/2023
@RetiredNinja 因此,unity 不能在一个帧中多次运行内容。我能做些什么来克服这个问题?
0赞 Alexei Levenkov 9/12/2023
考虑仔细重读 docs.unity3d.com/ScriptReference/Time-time.html,并找出您真正感兴趣的时间......基本上,你需要将完成时间与“当前”时间进行比较(无论你决定哪一个 - 真实、真实、可以暂停、模型/缩放时间),但你需要弄清楚你真正想要哪一个。
0赞 Alexei Levenkov 9/12/2023
请看看我的标题编辑是否有意义(因为原始标题编辑太通用了) - 如果这与您遇到的问题不匹配,请随时改进(而不是回滚)。
0赞 Retired Ninja 9/12/2023
一种方法是使用简单的产率来等待一帧,在协程中累积使用和循环,以将累积的时间用于将剩余时间保留到下一帧。Time.deltaTime

答:

0赞 derHugo 9/12/2023 #1

如前所述,目前您的协程将等待最少的帧数。count

WaitForSeconds基本上有点类似于

for(var time = 0f; time < timeToWait; time += Time.deltaTime)
{
    yield return null;
}

因此,即使非常非常小,例如 它仍然会等待一整帧 - 假设帧速率为 秒!timeToWait0.0000160 f/s0.017

也不知道你为什么使用它

var _waitTime = bladeSpawnFreq / count; 

我假设已经指每秒生成多少个对象。相反,我宁愿期望你计算bladeSpawnFreq

var _waitTime = 1 / bladeSpawnFreq;

两次生成之间的时间(以秒为单位)。


所以现在这可能太花哨了,可能会有更直接的解决方案,但你可以做的是异步。

// Wrapper for a thread-safe counter
private class CounterWrapper
{
    private int counter;
    
    public void Add()
    {
        Interlocked.Increment(ref counter);
    }

    public bool Reset(out int value)
    {
        value = Interlocked.Exchange(ref counter, 0);
        
        return value > 0;
    }
}

private async Task ScheduleSpawns(int count, CounterWrapper counter)
{
    var millisecondsBetweenSpawns = Mathf.RoundToInt(bladeSpawnFreq * 1000);

    for(var i = 0; i < count; i++)
    {
        counter.Add();
        await Task.Delay(millisecondsBetweenSpawns);
    }
}

private IEnumerator CO_AddBladesInTime(int count, Blade blade)
{
    var _spawnPos = blade.transform.position;

    var counter = new CounterWrapper();
    var task = Task.Run(async () => await ScheduleSpawns(count, counter));

    var _firstTime = Time.time;
    var _expectedDuration = 1 / bladeSpawnFreq * count;
    
    while(!task.IsCompleted || counter.Reset(out var toSpawn))
    {
        // each frame you pawn as many items as scheduled by the task to fulfill the spawn rate
        for(var i = 0; i < toSpawn; i++)
        {
            AddNewBlade(_spawnPos, blade.Level);
        }

        // this yields for one frame
        yield return null;
    }

    var _secondTime = Time.time;
    Debug.Log("exited coroutine at " + _secondTime);
    Debug.Log($"duration {_secondTime - _firstTime}");
    Debug.Log($"expected duration {_expectedDuration}");
}

所以这里发生的事情是

  • 将任务作为异步任务启动(->甚至可能在不同的线程上运行)。ScheduleSpawns
  • 它以毫秒为单位发生,并且比 Unity 帧更精确(如前所述,对于 60 fps,仅一帧就已经是 17 毫秒长)Task.Delay
  • 对计划的生成使用线程安全计数器
  • 异步任务更精确地增加了应生成多少项的计数器
  • Unity 主线程上的 Corotuine 每帧都会生成所有必需的项目,同时再次重置计数器

请注意,由于经常提到的帧的性质是相当长的;),因此最终可能会非常长。