Search Unity

Resolved Upcoming function to be put after or in StartCoroutine?

Discussion in 'Scripting' started by anrorolove, Oct 21, 2020.

  1. anrorolove

    anrorolove

    Joined:
    Sep 25, 2020
    Posts:
    18
    Hi, I'm trying to implement a countdown timer in my script for a character to wait then proceed move but I'm having a frustrating issue with the placement of my functions.

    When I do this, my character does wait until the WaitForSecond(timeToWait) is up but as soon it arrives at the next waypoint it starts spinning rapidly out of control.
    Code (CSharp):
    1. IEnumerator WaitAtPoint()
    2.         {
    3.             yield return new WaitForSeconds(timeToWait);
    4.             Move();
    5.         }
    6.  
    If I put it like this, then I'm aware that StartCoroutine starts kind of simultaneously with "Move" so my character doesn't wait and just keeps walking.
    Code (CSharp):
    1. StartCoroutine(WaitAtPoint());
    2. Move;
    I have tried to not use coroutine and use this instead, but my character just stops at the waypoint and don't move further at all.
    Code (CSharp):
    1. case ActionType.Wait:
    2.                 Vector3 dir = moveTarget.position - transform.position;
    3.                 changeAnim(dir);
    4.                 anim.SetBool("isWalking", false);
    5.                 timeToWait -= Time.deltaTime;
    6.                 if (timeToWait < 0)
    7.                     Move();
    8.                 break;
    here is the script in detail:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class Pathway : MonoBehaviour
    6. {
    7.     public Animator anim;
    8.     public Rigidbody2D myRigidbody;
    9.     public List<AIAction> Actions;
    10.  
    11.     int actionIndex = 0;
    12.     bool currentActionCompleted = false;
    13.  
    14.     void Start()
    15.     {
    16.         myRigidbody = GetComponent<Rigidbody2D>();
    17.         anim = GetComponent<Animator>();
    18.     }
    19.  
    20.     void Update()
    21.     {
    22.         MovingAction();
    23.     }
    24.  
    25.     void MovingAction()
    26.     {
    27.         if (currentActionCompleted)
    28.         {
    29.             actionIndex = actionIndex + 1;
    30.             if (actionIndex >= Actions.Count)
    31.             {
    32.                 actionIndex = 0;
    33.             }
    34.             Debug.Log($"Action completed. Moving to action {actionIndex}.");
    35.  
    36.             currentActionCompleted = false;
    37.         }
    38.  
    39.         AIAction currentAction = Actions[actionIndex];
    40.         float timeToWait = currentAction.WaitTime;
    41.         float moveSpeed = currentAction.MoveSpeed;
    42.         Transform moveTarget = currentAction.MoveTarget;
    43.  
    44.         switch (currentAction.ActionType)
    45.         {
    46.             case ActionType.Wait:
    47.                 Vector3 dir = moveTarget.position - transform.position;
    48.                 changeAnim(dir);
    49.                 anim.SetBool("isWalking", false);
    50.                 StartCoroutine(WaitAtPoint());
    51.                 break;
    52.  
    53.             case ActionType.Move:
    54.                 Move();
    55.                 break;
    56.         }
    57.  
    58.         IEnumerator WaitAtPoint()
    59.         {
    60.             yield return new WaitForSeconds(timeToWait);
    61.             Move(); //Make character spin out of control when it reaches moveTarget waypoint
    62.         }
    63.  
    64.         void Move()
    65.         {
    66.             anim.SetBool("isWalking", true);
    67.             Vector3 temp = Vector3.MoveTowards(transform.position, moveTarget.transform.position, moveSpeed * Time.deltaTime);
    68.             changeAnim(temp - transform.position);
    69.             myRigidbody.MovePosition(temp);
    70.          
    71.             if (temp == moveTarget.transform.position)
    72.             {
    73.                 currentActionCompleted = true;
    74.             }
    75.  
    76.         }
    77.     }
    78.  
    79.     public void SetAnimFloat(Vector3 setVector)...
    80.  
    81.     public void changeAnim(Vector3 direction)...
    82.  
    83. }
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,910
    You have to be careful with coroutines. I think the "spinning out of control" is happening because you are starting a new coroutine every single frame in your Update () method. Make sure you use some kind of flag to stop the coroutine from being started multiple times.

    Also you have a lot of local methods inside your MoveAction() method, which is highly unusual but not necessarily wrong.
     
  3. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,910
    Here this should sort you out:
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. public class Pathway : MonoBehaviour {
    6.     public Animator anim;
    7.     public Rigidbody2D myRigidbody;
    8.     public List<AIAction> Actions;
    9.  
    10.     int actionIndex = 0;
    11.     bool currentActionCompleted = false;
    12.     bool currentActionStarted = false;
    13.  
    14.     void Start() {
    15.         myRigidbody = GetComponent<Rigidbody2D>();
    16.         anim = GetComponent<Animator>();
    17.     }
    18.  
    19.     void Update() {
    20.         if (currentActionCompleted) {
    21.             currentActionStarted = false;
    22.             actionIndex = actionIndex + 1;
    23.             if (actionIndex >= Actions.Count) {
    24.                 actionIndex = 0;
    25.             }
    26.  
    27.             Debug.Log($"Action completed. Moving to action {actionIndex}.");
    28.             currentActionCompleted = false;
    29.         }
    30.  
    31.         AIAction currentAction = Actions[actionIndex];
    32.  
    33.         if (!currentActionStarted) {
    34.             switch (currentAction.ActionType) {
    35.                 case ActionType.Wait:
    36.                     StartCoroutine(Wait(currentAction));
    37.                     // perform waiting. Set currentActionCompleted = true when done
    38.                     break;
    39.  
    40.                 case ActionType.Move:
    41.                     StartCoroutine(Move(currentAction));
    42.                     // perform movement. Set currentActionCompleted = true when finished.
    43.                     break;
    44.             }
    45.         }
    46.     }
    47.  
    48.     IEnumerator Wait(AIAction action) {
    49.         currentActionStarted = true;
    50.         yield return new WaitForSeconds(action.WaitTime);
    51.         currentActionCompleted = true;
    52.     }
    53.  
    54.     IEnumerator Move(AIAction action) {
    55.         currentActionStarted = true;
    56.         Transform moveTarget = action.MoveTarget;
    57.         float moveSpeed = action.MoveSpeed;
    58.         while (Vector3.Distance(myRigidbody.position, moveTarget.position) > 0.001) {
    59.             yield return new WaitForFixedUpdate();
    60.             myRigidbody.MovePosition(Vector2.MoveTowards(myRigidbody.position, moveTarget.position, moveSpeed * Time.fixedDeltaTime));
    61.         }
    62.  
    63.         currentActionCompleted = true;
    64.     }
    65. }
     
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,003
    If you need sequential execution of actions with possible wait times in between, just use a coroutine. A single coroutine which you start from Start. Just get rid of your Update method.

    Code (CSharp):
    1.  
    2. public class Pathway : MonoBehaviour
    3. {
    4.     public Animator anim;
    5.     public Rigidbody2D myRigidbody;
    6.     public List<AIAction> Actions;
    7.  
    8.     void Start()
    9.     {
    10.         myRigidbody = GetComponent<Rigidbody2D>();
    11.         anim = GetComponent<Animator>();
    12.         StartCoroutine(Move());
    13.     }
    14.  
    15.     IEnumerator Move()
    16.     {
    17.         while (true)
    18.         {
    19.             foreach (var action in Actions)
    20.             {
    21.                 switch (action.ActionType)
    22.                 {
    23.                     case ActionType.Wait:
    24.                         yield return new WaitForSeconds(action.WaitTime);
    25.                         break;
    26.  
    27.                     case ActionType.Move:
    28.                         Transform moveTarget = action.MoveTarget;
    29.                         float moveSpeed = action.MoveSpeed;
    30.                         anim.SetBool("isWalking", true);
    31.                         Vector3 newPos = moveTarget.position;
    32.                         while (newPos != moveTarget.transform.position) {
    33.                             newPos = Vector3.MoveTowards(transform.position, moveTarget.position, moveSpeed * Time.deltaTime);
    34.                             changeAnim(newPos - transform.position);
    35.                             myRigidbody.MovePosition(newPos);
    36.                             yield return null;
    37.                         }
    38.                         break;
    39.                 }
    40.             }
    41.             yield return null;
    42.         }
    43.     }
    44. }
    About 6 years ago I've posted this example enemy wave spawner which works similar. It's just the basic skeleton and more a proof of concept.

    ps.

    Note when using coroutines you should avoid starting and stopping coroutines all the time. Starting a coroutine has quite a bit of overhead and always allocates memory. If you can keep a coroutine running that's in most cases beneficial. Also when a coroutine is waiting on a WaitForSeconds you have no overhead at all.
     
    Last edited: Oct 21, 2020
    PraetorBlue likes this.
  5. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,910
    Ah yeah Bunny's solution is even simpler. Less boolean flag nonsense to deal with.
     
  6. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,003
    That's actually the big advantage of coroutines that they give you statemachine behaviour out of the box. If you want to know more about how coroutines work in Unity, I've written a coroutine crash course on UA ^^.
     
    anrorolove and PraetorBlue like this.
  7. anrorolove

    anrorolove

    Joined:
    Sep 25, 2020
    Posts:
    18
    Thank you for giving me advice and for pointing me to the right direction! I'll definitely read your UA. I would like my NPC to loop through my waypoint system. I implemented your script but I'm not sure why my NPC is not walking to the waypoint and is stuck to its original position and just walking in place.
     

    Attached Files:

  8. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,003
    Well I just adapted your original code. Currently the movement code checks if you reached the target position exactly. This usually works when using MoveTowards. However since the actual position is set through the rigidbody it's of course possible that you either can't reach the target position exactly (because of a collision) or your position is otherwise not exact (maybe due to internal physics inaccuracies). So you may want to use a condition like what @PraetorBlue posted:

    Code (CSharp):
    1. while (Vector3.Distance(myRigidbody.position, moveTarget.position) > 0.001f) {
    here you can adjust the threshold (0.001f) to be more tolerant.

    Though a common issue with objects that do not move at all might be that your Animation actually modifies the root object positions. So no matter what you do with your rigidbody the animation might simply set the position back to a fix position. All this highly depends on how your object is setup, where the animation component is attached to and if the animation has root object curves or not. As always there's not one right way to accomplish your goals.
     
    anrorolove likes this.