Home > Software engineering >  Unity: Switching from one FixedUpdate-like coroutine to another one
Unity: Switching from one FixedUpdate-like coroutine to another one

Time:09-19

I have a coroutine DefaultFixedUpdate() that functions as a replacement for FixedUpdate() and another coroutine called WallSlidingFixedUpdate(). I have two variables that store which of the both is active. activeCoroutine and prevActiveCoroutine with the starting strings nameof(DefaultFixedUpdate) and "no coroutine". In FixedUpdate() from Unity I have this code that should change coroutines if activeCoroutine and prevActiveCoroutine are not the same:

void FixedUpdate()
    {
        if (activeCoroutine != prevActiveCoroutine)
        {
            print($"changing coroutines from {prevActiveCoroutine} to {activeCoroutine}");
            StopCoroutine(prevActiveCoroutine);
            StartCoroutine(activeCoroutine);
            prevActiveCoroutine = activeCoroutine;
            print("changed coroutines");
        }
    }

When starting the game DefaultFixedUpdate() starts as expected and prevActiveCoroutine changes as expected to activeCoroutine and when a condition inside DefaultFixedUpdate() is met to switch to WallSlidingFixedUpdate() activeCoroutine changes to the new string as expected. But when StartCoroutine(activeCoroutine) gets called the code inside WallSlidingFixedUpdate() doesn't run for some reason. StopCoroutine(prevActiveCoroutine) does work. The end result is that none of the two corutines runs but expected was that WallSlidingCoroutine() runs.

