如何延迟 Unity 和 C# 中的方法?

How to delay a method in Unity and C#?

我叫 Laurenz,我的问题是如何使用 C# 在 Unity 中延迟我的精灵的颜色变化。

现在我有一个随机生成器,它根据数字选择颜色,但每一帧都会发生这种情况。所以现在真正的挑战是如何延迟它以减少它的变化。

public class colorchange : MonoBehaviour
{
    public int color;
    public bool stop = true;
    void Start()
    {

    }

    void Update()
    {

        Debug.Log("Hello");
        color = Random.Range(1, 5);
        if (color == 2)
        {
            gameObject.GetComponent<SpriteRenderer>().color = Color.blue;
        }
        if (color == 3)
        { 
            gameObject.GetComponent<SpriteRenderer>().color = Color.red;
        }
        if (color == 4)
        {
            gameObject.GetComponent<SpriteRenderer>().color = Color.yellow;
        }
    }
}

您可以使用计时器和Time.deltaTime:

public class colorchange : MonoBehaviour
{
    public int color;
    public bool stop = true;
    public float delay;
    private float timer;
    void Start()
    {
        timer = delay;
    }

    void Update()
    {
        timer -= Time.deltaTime;
        if (timer <= 0) {
            timer = delay;
            Debug.Log("Hello");
            color = Random.Range(1, 5);
            if (color == 2)
            {
                gameObject.GetComponent<SpriteRenderer>().color = Color.blue;
            }
            if (color == 3)
            { 
                gameObject.GetComponent<SpriteRenderer>().color = Color.red;
            }
            if (color == 4)
            {
                gameObject.GetComponent<SpriteRenderer>().color = Color.yellow;
            }
        }
    }
}

您可以将代码放入 Coroutine 的循环中,每隔几秒迭代一次:

public class colorchange : MonoBehaviour
{
    public int color;        
    public float delaySeconds = 1f;
    IEnumerator changeColorCoroutine;

    SpriteRenderer mySprite;

    public bool doChangeColor;

    void Start()
    {
        // cache result of expensive GetComponent call
        mySprite = GetComponent<SpriteRenderer>();

        // initialize flag
        doChangeColor = true;

        // create coroutine
        changeColorCoroutine = ChangeColor();

        // start coroutine
        StartCoroutine(changeColorCoroutine);
    }

    void OnMouseDown()
    {
        // toggle doChangeColor
        doChangeColor = !doChangeColor;
    }

    IEnumerator ChangeColor()
    {
        WaitUntil waitForFlag = new WaitUntil( () => doChangeColor);

        while (true)
        {
            yield return waitForFlag;

            Debug.Log("Hello");
            color = Random.Range(1, 5);

            // switch for neater code
            switch (color)
            {
            case 2:
                mySprite.color = Color.blue;
                break;

            case 3:
                mySprite.color = Color.red;
                break;

            case 4:
                mySprite.color = Color.yellow;
                break;
            }

            yield return new WaitForSeconds(delaySeconds);
        }
    }
}

