如何在Unity中简单实现脚本等待/休眠

99

我该如何在TextUI.text = ....之间放置一个睡眠函数,以便在每个短语之间等待3秒钟?

public Text GuessUI;
public Text TextUI;

[...truncated...]

TextUI.text = "Welcome to Number Wizard!";
TextUI.text = ("The highest number you can pick is " + max);
TextUI.text = ("The lowest number you can pick is " + min);

我已经尝试了各种方法,但都没有奏效,比如这个:

TextUI.text = "Welcome to Number Wizard!";
yield WaitForSeconds (3);
TextUI.text = ("The highest number you can pick is " + max);
yield WaitForSeconds (3);
TextUI.text = ("The lowest number you can pick is " + min);
在Bash中,它将是这样:
echo "Welcome to Number Wizard!"
sleep 3
echo "The highest number you can pick is 1000"
sleep 3
.....

但我不知道如何在Unity中使用C#实现这一点。


1
“didn't worked” 究竟是什么意思? - Max Yankov
1
yield WaitForSeconds (3); 不起作用。 - DiogoSaraiva
2
“不起作用”到底是什么意思? - Max Yankov
1
Thread.Sleep(3000)有什么问题? - Mustafa Ekici
1
我认为他们的意思是它没有减速。 - RadicalRhino
9个回答

178

在Unity中有许多等待的方法。它们非常简单,但我认为值得介绍最常用的几种方法:

1.使用协程和WaitForSeconds

这是最简单的方法。将需要等待一段时间的所有代码放入协程函数中,然后可以使用WaitForSeconds进行等待。请注意,在协程函数中,您需要使用StartCoroutine(yourFunction)调用函数。

以下示例将旋转90度,等待4秒钟,旋转40度并等待2秒钟,最后旋转20度。

void Start()
{
    StartCoroutine(waiter());
}

IEnumerator waiter()
{
    //Rotate 90 deg
    transform.Rotate(new Vector3(90, 0, 0), Space.World);

    //Wait for 4 seconds
    yield return new WaitForSeconds(4);

    //Rotate 40 deg
    transform.Rotate(new Vector3(40, 0, 0), Space.World);

    //Wait for 2 seconds
    yield return new WaitForSeconds(2);

    //Rotate 20 deg
    transform.Rotate(new Vector3(20, 0, 0), Space.World);
}

2.使用协程和WaitForSecondsRealtime

WaitForSecondsWaitForSecondsRealtime之间唯一的区别是WaitForSecondsRealtime使用未缩放时间进行等待,这意味着当使用Time.timeScale暂停游戏时,WaitForSecondsRealtime函数不会受到影响,但WaitForSeconds会受到影响。

void Start()
{
    StartCoroutine(waiter());
}

IEnumerator waiter()
{
    //Rotate 90 deg
    transform.Rotate(new Vector3(90, 0, 0), Space.World);

    //Wait for 4 seconds
    yield return new WaitForSecondsRealtime(4);

    //Rotate 40 deg
    transform.Rotate(new Vector3(40, 0, 0), Space.World);

    //Wait for 2 seconds
    yield return new WaitForSecondsRealtime(2);

    //Rotate 20 deg
    transform.Rotate(new Vector3(20, 0, 0), Space.World);
}

等待并仍然能够看到已等待的时间:

3.使用协程并每帧使用Time.deltaTime递增一个变量。

一个很好的例子是当你需要计时器在屏幕上显示已等待了多长时间。基本上像一个计时器。

当你想要使用一个boolean变量来中断等待/休眠时,这也是很好的。这就是可以使用yield break;的地方。

bool quit = false;

void Start()
{
    StartCoroutine(waiter());
}

