Using Mathf.MoveTowards in a coroutine and the correct way to use WaitUntill

34 Views Asked by At

Im writing (trying) a coroutine to handle the loading of rooms in a game. Once a player enters a trigger it starts a coroutine to handle loading the different elements of the next room. Question here, im having trouble with a screen transition that uses a shader and a float in the material to transition from transparent to black, but when I call this in a coroutine using Mathf.MoveTowards is only changing a tiny portion of the float at a time, almost as if it's getting interrupted.

public class RoomManager : MonoBehaviour {

    public static RoomManager Instance { get; private set; }

    public Room currentRoom;

    private void Awake() {
        if (Instance != null && Instance != this) {
            Destroy(this);
        } else {
            Instance = this;
        }
    }

    public IEnumerator LoadRoom(Room nextRoom) {

        Debug.Log("Started Coroutine : Room loading is " + nextRoom);

        StartCoroutine(RoomTransitionController.Instance.RoomTransitionHide());

        yield return new WaitUntil(RoomTransitionController.Instance.TransitionHasLoaded);

        Debug.Log("waiting done");

        currentRoom = nextRoom;

        CameraController.Instance.UpdateCameraPos(currentRoom);

        StartCoroutine(RoomTransitionController.Instance.RoomTransitionShow());

        yield return null;
    }
}

public class RoomTransitionController : MonoBehaviour {

    public static RoomTransitionController Instance { get; private set; }

    private Image image;

    public float transitionSpeed = 1.25f;

    private bool loadedTransition;

    private void Awake() {
        if(Instance != null && Instance != this) {
            Destroy(this);
        }
        else {
            Instance = this;
        }
    }

    private void Start() {
        image = GetComponent<Image>();

        if(image.material.GetFloat("_Cutoff") != 1f ) {
            image.material.SetFloat("_Cutoff", 1f);
        }
        loadedTransition = false;
    }

    public bool TransitionHasLoaded() {
        return image.material.GetFloat("_Cutoff") == -0.1f - image.material.GetFloat("_EdgeSmoothing");
    }

    public IEnumerator RoomTransitionHide() {
        loadedTransition = false;

        while(!loadedTransition) {

            image.material.SetFloat("_Cutoff", Mathf.MoveTowards(image.material.GetFloat("_Cutoff"), -0.1f - image.material.GetFloat("_EdgeSmoothing"), transitionSpeed * Time.deltaTime));

            yield return new WaitUntil( TransitionHasLoaded );
            loadedTransition = true;
        }
        yield return null;
    }

    public IEnumerator RoomTransitionShow() {

        while( loadedTransition ) {
            image.material.SetFloat("_Cutoff", Mathf.MoveTowards(image.material.GetFloat("_Cutoff"), 1f, transitionSpeed * Time.deltaTime));

            loadedTransition = false;
        }
        yield return null;
    }

I know the shader is working correctly because it can get it working using the update method, but i would prefer use a coroutine and understand it better.

1

There are 1 best solutions below

2
derHugo On BEST ANSWER

well you call the line