These are the two Coroutines. The changing of activeCoroutine happens almost at the bottom of both functions.

    IEnumerator DefaultFixedUpdate()
    {
        while (true)
        {
            //setup
            gravity = (velocity.y < 0 || !Input.GetKey(KeyCode.Space)) ? dropGravity : normalGravity;
            Vector2 pos = transform.position   col.size.y * 0.5f * Vector3.up;
            Vector2 timeStepVelocity = velocity * Time.fixedDeltaTime;
            Vector2 pos1 = pos;
            velocity.y -= gravity * Time.fixedDeltaTime;

            //checking collision
            RaycastHit2D[] colChecks = Physics2D.BoxCastAll(pos, col.size, 0, timeStepVelocity, timeStepVelocity.magnitude, 0b1000000);

            //remove unwanted collisions that are behind and don't make sense to collide with
            List<RaycastHit2D> newColChecks = new();
            foreach (RaycastHit2D colCheck in colChecks)
                if (Vector2.Dot(colCheck.normal, timeStepVelocity) < 0)
                    newColChecks.Add(colCheck);
            colChecks = newColChecks.ToArray();

            //remove unwanted velocities
            foreach (RaycastHit2D colCheck in colChecks)
            {
                if (Mathf.Abs(colCheck.normal.x / colCheck.normal.y) > 1) // left and right
                {
                    velocity.x = Mathf.Sign(colCheck.normal.x) > 0 ? Mathf.Max(velocity.x, 0) : Mathf.Min(velocity.x, 0);
                }
                else // top bottom
                {
                    velocity.y = Mathf.Sign(colCheck.normal.y) > 0 ? Mathf.Max(velocity.y, 0) : Mathf.Min(velocity.y, 0);
                }
            }

            //calculate correct destination
            if (colChecks.Length == 0)
                pos  = timeStepVelocity;
            else
            {
                //create bounding box for all possible future positions
                Vector4 posBox = new Vector4(pos.x, pos.x   timeStepVelocity.x, pos.y, pos.y   timeStepVelocity.y);

                //move to surface
                pos = colChecks[0].centroid;

                //slide along surface until hitting another tangent or the bounding box
                Vector2 tangent = new Vector3(colChecks[0].normal.y, -colChecks[0].normal.x);
                Vector2 centroid = pos;

                //slide until hitting distance bounds
                if (Mathf.Abs(tangent.x) > Mathf.Abs(tangent.y)) //top and bottom
                {
                    pos  = tangent / tangent.x * (timeStepVelocity.x - pos.x   pos1.x);
                    tangent = Mathf.Sign(centroid.y - pos1.y) * tangent;
                }
                else //left and right
                {
                    pos  = tangent / tangent.y * (timeStepVelocity.y - pos.y   pos1.y);
                    tangent = Mathf.Sign(centroid.x - pos1.x) * tangent;
                }

                //save previous surface that is being slid on
                Vector2 prevTangent = Tangent(colChecks[0].normal);

                //check if I collided with another tangent along the way
                colChecks = Physics2D.BoxCastAll(centroid, col.size, 0, pos, Vector2.Distance(pos, centroid), 0b1000000);

                //remove unwanted collisions that are behind and don't make sense to collide with
                newColChecks = new();
                foreach (RaycastHit2D colCheck in colChecks)
                    if (Vector2.Dot(colCheck.normal, tangent * Mathf.Sign(Vector2.Dot(pos - centroid, tangent))) < 0 || Vector2.Dot(colCheck.normal, timeStepVelocity) < 0)
                        newColChecks.Add(colCheck);
                colChecks = newColChecks.ToArray();

                if (colChecks.Length > 1)
                {
                    pos = centroid   (pos - centroid).normalized * colChecks[1].distance;
                }

                //repeat
                int loopBreak = 0;
                string loopBreakMessage = "";
                while (true)
                {
                    loopBreakMessage  = centroid   "\n";
                    if (loopBreak > 4)
                    {
                        Debug.LogError("loop limit exceeded\n"   loopBreakMessage);
                        break;
                    }
                    loopBreak  ;

                    //check if I collided with another tangent along the way
                    colChecks = Physics2D.BoxCastAll(centroid, col.size, 0, pos - centroid, Vector2.Distance(pos, centroid), 0b1000000);

                    //remove unwanted collisions that are behind and don't make sense to collide with
                    newColChecks = new();
                    foreach (RaycastHit2D colCheck in colChecks)
                        if (Tangent(colCheck.normal) != prevTangent)
                            newColChecks.Add(colCheck);
                    colChecks = newColChecks.ToArray();

                    if (colChecks.Length > 2)
                    {
                        prevTangent = colChecks[0].normal;

                        if (!VectorOnSameQuarter(colChecks[1].normal, colChecks[2].normal)) // stuck in a corner
                        {
                            pos = centroid   (pos - centroid).normalized * colChecks[2].distance;
                            break;
                        }
                        else //continue along new tangent
                        {
                            //corner pos
                            centroid  = (pos - centroid).normalized * colChecks[2].distance;
                            //end pos if there's no surface along the way
                            pos = PosAlongVectorInsideRectangle(posBox, centroid, Tangent(colChecks[2].normal));
                        }
                    }
                    else
                    {
                        break;
                    }
                }
                AlignVelocityWithGround();
            }

            //check if player is grounded and save ground tangent to remove small hops when going down slopes
            PlayerState.OnGround = false;
            RaycastHit2D[] jumpChecks = Physics2D.BoxCastAll(pos, col.size, 0, Vector2.down, 1e-3f, 0b1000000);
            if (jumpChecks.Length != 0)
            {
                foreach (RaycastHit2D jumpCheck in jumpChecks)
                {
                    if (jumpCheck.normal.y > Mathf.Abs(jumpCheck.normal.x))
                    {
                        PlayerState.OnGround = true;
                        groundTangent = new Vector2(jumpCheck.normal.y, -jumpCheck.normal.x);
                    }
                }
                PlayerState.state = PlayerState.state == PlayerState.State.WallSliding ? PlayerState.State.Default : PlayerState.state;
                //check for a wall slide and save wall tangent for smooth sliding
                if (!PlayerState.OnGround && velocity.y < 0 && Mathf.Abs(jumpChecks[0].normal.x) > Mathf.Abs(jumpChecks[0].normal.y) && Mathf.Asin(jumpChecks[0].normal.y) * Mathf.Rad2Deg < 15)
                {
                    groundTangent = new Vector2(jumpChecks[0].normal.y, -jumpChecks[0].normal.x);
                    transform.position = pos - col.size.y * 0.5f * Vector2.up;
                    activeCoroutine = nameof(WallSlidingFixedUpdate);
                    yield break;
                }
            }

            transform.position = pos - col.size.y * 0.5f * Vector2.up;
            yield return new WaitForFixedUpdate();
        }
    }
    IEnumerable WallSlidingFixedUpdate()
    {
        print("wallsliding coroutine entered");
        while (true)
        {
            print("wallsliding coroutine happening");

            Vector2 pos = transform.position;

            pos -= Time.fixedDeltaTime * wallSlideSpeed * groundTangent / groundTangent.y;

            transform.position = pos;

            if (InputManager.Left && groundTangent.y > 0)
            {
                activeCoroutine = nameof(DefaultFixedUpdate);
                yield break;
            }
            else if (InputManager.Right)
            {
                activeCoroutine = nameof(DefaultFixedUpdate);
                yield break;
            }
            yield return new WaitForFixedUpdate();
        }
    }

CodePudding user response:

Holy moly, how did I miss this. The return value of WallSlidingFixedUpdate is IEnumerable and not IEnumerator. I changed it to IEnumertor and now it works.

CodePudding user response:

You figured your issue about the return type out already -> It needs to be IEnumerator.

Anyway my question is WHY?

Instead of going through all that trouble for switching between different Coroutine instances via a string I would rather have only a single outer scope routine running and then rather switch between which inner block to execute

private enum RoutineType
{
    Default,
    WallSliding
}

private RouineType activeRoutine;

private IEnumerator FixedUptadeRoutine()
{
    while(true)
    {
        switch(activeRoutine)
        {
            case RoutineType.WallSliding:
                yield return DefaultRoutine();
                break;

            case default:
                yield return WallSlidingRoutine();
                break;
        }
    }
}

private IEnumerator DefaultRoutine()
{
    while(true)
    {
        // NOTE: Btw you want to wait for fixed update FIRST
        // Before applying all the values

        ...

        // And now this will simply make sure you change the routine type
        activeRoutine = RoutineType.WallSliding;
        // and exit this one so the outer routine will handle the switch in the next frame
        yield break;

        ...
    }
}

private IEnumerator WallSlidingRoutine()
{
    ...
}

you can simply yield return any IEnumerator so it will be executed and at the same time the outer routine waits for it to finish. There is no need to have multiple start and stop coroutines.

  • Related