IEnumerator waiter()
{
    float counter = 0;
    //Rotate 90 deg
    transform.Rotate(new Vector3(90, 0, 0), Space.World);

    //Wait for 4 seconds
    float waitTime = 4;
    while (counter < waitTime)
    {
        //Increment Timer until counter >= waitTime
        counter += Time.deltaTime;
        Debug.Log("We have waited for: " + counter + " seconds");
        //Wait for a frame so that Unity doesn't freeze
        //Check if we want to quit this function
        if (quit)
        {
            //Quit function
            yield break;
        }
        yield return null;
    }

    //Rotate 40 deg
    transform.Rotate(new Vector3(40, 0, 0), Space.World);

    //Wait for 2 seconds
    waitTime = 2;
    //Reset counter
    counter = 0;
    while (counter < waitTime)
    {
        //Increment Timer until counter >= waitTime
        counter += Time.deltaTime;
        Debug.Log("We have waited for: " + counter + " seconds");
        //Check if we want to quit this function
        if (quit)
        {
            //Quit function
            yield break;
        }
        //Wait for a frame so that Unity doesn't freeze
        yield return null;
    }

    //Rotate 20 deg
    transform.Rotate(new Vector3(20, 0, 0), Space.World);
}

你可以通过将while循环移动到另一个协程函数中并进行yield操作来简化此过程,同时仍然能够看到计数器的计数,甚至可以中断计数。
bool quit = false;

void Start()
{
    StartCoroutine(waiter());
}

IEnumerator waiter()
{
    //Rotate 90 deg
    transform.Rotate(new Vector3(90, 0, 0), Space.World);

    //Wait for 4 seconds
    float waitTime = 4;
    yield return wait(waitTime);

    //Rotate 40 deg
    transform.Rotate(new Vector3(40, 0, 0), Space.World);

    //Wait for 2 seconds
    waitTime = 2;
    yield return wait(waitTime);

    //Rotate 20 deg
    transform.Rotate(new Vector3(20, 0, 0), Space.World);
}

IEnumerator wait(float waitTime)
{
    float counter = 0;

    while (counter < waitTime)
    {
        //Increment Timer until counter >= waitTime
        counter += Time.deltaTime;
        Debug.Log("We have waited for: " + counter + " seconds");
        if (quit)
        {
            //Quit function
            yield break;
        }
        //Wait for a frame so that Unity doesn't freeze
        yield return null;
    }
}

等待/休眠直到变量改变或等于另一个值:

4.使用协程和WaitUntil函数:

等待直到条件成为true。例如,一个函数等待玩家的得分为100,然后加载下一关卡。

float playerScore = 0;
int nextScene = 0;

void Start()
{
    StartCoroutine(sceneLoader());
}

IEnumerator sceneLoader()
{
    Debug.Log("Waiting for Player score to be >=100 ");
    yield return new WaitUntil(() => playerScore >= 10);
    Debug.Log("Player score is >=100. Loading next Level");

    //Increment and Load next scene
    nextScene++;
    SceneManager.LoadScene(nextScene);
}

5.使用协程和WaitWhile函数。

在条件为true时等待。例如,当您想要在按下escape键时退出应用程序时。

void Start()
{
    StartCoroutine(inputWaiter());
}

IEnumerator inputWaiter()
{
    Debug.Log("Waiting for the Exit button to be pressed");
    yield return new WaitWhile(() => !Input.GetKeyDown(KeyCode.Escape));
    Debug.Log("Exit button has been pressed. Leaving Application");

    //Exit program
    Quit();
}

void Quit()
{
    #if UNITY_EDITOR
    UnityEditor.EditorApplication.isPlaying = false;
    #else
    Application.Quit();
    #endif
}

6.使用Invoke函数:

您可以告诉Unity在将来调用函数。当您调用Invoke函数时,可以将要调用的函数作为第一个参数传递给它,将要等待的时间作为第二个参数传递给它。下面的示例将在Invoke被调用后的5秒后调用feedDog()函数。

void Start()
{
    Invoke("feedDog", 5);
    Debug.Log("Will feed dog after 5 seconds");
}

void feedDog()
{
    Debug.Log("Now feeding Dog");
}

7.通过使用Update()函数和Time.deltaTime

这个方法与#3类似,只是不使用协程,而是使用Update函数。

这种方法的问题在于需要大量变量来确保计时器在等待结束后仅运行一次,而非每次都运行。

float timer = 0;
bool timerReached = false;

void Update()
{
    if (!timerReached)
        timer += Time.deltaTime;

    if (!timerReached && timer > 5)
    {
        Debug.Log("Done waiting");
        feedDog();

        //Set to false so that We don't run this again
        timerReached = true;
    }
}

