统一:以可变 on/off 比率的频率闪烁

Unity: flash at frequency with variable on/off ratio

我希望能够以特定频率闪烁东西。例如,假设 2Hz。我还希望能够指定一个比率,我可以在其中显示 2/3 的周期,并隐藏 1/3,因此比率将为 2:1。这是一堆疯狂的闪光,所以我需要在我做的方式上保持灵活性。可能有一些频率为 3:5 且频率为 2Hz 的闪烁,还有一些频率为 4Hz 且频率为 1:1 的闪烁,依此类推。

此外,我需要能够同步闪光。因此,如果一个对象已经在闪烁,而我开始闪烁另一个对象,则它们需要同步(或者更确切地说,它们的周期需要同步,闪烁可能会因比率不同而有所不同)。但是如果是同一个频率,他们需要同时"turn on",即使他们的比例不同。此外,它们都需要同时打开,最慢的打开。

我目前的方法:我有一个 GameObject FlashCycle,本质上是在它的更新方法中计算我拥有的 3 个频率(2Hz、4Hz 和 8Hz)的进度。

 float time = Time.time;
 twoHerzProgress = (time % twoHerzInSeconds) / twoHerzInSeconds;
 fourHerzProgress = (time % fourHerzInSeconds) / fourHerzInSeconds;
 eightHerzProgress = (time % eightHerzInSeconds) / eightHerzInSeconds;

我尝试了不同的 times,但这并不重要,所以如果您认为它不是一个坏主意,我们就坚持使用那个吧!

现在,每当我想闪烁一个对象时,在它自己的 Update() 我这样做:

switch (flashRate.herz)
    {
        case FlashRateInterval.twoHerz:
            show = flashCycle.oneHerzProgress <= onTimePercentage;
        case FlashRateInterval.fourHerz:
            show =flashCycle.twoHerzProgress <= onTimePercentage;
        case FlashRateInterval.eightHerz:
            show =flashCycle.fourHerzProgress <= onTimePercentage;
        default:
            show =true;
    }

然后继续并在 show == true.

时显示对象

不幸的是,这不会以非常平滑和规则的间隔闪烁对象。我测量了2Hz的间隔,得到了高达48ms的比率差异,虽然看起来不大,但在屏幕上确实有所不同。

所以问题归结为:如何在保持灵活性(比率和频率方面)的同时获得快速、规则的闪光并获得同步闪光?

感谢您的帮助!

您有一些不同之处,因为您在 Update() 循环中使用 <= 条件执行所有操作。在 slower/faster 机器上你会有 more/less 差异,因为帧的持续时间永远不会等于你的频率。

尝试在 Corotine 中完成所有操作:unity coroutine docs

//bad code below but i think its more understandable like this
IEnumerator Flash() 
{
   while(true)
   {
     BlinkOn();
     Sync();//sync here another cicle if you want to sync when on starts
     yield return new WaitForSeconds(yourDuration);// yourDuration*multiplier/something+0.5f....ecc

     BlinkOff()
     Sync();//sync here another cicle if you want to sync when of starts
     yield return new WaitForSeconds(yourDuration);
   }
}

您可以使用 Coroutines and WaitForSeconds 来实现

// onRatio and offRatio are "optional" parameters
// If not provided, they will simply have their default value 1
IEnumerator Flash(float frequency ,float onRatio = 1, float offRatio = 1)
{

    float cycleDuration = 1.0f / frequency;
    float onDuration = (onRatio/ (onRatio + offRatio)) * cycleDuration;
    float offDuration = (offRatio/ (onRatio + offRatio)) * cycleDuration; 

    while(true)
    {
        show = true;

        yield return new WatForSeconds(onDuration);        

        show = false;

        yield return new WatForSeconds(offDuration);
    }
}

所以你可以用一个频率来调用它,例如8赫兹

StartCoroutine(Flash(8.0f));

这实际上等于您设置 onRatio = offRatio 的任何调用,例如

StartCoroutine(Flash(8.0f, onRatio = 1, offRatio = 1));

StartCoroutine(Flash(8.0f, onRatio = 2, offRatio = 2));

....

或频率和比率,例如1(开):2(关)8Hz

StartCoroutine(Flash(8.0f, onRatio = 1, offRatio = 2));

