0

I wrote a simple function that modifies the value of any desired variable gradually to the specified destination within the limited time:

public async Task SetValueInTime(float duration, Func<float> GetDstValue, Func<float> GetCurrentValue, Action<float> SetValue, CancellationToken cancellationToken)
    {
        float startTime = Time.time;
        while (true)
        {
            bool aboutToBreak = Time.time + Time.deltaTime - startTime >= duration;
            if (!aboutToBreak)
            {
                float percentage = (Time.time - startTime) / duration;
                float currentValue = GetCurrentValue();
                Debug.Log("pre set");
                SetValue(currentValue + (GetDstValue() - currentValue) * percentage);
                Debug.Log("post set");
            }
            else
            {
                SetValue(GetDstValue());
                break;
            }
            if (cancellationToken.IsCancellationRequested)
            {
                break;
            }
            await Task.Yield();
        }
    }

For example, if I want to raise the value of a variable from 0 to 10 in 5 seconds, I'd call the function above like this:

float v = 0;
var tokenSource = new CancellationTokenSource();
SetValueInTime(5, () => 10, () => v, (x) => { v = x; }, tokenSource.Token);

It usually works fine, until I used it to set the material property block attributes of some renderer components. Something weird happened. The following code should change the intensity of a color property from the renderers of the GameObjects in a list from 0 to 10 in 3 seconds:

public Color startColor = Color.white;
public GameObject[] glowGO;
for(int i = 0; i< 1; i++)
        {
            SetValueInTime(3, () => 10,
                () =>
                {
                    MaterialPropertyBlock block = new MaterialPropertyBlock();
                    glowGO[i].GetComponent<Renderer>().GetPropertyBlock(block);
                    float intensity = Mathf.Log(block.GetVector("_Tint").x / startColor.r, 2);
                    return intensity;
                }, (x) =>
                {
                    MaterialPropertyBlock block = new MaterialPropertyBlock();
                    var tempColor = startColor * Mathf.Pow(2, x);
                    block.SetVector("_Tint", new Vector4(tempColor.r, tempColor.g, tempColor.b, 1));
                    glowGO[i].GetComponent<Renderer>().SetPropertyBlock(block);

                }, glowToken.Token);
        }

But strangely, it doesn't work as I expected. Even when the GameObject list contains only 1 GameObject, that the for loop runs only once, the same behavior appears. Everything is fine the first loop, but the second time, it stuck at SetValue(). By stuck I am referring to what I've learned after some debugging, that it's neither breaking, nor continuing, but instead just stops doing anything after SetValue() of the second loop.

What's even more strange is that if I manually set the index of the target GameObject to 0 and move it out of the for loop, it just works now. Nothing's changed except it's now no longer in a for loop. I might be missing something but I belive there shouldn't be any difference whether it's called once inside a for loop or without a for loop.

glowToken = new CancellationTokenSource();
        SetValueInTime(glowTime, () => 10,
                () =>
                {
                    MaterialPropertyBlock block = new MaterialPropertyBlock();
                    glowGO[0].GetComponent<Renderer>().GetPropertyBlock(block);
                    float intensity = Mathf.Log(block.GetVector("_Tint").x / startColor.r, 2);
                    return intensity;
                }, (x) =>
                {
                    MaterialPropertyBlock block = new MaterialPropertyBlock();
                    var tempColor = startColor * Mathf.Pow(2, x);
                    Debug.Log(tempColor);
                    block.SetVector("_Tint", new Vector4(tempColor.r, tempColor.g, tempColor.b, 1));
                    glowGO[0].GetComponent<Renderer>().SetPropertyBlock(block);

                }, glowToken.Token);

Appreciate everyone trying to help!

braniac ba
  • 11
  • 1
  • I closed this as a duplicate for the following reason: Most of the Unity API can only be used on the Unity main thread! Async tasks or threads always have to make sure to dispatch any action directly affecting components back into the Unity main thread. There is no need/use for going async here at all as I don't see anything "heavy" happening .. all you want is a certain delay => as described in the duplicate rather use Coroutines for this! – derHugo Oct 27 '21 at 06:31

1 Answers1

0

I'm pretty sure this is related to how Unity only likes MonoBehaviours to operate on the main thread. When you create a Task to operate on a Thread, in the first case you're just dealing with a float and works fine. In the second part you're now trying to set some component properties and it sounds it's silently failing deep within Unity's system. If you change the above code into a coroutine that should fix it.

There's a really good write up by google on how best to handle async logic in Unity

In the article it shows how to specifically set the TaskScheduler so that new tasks stay on the main thread. (It does a better job of explaining it). Looks like...

var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
  • Actually firebase `ContiueWithOnMainThread` is just a fancy implementation of a "Main Thread Dispatcher" which basically I the end is nothing more than just a `ConcurrentQueue` where you add your callback actions and then poll and invoke them in the next `Update` call ;) I think your first half of the answer is more appropriate here: Don't use async code for this use case in the first place! ;) – derHugo Oct 27 '21 at 06:34