void feedDog()
{
    Debug.Log("Now feeding Dog");
}

在Unity中,还有其他等待的方式,但你一定要知道上面提到的这些,因为这样可以更容易地制作Unity游戏。何时使用每种方式取决于具体情况。

针对您的特定问题,这是解决方案:

IEnumerator showTextFuntion()
{
    TextUI.text = "Welcome to Number Wizard!";
    yield return new WaitForSeconds(3f);
    TextUI.text = ("The highest number you can pick is " + max);
    yield return new WaitForSeconds(3f);
    TextUI.text = ("The lowest number you can pick is " + min);
}

要从您的 start 或 Update 函数中调用/启动协程函数,您需要使用以下方式调用:

StartCoroutine (showTextFuntion());

4
除了invoke之外,这些都是协程。实际上,这只是一个方法。 - Tyler S. Loeper
3
@TylerSigi在第7条中使用了Update函数,其余大部分使用协程但等待方式不同。它们不是为了好玩而被添加的,它们在不同的场景中是必需的,否则您将无法获得所需的行为,或者您最终将重新发明轮子,而Unity提供的内置函数可以处理一些东西。 - Programmer
关于Invoke(和InvokeRepeating)的特别说明:据我所知,它与其他方法不同,也可以在未激活的GameObject或禁用的组件上工作,这使得它在某些情况下非常强大! - derHugo
能否在不使用协程的情况下实现? - Uzair_07

14

你在使用 WaitForSeconds 上是正确的。但我怀疑您尝试在没有协程的情况下使用它。这是它的正确使用方法:

public void SomeMethod()
{
    StartCoroutine(SomeCoroutine());
}

private IEnumerator SomeCoroutine()
{
    TextUI.text = "Welcome to Number Wizard!";
    yield return new WaitForSeconds (3);
    TextUI.text = ("The highest number you can pick is " + max);
    yield return new WaitForSeconds (3);
    TextUI.text = ("The lowest number you can pick is " + min);
}

1
我不明白... 我应该用什么来替换SomeCoroutine? - DiogoSaraiva
你必须将“WaitForSeconds”作为iEnumerator的yeild才能使其正常工作。尝试了解Unity协程。 - Plastic Sturgeon

8

使用 .Net 4.x,您可以使用基于任务的异步模式(TAP)来实现此目的:

// .NET 4.x async-await
using UnityEngine;
using System.Threading.Tasks;
public class AsyncAwaitExample : MonoBehaviour
{
     private async void Start()
     {
        Debug.Log("Wait.");
        await WaitOneSecondAsync();
        DoMoreStuff(); // Will not execute until WaitOneSecond has completed
     }
    private async Task WaitOneSecondAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Debug.Log("Finished waiting.");
    }
}

这是一个使用 .Net 4.x 和 Unity 的功能,请参见此链接以获取更多信息
同时,此链接提供了示例项目,并与协程进行比较
但要注意,文档中指出这并不能完全替代协程

1
据我所知,这是不稳定的,因为异步/等待任务未绑定到Unity游戏对象。这会导致任务在播放模式退出后继续运行。 - Isaak Eriksson
1
@IsaakEriksson 是的,但是可以通过标准的.NET Task最佳实践轻松纠正,方法是在创建时附加一个CancellationToken(通过CancellationTokenSource创建),这样当您希望取消任何Task时,只需调用CancellationTokenSource.Cancel()即可。在Unity中,当退出Play模式并返回Edit模式时,您可以利用EditorApplication.playModeStateChanged特别是PlayModeStateChange.EnteredEditMode和/或PlayModeStateChange.ExitingPlayMode,此时您可以Cancel您的任务。我把所有这些都放在我的class EditorWatcher中... - user585968
1
使用[InitializeOnLoad]标记。我将始终使用基于时间的简单函数或者如果需要在工作线程上执行真正异步操作,使用async/await而不是Unity伪协程,因为后者与.NET的所有行为不一致。 :) - user585968
1
有道理。然而,在实践中,当我们忘记添加取消令牌时,这会成为错误的源头,尤其是在更大的代码库中。 - Isaak Eriksson

