Search code examples
c#unity-game-engineflashing

Unity: flash at frequency with variable on/off ratio


I want to be able to flash stuff at a certain frequency. For an example, let's say 2Hz. I also want to be able to specify a ratio, where I can have the thing displayed for let's say 2/3 of the cycle and have it hidden for 1/3, so the ratio would be 2:1. It's a wild bunch of flashing, so I Need to stay flexible in the way I do it. There might be some flashing with a ratio of 3:5 and a frequency of 2Hz, and some other flashing at 4Hz with ratio 1:1, and so on.

Also, I need to be able to flash in sync. So if one object is flashing already and I start flashing another one, they need to be in sync (or rather their cycles need to be in sync, the flashing may vary as the ratio may be different). But if at the same frequency, they need to "turn on" at the same time, even if their ratios are different. Also, they all need to turn on at the same time the slowest turns on.

My current approach: I have a GameObject FlashCycle, that essentially in it's update method calculates a progress for the 3 frequency's I have (2Hz, 4Hz and 8Hz).

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

I have tried different times, but that didn't really matter so let's just stick to that one if you don't think it's a bad idea!

Now, whenever I want to flash an object, in it's own Update() I do this:

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

and then just continue and have the object displayed if show == true.

Unfortunately this doesn't flash the objects at a nice smooth and regular interval. I measured the 2Hz interval and got differences in the ratio of up to 48ms, and though it seems like not much it really makes a difference on the screen.

So the question boils down to: How can I get quick, reqular flashes while maintaining the flexibility (ratio and frequency wise) and have a syncronized flash?

Thanks for your help!


Solution

  • You could use Coroutines and WaitForSeconds to achieve that

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

    so you can call it either with a frequency e.g. 8Hz

    StartCoroutine(Flash(8.0f));
    

    this is actually equal to any call where you set onRatio = offRatio e.g.

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

    or with a frequency and ratios e.g. 1(on):2(off) with 8Hz

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

    With this setup the Coroutine runs "forever" in the while(true)-loop. So, don't forget before you start a new Coroutine with different parameters to first stop all routines with

     StopAllCoroutines();
    

    Now if you want to change that dynamically in an Update method, you would have to add some controll flags and additional variables in roder to make sure a new Coroutine is only called when something changed:

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

    Notes:

    1. I dont know your FlashRateInterval but if you need to use it for some reason you could make it like

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

      in order to directly use the correct values.

    2. I would call a frequency variable flashRate.herz. You also wouldn't call a size value cube.meters. I'ld recommend to rename it to flashRate.frequency.


    To archieve that syncing you would somehow need access to all Behaviours and compare their values (so I'ld say some static List<YourBehavior>) and than e.g. in the Coroutine wait until all bools are e.g. set to true before continuing with your own one. For that you would need an additional bool since it is possible that show is true permanently on one component.

    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)
        {
    
        .........
    

    Instead of using FindObjectsOfType<YOUR_COMPONENT>() which is kind of slow you could also do something like

    public static List<YOUR_COMPONENT> Components = new List<YOUR_COMPONENT>();
    
    private void Awake()
    {
        if(!Components.Contains(this)){
            Components.Add(this);
        }
    }
    

    so you also get currently disabled components and objects