 image.material.SetFloat("_Cutoff", Mathf.MoveTowards(image.material.GetFloat("_Cutoff"), -0.1f - image.material.GetFloat("_EdgeSmoothing"), transitionSpeed * Time.deltaTime));

exactly once and then you wait until TransitionHasLoaded returns true - or in the second routine you just set loadedTransition = false; which means your loop will iterate maximum once as well.


To reduce complexity right away: You can directly yield return another IEnumerator from your main one!

So there isn't really a need to expose TransitionHasLoaded at all as you will already know when the inner IEnumerator is finished:

public IEnumerator RoomTransitionHide() 
{
    // get your initial values
    var initialCutoff = image.material.GetFloat("_Cutoff");
    // if this never changes anyway you might even cache it only once in a class field
    var target = image.material.GetFloat("_EdgeSmoothing");

    var current = initialCutoff;

    // keep iterating until the values match
    // note that you do not want to continuously access shader properties - it's quite expensive
    while(!Mathf.Approximately(current, target)) 
    {
        // move ONE step and apply
        current = Mathf.MoveTowards(current, target, transitionSpeed * Time.deltaTime);
        image.material.SetFloat("_Cutoff", current);

        // then render this frame and continue from here in the next frame
        yield return null;
    }

    // to make sure to end with a clean value
    image.material.SetFloat("_Cutoff", target);
}

public IEnumerator RoomTransitionShow()
{
    var initialCutoff = image.material.GetFloat("_Cutoff");
    var target = 1f;

    // personally I like more deterministic loops so while the above should behave the same
    // I usually prefer the following approach just to give you an alternative way

    // compute the duration based on the given speed
    var duration = (target - initialCutoff) / transitionSpeed;

    // iterate for duration seconds
    for(var timePassed = 0f; timePassed < duration; timePassed += Time.deltaTime)
    {
        // linear factor moving from 0 to 1 within duration seconds
        var factor = timePassed / duration;

        // linear interpolation between initial and target
        image.material.SetFloat("_Cutoff", Mathf.Lerp(initialCutoff, target));

        yield return null;
    }

    image.material.SetFloat("_Cutoff", target);
}

To take this one step further: I usually would prefer to use a given duration instead of a "magic" transitionSpeed. Your value of 1.25 is just way harder to understand then just setting 0.8 seconds. So I would actually rather do

// rather use and adjust the duration - easier to maintain than speed (in your use case)
[SerializeField] private float fadeDuration = 0.8f;
[SerializeField] private Image image;

// backing field for currentCutoff
private float _currentCutoff = 1f;

// property for easy syncing of _currentCutoff and the actual applied value
private float currentCutoff
{
    get => _currentCutoff;
    set
    {
        _currentCutoff = value;
        image.material.SetFloat("_Cutoff", value);
    }
}

private float _edgeSmoothing;

// initialize yourself completely in Awake
// there is no use/need in delaying this into Start
private void Awake() 
{
    if(Instance && Instance != this) 
    {
        Destroy(this);
    }
    else 
    {
        Instance = this;
    }

    if(!image)
    {
        image = GetComponent<Image>();
    }

    // cache once as assuming this will never change during runtime
    _edgeSmoothing = image.material.GetFloat("_EdgeSmoothing");

    // apply initial value
    currentCutoff = 1f;
}

// you can actually combine both routines and just have a switch for the target value
public IEnumerator RoomTransition(bool show) 
{
    // if show is true go towards 1 otherwise towards the cached _EdgeSmoothing
    var target = show ? 1f : _edgeSmoothing;

    // as before iterate for the duration
    for(var timePassed = 0f; timePassed < duration; timePassed += Time.deltaTime)
    {
        // linear factor 0 -> 1
        var factor = timePassed / duration;
        // this combines both, storing the value for cheap access into _currentCutoff backing field
        // and also applies the value to the image
        currentCutoff = Mathf.Lerp(currentCutoff, target, factor);

        yield return null;
    }

    // again for clean final value apply hard
    currentCutoff = target;
}

and then - regardless which of the above ways you choose - you can actually just yield and await these from within

 public IEnumerator LoadRoom(Room nextRoom) 
 {

    Debug.Log("Started Coroutine : Room loading is " + nextRoom);

    // This already executes the routine and wait until it  is finished
    yield return RoomTransitionController.Instance.RoomTransitionHide();
    // or accordingly
    //yield return RoomTransitionController.Instance.RoomTransition(false);

    Debug.Log("waiting done");

    currentRoom = nextRoom;

    CameraController.Instance.UpdateCameraPos(currentRoom);

    // and here accordingly
    yield return RoomTransitionController.Instance.RoomTransitionShow();
    // or accordingly
    //yield return RoomTransitionController.Instance.RoomTransition(true);
}