2
记住协程堆栈!如果在Update()中启动协程,您可能会得到大量的协程等待排队,并且几乎同时执行,就在您的等待之后。为了避免这种情况,一个好的方法是使用布尔值,防止“堆叠”协程:
  bool isRunning = false;
  IEnumerator MyCoroutine(){
    isRunning = true;
    print("started");
    yield return new WaitForSeconds(3);
    print("3 seconds elapsed");
    yield return new WaitForSeconds(3);
    print("more 3 seconds");
    yield return new WaitForSeconds(2);
    print("ended");
    isRunning = false;
  }
  void Update(){
    if (!isRunning) StartCoroutine(MyCoroutine());
  }

来源:https://answers.unity.com/questions/309613/calling-startcoroutine-multiple-times-seems-to-sta.html


这似乎有点不必要...为什么不直接在MyCoroutine中放一个while(true)循环呢? - derHugo

0

这个关于async任务的回答中补充一点

还有一个(免费的)第三方库{{link2:UniTask}}(也可以通过OpenUPM获得),它提供了一个更轻量级的async实现,并且还能无缝地集成异步与Unity主线程之间的交互

以下是他们仓库中的示例

// extension awaiter/methods can be used by this namespace
using Cysharp.Threading.Tasks;

// You can return type as struct UniTask<T>(or UniTask), it is unity specialized lightweight alternative of Task<T>
// zero allocation and fast excution for zero overhead async/await integrate with Unity
async UniTask<string> DemoAsync()
{
    // You can await Unity's AsyncObject
    var asset = await Resources.LoadAsync<TextAsset>("foo");
    var txt = (await UnityWebRequest.Get("https://...").SendWebRequest()).downloadHandler.text;
    await SceneManager.LoadSceneAsync("scene2");

    // .WithCancellation enables Cancel, GetCancellationTokenOnDestroy synchornizes with lifetime of GameObject
    var asset2 = await Resources.LoadAsync<TextAsset>("bar").WithCancellation(this.GetCancellationTokenOnDestroy());

    // .ToUniTask accepts progress callback(and all options), Progress.Create is a lightweight alternative of IProgress<T>
    var asset3 = await Resources.LoadAsync<TextAsset>("baz").ToUniTask(Progress.Create<float>(x => Debug.Log(x)));

    // await frame-based operation like a coroutine
    await UniTask.DelayFrame(100); 

    // replacement of yield return new WaitForSeconds/WaitForSecondsRealtime
    await UniTask.Delay(TimeSpan.FromSeconds(10), ignoreTimeScale: false);
    
    // yield any playerloop timing(PreUpdate, Update, LateUpdate, etc...)
    await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);

    // replacement of yield return null
    await UniTask.Yield();
    await UniTask.NextFrame();

    // replacement of WaitForEndOfFrame(requires MonoBehaviour(CoroutineRunner))
    await UniTask.WaitForEndOfFrame(this); // this is MonoBehaviour

    // replacement of yield return new WaitForFixedUpdate(same as UniTask.Yield(PlayerLoopTiming.FixedUpdate))
    await UniTask.WaitForFixedUpdate();
    
    // replacement of yield return WaitUntil
    await UniTask.WaitUntil(() => isActive == false);

    // special helper of WaitUntil
    await UniTask.WaitUntilValueChanged(this, x => x.isActive);

    // You can await IEnumerator coroutines
    await FooCoroutineEnumerator();

    // You can await a standard task
    await Task.Run(() => 100);

    // Multithreading, run on ThreadPool under this code
    await UniTask.SwitchToThreadPool();

    /* work on ThreadPool */

    // return to MainThread(same as `ObserveOnMainThread` in UniRx)
    await UniTask.SwitchToMainThread();

    // get async webrequest
    async UniTask<string> GetTextAsync(UnityWebRequest req)
    {
        var op = await req.SendWebRequest();
        return op.downloadHandler.text;
    }

    var task1 = GetTextAsync(UnityWebRequest.Get("http://google.com"));
    var task2 = GetTextAsync(UnityWebRequest.Get("http://bing.com"));
    var task3 = GetTextAsync(UnityWebRequest.Get("http://yahoo.com"));

    // concurrent async-wait and get results easily by tuple syntax
    var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3);

    // shorthand of WhenAll, tuple can await directly
    var (google2, bing2, yahoo2) = await (task1, task2, task3);

    // return async-value.(or you can use `UniTask`(no result), `UniTaskVoid`(fire and forget)).
    return (asset as TextAsset)?.text ?? throw new InvalidOperationException("Asset not found");
}
在这种情况下,这可能只是看起来像例如。
public void RunNumberWizard()
{
    // allows to run that async task without awaiting it
    RunNumberWizardAsync().Forget();
}