使用此设置,协程在 while(true) 循环中运行 "forever"。因此,不要忘记在启动具有不同参数的新协程之前首先使用

停止所有例程
 StopAllCoroutines();

现在,如果您想在 Update 方法中动态更改它,则必须在 roder 中添加一些控制标志和其他变量,以确保仅在发生更改时才调用新的协程:

FlashRateInterval currentInterval;
float currentOnRatio = -1;
float currentOffRatio = -1;

void Update()
{
    // if nothing changed do nothing
    if(flashRate.herz == currentInterval
       //todo && Mathf.Approximately(<yourOnRatio>, currentOnRatio)
       //todo && Mathf.Approximately(<yourOffRatio>, currentOffRatio)
    ) return;

    StopAllCoroutines();

    currentInterval = flashRate.herz;
    //todo currentOnRatio = <yourOnRatio>;
    //todo currentOffRatio = <yourOffRatio>;

    switch (flashRate.herz)
    {
        case FlashRateInterval.twoHerz:
            StartCoroutine(2.0f);
            //todo StartCoroutine(2.0f, onRatio = <yournRatio>, offRatio = <yourOffRatio>);
        case FlashRateInterval.fourHerz:
            StartCoroutine(4.0f);
            //todo StartCoroutine(4.0f, onRatio = <yournRatio>, offRatio = <yourOffRatio>);
        case FlashRateInterval.eightHerz:
            StartCoroutine(8.0f);
            //todo StartCoroutine(8.0f, onRatio = <yournRatio>, offRatio = <yourOffRatio>);
        default:
            show =true;
    }
}

备注:

  1. 我不知道你的 FlashRateInterval 但如果你出于某种原因需要使用它,你可以像

    public enum FlashRateInterval
    {
        AllwaysOn,
    
        twoHerz = 2,
        fourHerz = 4,
        eightHerz = 8
    }
    

    为了直接使用正确的值。

  2. 我会调用一个频率变量flashRate.herz。您也不会调用大小值 cube.meters。我建议将其重命名为 flashRate.frequency.


为了实现同步,您需要以某种方式访问​​所有行为并比较它们的值(所以我会说一些 static List<YourBehavior>),而不是例如在 Coroutine 中等到所有 bools 都是例如在继续你自己的之前设置为真。为此,您需要一个额外的布尔值,因为 show 可能在一个组件上永久为真。

public bool isBlinking;

IEnumerator Flash(float frequency ,float onRatio = 1, float offRatio = 1)
{
    //todo: You'll have to set this false when not blinking -> in Update
    isBlinking = true;

    float cycleDuration = 1.0f / frequency;
    float onDuration = (onRatio/ (onRatio + offRatio)) * cycleDuration;
    float offDuration = (offRatio/ (onRatio + offRatio)) * cycleDuration; 

    // SYNC AT START
    show = false;

    // wait until all show get false
    foreach(var component in FindObjectsOfType<YOUR_COMPONENT>())
    {
        // skip checking this component
        if(component == this) continue;

        // if the component is not running a coroutine skip
        if(!component.isBlinking) continue;

        // Now wait until show gets false
        while(component.show)
        {
            // WaitUntilEndOfFrame makes it possible
            // for us to check the value again already before
            // the next frame
            yield return new WaitForEndOfFrame;
        }
    }

    // => this line is reached when all show are false

    // Now lets just do the same but this time wating for true
    // wait until all show get false
    foreach(var component in FindObjectsOfType<YOUR_COMPONENT>())
    {
        // skip checking this component
        if(component == this) continue;

        // if the component is not running a coroutine skip
        if(!component.isBlinking) continue;

        // Now wait until show gets false
        while(!component.show)
        {
            // WaitUntilEndOfFrame makes it possible
            // for us to check the value again already before
            // the next frame
            yield return new WaitForEndOfFrame;
        }
    }

    // this line is reached when all show are getting true again => begin of loop

    while(true)
    {

    .........

除了使用有点慢的 FindObjectsOfType<YOUR_COMPONENT>(),您还可以使用

public static List<YOUR_COMPONENT> Components = new List<YOUR_COMPONENT>();

private void Awake()
{
    if(!Components.Contains(this)){
        Components.Add(this);
    }
}

因此您还可以获得当前禁用的组件和对象