你可以用一个简单的协同程序来处理这个问题。如果您不知道如何操作,请查看 Unity 官方 API 文档中的 link ( https://docs.unity3d.com/ScriptReference/WaitForSeconds.html )。

如果不想使用协程,可以使用Unity的InvokeRepating方法。创建一个用于更改颜色的新函数,并每隔 x 秒调用一次。 这也是 InvokeRepating 方法的 link https://docs.unity3d.com/ScriptReference/MonoBehaviour.InvokeRepeating.html

ps:在这种情况下,使用 Switch 确定颜色整数比对每个颜色代码使用 if 和 else if 更有效。它更高效,更容易扩大规模。

有多种方法可以满足您的需求。

计时器

您可以使用计时器每隔 x 时间更改一次颜色。

至少有两种方法可以做到这一点:

1

public class colorchange : MonoBehaviour
{
    public int color;
    public bool stop = true;

    private SpriteRenderer _mySpriteRenderer;
    private float _timeBetweenChanges = 0.5F;
    private float _lastChange = 0f;

    void Start()
    {
        _mySpriteRenderer = GetComponent<SpriteRenderer>();
    }

    void Update()
    {
        if (Time.time - _lastChange >= _timeBetweenChanges)
        {
            _lastChange = Time.time;
            color = Random.Range(1, 5);

            if (color == 2)
            {
                _mySpriteRenderer.color = Color.blue;
            }
            if (color == 3)
            { 
                _mySpriteRenderer.color = Color.red;
            }
            if (color == 4)
            {
                _mySpriteRenderer.color = Color.yellow;
            }
        }
    }
}

注意: 您可以缓存您的组件 SpriteRenderer 以避免每次调用 GetComponentSee here

2

public class colorchange : MonoBehaviour
{
    public int color;
    public bool stop = true;

    private SpriteRenderer _mySpriteRenderer;
    private float _timeBetweenChanges = 0.5F;

    void Start()
    {
        _mySpriteRenderer = GetComponent<SpriteRenderer>();
        InvokeRepeating("ChangeColor", 0F, _timeBetweenChanges);
    }

    void Update()
    {
        // You don't need Update here, you can safly remove it unless you need it for anything else
    }

    void ChangeColor()
    {
        color = Random.Range(1, 5);
        if (color == 2)
        {
            _mySpriteRenderer.color = Color.blue;
        }
        if (color == 3)
        { 
            _mySpriteRenderer.color = Color.red;
        }
        if (color == 4)
        {
            _mySpriteRenderer.color = Color.yellow;
        }
    }
}

随机

你可以用一个RNG来决定你是否要改变这一帧的颜色。这意味着您无法定义每次 sec / min / in general 更改颜色的次数。

public class colorchange : MonoBehaviour
{
    public int color;
    public bool stop = true;

    private SpriteRenderer _mySpriteRenderer;
    private float _randomChance = 0.2F;

    void Start()
    {
        _mySpriteRenderer = GetComponent<SpriteRenderer>();
    }

    void Update()
    {
        if (Random.Range(0F, 1F) < _randomChance)
        {
            color = Random.Range(1, 5);

            if (color == 2)
            {
                _mySpriteRenderer.color = Color.blue;
            }
            if (color == 3)
            { 
                _mySpriteRenderer.color = Color.red;
            }
            if (color == 4)
            {
                _mySpriteRenderer.color = Color.yellow;
            }
        }
    }
}

这里有 20 个

or a would be to use InvokeRepeating and stop it using CancelInvoke 的另一种选择。在像您这样的用例中,我发现这种方式更易于实施和控制:

public float Interval = 1;

[SerializeField] private SpriteRenderer spriteRenderer;

private void Awake()
{
    // DO THIS ONLY ONCE
    if(!spriteRenderer) spriteRenderer = GetComponent<SpriteRenderer>();
}

// Automatically called when this component or GameObject gets enabled
private void OnEnable ()
{
    // Start invoking every Interval seconds
    InvokeRepeating(nameof(ChangeColor), Interval, Interval);
}

// Automatically called when this component or GameObject gets disabled
private void OnDisable()
{
    // Stop the repeated invoking 
    CancelInvoke();
}

private void ChangeColor()
{
    Debug.Log("Hello");
    color = Random.Range(1, 5);

    // You should also use a switch case here
    switch(color)
    {
        case 2:
            spriteRenderer.color = Color.blue;
            break;

        case 3:
            spriteRenderer.color = Color.red;
            break;

        case 4:
             spriteRenderer.color = Color.yellow;
             break;
    }
}

然后您可以通过启用和禁用此组件来简单地启用和禁用颜色更改。

或者简单地将相应的代码行从 OnEnableOnDisable 移动到相应的 public 方法,如 public void StartColorChangepublic void StopColorChange.

这可以通过多种方式完成。在向您展示具体内容之前,这是我将使用的所有示例代码通用的基本结构:

public class ColorChanger : MonoBehaviour {
    //Avoid Find and GetComponent methods in performance-critical contexts like Update and FixedUpdate
    //Store the value once in the beginning. This is called 'caching'
    public SpriteRenderer _renderer;

    //Don't hard-code stuff like this
    public Color[] _colors;

    public float _colorChangeInterval = 0.5f;

    //Convenience property to access _renderer.color
    public Color Color {
        get => _renderer.color;
        set => _renderer.color = value;
    }

    private void Start() {
        //Attempts to find the SpriteRenderer in the object if it wasn't set in the inspector
        if (!_renderer)
            _renderer = GetComponent<SpriteRenderer>();
    }

    //This piece of code does a specific thing, so it's best to put it in a method
    public void ChangeColor() {
        if (_colors.Length < 1)
            Debug.LogError($"You forgot to set {nameof(_colors)} in the Inspector. Shame! Shame!");
        Color = _colors[Random.Range(0, _colors.Length - 1)];
    }
}

在我看来,以下是一些主要内容,按照直观程度排序:


计时器模式:

有两种口味。

1) 可以是经过时间的累加器(如下面的代码),或者相反,从间隔递减到零:

private float _elapsed;

private void Update() {
    _elapsed += Time.deltaTime;

    if (_elapsed < _colorChangeInterval)
        return;
    ChangeColor();
    _elapsed %= _colorChangeInterval;
}

或 2) 可以是从上次开始或直到下一次(如下所示)的时间戳检查触发器,时间戳:

//Replaces _elapsed
private float _timestamp;

private void Start() {
    //...
    _timestamp = Time.time; //Initial timestamp
}

private void Update() {
    if (Time.time < _timestamp + _colorChangeInterval)
        return;
    ChangeColor();
    _timestamp = Time.time;
}

协程和 WaitForSeconds:

当您需要延迟或统一序列代码时,这是推荐的程序。

注意unity还提供了其他类型的等待方法,如WaitWhileWaitUntil等...

//Since unlike code in Update, coroutines need to be started and stopped, we start it when the script is enabled
private void OnEnable() {
    StartCoroutine(ChangeColorContinuously());
}

//This is automatically stopped by unity when the script is disabled
private IEnumerator ChangeColorContinuously() {
    while (true) {
        yield return new WaitForSeconds(_colorChangeInterval);
        ChangeColor();
    }
}

不要执行异步等待!

好吧,它可以完成,但是它有很多陷阱并且非常 推荐给初学者。

而且无论如何它都无意取代协程。


不要执行 InvokeRepeating!

这是一种依赖魔法字符串和反射的方法。对于示例代码的快速简单设置很有用,但如果可能的话(由于上述方法,这是可能的)应该像生产代码中的瘟疫一样避免。