private async UniTask RunNumberWizardAsync()
{
    TextUI.text = "Welcome to Number Wizard!";
    await UniTask.Delay(TimeSpan.FromSeconds(3));
    TextUI.text = ("The highest number you can pick is " + max);
    await UniTask.Delay(TimeSpan.FromSeconds(3));
    TextUI.text = ("The lowest number you can pick is " + min);
}

0
这是我写的一段代码的例子,希望能对你有所帮助。
using System.Collections;
using UnityEngine;


 public class HideOnStart : MonoBehaviour
    {
        enum Priority
        {
            INSTANT,
            DELAYED,
            LAST
        }

        [SerializeField] Priority priority;

        void Start()
        {
            if (priority == Priority.INSTANT) gameObject.SetActive(false);
            else { StartCoroutine(delay()); }
        }

        IEnumerator delay()
        {
            yield return new WaitForSeconds((int)priority / 50);
            gameObject.SetActive(false);
        }
    }

-1

使用 async 和 await

public void Start() {
     doTask();
}

 async void doTask() {
        Debug.Log("Long running task started");
        // wait for 5 seconds, update your UI
        await Task.Delay(TimeSpan.FromSeconds(5f));

        // update your UI
        Debug.Log("Long running task has completed");
}

2
“async void”通常是一种反模式!它会阻止任何适当的异常处理!此外,请注意,Unity API大多数情况下只能在主线程上使用。因此,如果您使用“async”,您必须确保延迟代码实际上是在Unity主线程上再次执行!因此,对于这种类型的任务,使用“async”基本上没有任何意义;) - derHugo

-1

这里有一种更简单的方法,不需要使用StartCoroutine:

float t = 0f;
float waittime = 1f;

并且在Update/FixedUpdate中:

if (t < 0){
    t += Time.deltaTIme / waittime;
    yield return t;
}

3
你不能在Update/FixedUpdate中使用yield return,因为这些方法的返回类型是void。 - Matthew

-2

//这是我用Unity编写的等待代码示例,我使用一个值并在每个更新中更新它,一旦它达到if语句正在寻找的值,就会运行任务。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnterCarCollider : MonoBehaviour
{
    public GameObject player;

    //Calls & Delcares vehicle objects
    public GameObject Camera;
    public VehicleControl ascript;
    public Collider enterDriverCollider;
    public Collider parkBreakCollider;
    public GameObject enterVehicleDriverToolTip;

    public int forStayInTime = 32;
    public int timeInActiveTriggeredCollider;

    private void Start()
    {
        ascript = GetComponent<VehicleControl>();
        timeInActiveTriggeredCollider = 0;
    }

    private void OnTriggerStay(Collider other)
    {
        if (forStayInTime <= timeInActiveTriggeredCollider)
        {
            if (Input.GetKey(KeyCode.E))
            {
                ascript.enabled = !ascript.enabled;
                Camera.active = true;
                player.active = false;
                enterDriverCollider.enabled = false;
                parkBreakCollider.enabled = false;
           }
           // TODO: Enter car message
           enterVehicleDriverToolTip.active = true;
        }
        timeInActiveTriggeredCollider++;
    }

    private void OnTriggerExit(Collider other)
    {
        enterVehicleDriverToolTip.active = false;
        timeInActiveTriggeredCollider = 0;
    }

    private void Update()
    {
        if (enterDriverCollider.enabled is false)
        {
            timeInActiveTriggeredCollider = 0;
        }
    }
}

这个补充了什么,是之前的这个答案没有提到的吗? - derHugo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接