Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Resolved How to stop a NPC from walking at some specific waypoints along the path?

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

  1. anrorolove

    anrorolove

    Joined:
    Sep 25, 2020
    Posts:
    18
    I would like to make my NPC idle/stop walking at some chosen waypoints for a few seconds before resuming its path but I have a hard time figuring out how to code it to be able to customize it easily in the Inspector. This is my current Waypoint System script that I attached to my NPC gameobject.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class Path : MonoBehaviour
    6. {
    7.     public Animator anim;
    8.     public Rigidbody2D myRigidbody;
    9.  
    10.     [SerializeField]
    11.     float moveSpeed = 2f;
    12.  
    13.     [SerializeField]
    14.     Transform[] waypoints = default;
    15.  
    16.     int waypointIndex = 0;
    17.  
    18.     void Start()
    19.     {
    20.         myRigidbody = GetComponent<Rigidbody2D>();
    21.         anim = GetComponent<Animator>();
    22.         Vector3 temp = waypoints[waypointIndex].transform.position;
    23.     }
    24.  
    25.     void Update()
    26.     {
    27.         Move();
    28.     }
    29.  
    30.    
    31.     public void SetAnimFloat(Vector2 setVector)
    32.     {
    33.         anim.SetFloat("input_x", setVector.x);
    34.         anim.SetFloat("input_y", setVector.y);
    35.     }
    36.  
    37.     public void changeAnim(Vector2 direction)
    38.     {
    39.         if (Mathf.Abs(direction.x) > Mathf.Abs(direction.y))
    40.         {
    41.             if (direction.x > 0)
    42.             {
    43.                 SetAnimFloat(Vector2.right);
    44.             }
    45.             else if (direction.x < 0)
    46.             {
    47.                 SetAnimFloat(Vector2.left);
    48.             }
    49.         }
    50.         else if (Mathf.Abs(direction.x) < Mathf.Abs(direction.y))
    51.         {
    52.             if (direction.y > 0)
    53.             {
    54.                 SetAnimFloat(Vector2.up);
    55.             }
    56.             else if (direction.y < 0)
    57.             {
    58.                 SetAnimFloat(Vector2.down);
    59.             }
    60.         }
    61.     }
    62.    
    63.  
    64.     public void Move()
    65.     {
    66.         Vector3 temp = Vector3.MoveTowards(transform.position, waypoints[waypointIndex].transform.position, moveSpeed * Time.deltaTime);
    67.         changeAnim(temp - transform.position);
    68.         myRigidbody.MovePosition(temp);
    69.  
    70.         if (temp == waypoints[waypointIndex].transform.position)
    71.         {
    72.             waypointIndex += 1;
    73.         }
    74.  
    75.         if (waypointIndex == waypoints.Length)
    76.         {
    77.             waypointIndex = 0;
    78.         }
    79.  
    80.     }
    81.  
    82. }
     
  2. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    664
    Perhaps make Move a coroutine and insert a
    yield return WaitForSeconds
    between lines 72 and 73.
     
  3. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,722
    In addition to what @stevensrmiller said above, you should add a layer of indirection to your waypoints list. So instead of just have a list of transforms, you could have a list of
    PatrolAction
    s. A
    PatrolAction
    could be either "walk to a waypoint", or "stand and wait for X seconds". Then your NPC can iterate over the
    PatrolAction
    s to decide what to do at any given time. You could even extend this later. Maybe you could add a "dialog" action or a "look in direction" action later on, etc..
     
  4. Meishin

    Meishin

    Joined:
    Apr 12, 2019
    Posts:
    26
    @PraetorBlue's answer is definitely the way to go for future improvements. Also you need to branch out from Move in the update if your using coroutines (see ex below).

    If you just want a quick and dirty solution you can just add a list of "WaitAtPathIndexes" and whenever the character reaches a waypoint (in Move :
    Code (CSharp):
    1. if (temp == waypoints[waypointIndex].transform.position)
    ) you check whether the index is in your list and if so branch out from Move for the desired time.

    Using a private var isWaiting for ex ;
    Code (CSharp):
    1.  
    2.  
    3. private bool isWaiting = true;                      // To branch out of Move Loop in Update
    4. [Serialized] private float wayPointWaitTime = 2f;   //  Dirtiest way of defining it, but quick
    5. [Serialized] private List<int> WaitAtPathIndexes = new List<int>();  // Same
    6.  
    7. void Update()
    8. {
    9.    If(isWaiting)
    10.       DoWhatYouWantToDo
    11.    Else
    12.       Move()
    13. }
    14.  
    15. public void Move()
    16.     {
    17.         Vector3 temp = Vector3.MoveTowards(transform.position, waypoints[waypointIndex].transform.position, moveSpeed * Time.deltaTime);
    18.         changeAnim(temp - transform.position);
    19.         myRigidbody.MovePosition(temp);
    20.  
    21.         if (temp == waypoints[waypointIndex].transform.position)
    22.         {
    23.             if(WaitAtPathIndexes.Contains(waypointIndex))            // Check here if should wait
    24.                 StartCoroutine(WaitForSec(wayPointWaitTime ))
    25.             waypointIndex += 1;                                    // Note that this code will be triggered right away
    26.         }
    27.  
    28.         if (waypointIndex == waypoints.Length)
    29.             waypointIndex = 0;
    30. }
    31.  
    32. IEnumerator WaitForSec(float time)
    33. {
    34.     isWaiting = True;                    // Will disable Move calls in next Updates
    35.     yield return WaitForSeconds(time);    // Waits for time seconds
    36.     isWaiting = False;                     // Re-Enable Move calls in next Update
    37. }
    38.  
     
    Last edited: Oct 9, 2020
  5. anrorolove

    anrorolove

    Joined:
    Sep 25, 2020
    Posts:
    18
    Thank you for your quick fix suggestion! It's definitely an easier way to integrate into my script. but if I use
     private float wayPointWaitTime =2f 
    , won't all the waypoints I want my NPC to idle at have the same sec of wait time? I would like to be able to idle let say 3sec at one waypoint and 6sec at another.
     
  6. anrorolove

    anrorolove

    Joined:
    Sep 25, 2020
    Posts:
    18
    I'll definitely want to make the player character interact with the NPC as the next step as you said, so your suggestion certainly sounds ideal! I understand your solution but I'm a beginner self-learner so I'm having a hard time arranging and putting the idea into organized codes in unity. Please, if you could add the coding to my script or fix anything to make it more complete, it would be extremely helpful. I wish to have one neat script that I can attach to multiple different NPCs. I'm so sorry if I'm asking for too much. I have flipped the internet to look for tutorials about NPC but it is mostly only enemy NPC which is not what I intend to have. I have attached all the information pertinent to my NPC down below. My game is a 2D top-down RPG game.

    My NPC has an animator controller (walking and idle motion animations).
    First, I would like my NPC to walk along a looping made-path (via waypoints) and stop and "idle" at some chosen waypoints. Let say "waypoint (4)" for 3sec and "waypoint (8)" for 6 secs.

    Second, if the player character is in the NPC collider vicinity, it will stop the NPC movement (probably transition from the walking to the idle animation) and make him look toward the player direction, and by pressing the space key bar it can engage in a dialog. It sounds simple enough but I feel so stupid for not be able to code it.
     

    Attached Files:

  7. Terraya

    Terraya

    Joined:
    Mar 8, 2018
    Posts:
    646
    Hey there!

    First i would recommend that what @PraetorBlue said ,
    also apart of that, i also would recommend to track your actions since its
    very usefull and most of the time also necessary to know what you do,

    if you store your actions as "actions" then you can always stop/start or pause them ,

    to your actual problem: PraetorBlue said it already,
    Create different "Actions" instead of using transforms,
    with that you can work flexible
     
  8. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,722
    What I described is not a huge amount of work, but it's also not a trivial thing. I can provide a bit of a framework here.

    Since you are a beginner, I suggest something simple. A more seasoned programmer would probably use inheritance and/or interfaces to accomplish this, but those are a bit more advanced topics and they are also pretty tricky to get working with Unity's inspector, and I think part of what you want is to be able to design the behavior of the AI in the inspector.

    So what I'd start with is something that is probably not ideal from a "clean code" standard, but it is easy to understand and pretty simple to code. First, create an enum describing the different types of actions you want your AI to be able to do. For now it's just moving and waiting. Put this in its own file called ActionType.cs:

    Code (CSharp):
    1. public enum ActionType {
    2.   Move,
    3.   Wait
    4. }
    Then we can create an AIAction class. This describes a single action you want your AI to be able to perform. Create this as AIAction.cs:

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. // This lets Unity draw this object in the inspector
    5. [Serializable]
    6. public class AIAction {
    7.   public ActionType ActionType;
    8.   public Transform MoveTarget;
    9.   public float MoveSpeed;
    10.   public float WaitTime;
    11. }
    Ok now we can assign a list of actions to your NPC. In a component attached to the NPC, add this field:
    Code (CSharp):
    1. public List<AIAction> Actions;
    Now you should be able to see the "Actions" field on your NPC component in the inspector. You can now add as many actions as you want to this list, and set their properties. Then, in Update for your NPC, you can look at the current action and act on it. Something like this:

    Code (CSharp):
    1. int actionIndex = 0;
    2. bool currentActionCompleted = false;
    3.  
    4. void Update() {
    5.   if (currentActionCompleted) {
    6.     actionIndex = actionIndex + 1;
    7.     if (actionIndex >= Actions.Count) {
    8.        // This will loop around back to the beginning of the list.
    9.        actionIndex = 0;
    10.     }
    11.  
    12.     Debug.Log($"Action completed. Moving to action {actionIndex}.");
    13.     currentActionCompleted = false;
    14.   }
    15.  
    16.   AIAction currentAction = Actions[actionIndex];
    17.  
    18.   switch (currentAction.ActionType) {
    19.     case ActionType.Wait :
    20.       float timeToWait = currentAction.WaitTime;
    21.       // perform waiting. Set currentActionCompleted = true when done
    22.       break;
    23.  
    24.     case ActionType.move :
    25.       Transform moveTarget = currentAction.MoveTarget;
    26.       float moveSpeed = currentAction.MoveSpeed;
    27.       // perform movement. Set currentActionCompleted = true when finished.
    28.       break;
    29.   }
    30. }
    Warning - I wrote this code in the editor. I can't guarantee it works properly or even compiles as I have not tested it. You have to fill in the moving and waiting part :D
     
    Last edited: Oct 9, 2020
    anrorolove and Meishin like this.
  9. anrorolove

    anrorolove

    Joined:
    Sep 25, 2020
    Posts:
    18
    Thank you so much for simplifying it! However, I encountered a few problems. So I made 3 scripts so far. One for ActionType, one for AIAction, and the last one I called Pathway to attach the
    public List<AIAction> Actions;
    .

    First, shouldn't
    public class AIAction {
    derived from MonoBehaviour? Else, I get an 'AIAction' is missing the class attribute 'ExtensionOfNativeClass'! error.

    Second, I'm a bit confused about where this part should go. When I put it in the "Pathway" script, I get this error CS0117: 'ActionType' does not contain a definition for 'move'. I think something went straight over the head...

    int actionIndex = 0;
    bool currentActionCompleted = false; //Etc.

    Here are My 3 scripts:
    ActionType:
    Code (CSharp):
    1. public enum ActionType
    2. {
    3.     Move,
    4.     Wait
    5. }
    AIAction:
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. // This lets Unity draw this object in the inspector
    5. [Serializable]
    6. public class AIAction : MonoBehaviour
    7. {
    8.     public ActionType ActionType;
    9.     public Transform MoveTarget;
    10.     public float MoveSpeed;
    11.     public float WaitTime;
    12. }
    Pathway:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class Pathway : MonoBehaviour
    6. {
    7.     public List<AIAction> Actions;
    8.  
    9.     int actionIndex = 0;
    10.     bool currentActionCompleted = false;
    11.  
    12.     void Update()
    13.     {
    14.         if (currentActionCompleted)
    15.         {
    16.             actionIndex = actionIndex + 1;
    17.             if (actionIndex >= Actions.Count)
    18.             {
    19.                 // This will loop around back to the beginning of the list.
    20.                 actionIndex = 0;
    21.             }
    22.  
    23.             Debug.Log($"Action completed. Moving to action {actionIndex}.");
    24.             currentActionCompleted = false;
    25.         }
    26.  
    27.         AIAction currentAction = Actions[actionIndex];
    28.  
    29.         switch (currentAction.ActionType)
    30.         {
    31.             case ActionType.Wait:
    32.                 float timeToWait = currentAction.WaitTime;
    33.                 // perform waiting. Set currentActionCompleted = true when done
    34.                 break;
    35.  
    36.             case ActionType.move:
    37.                 Transform moveTarget = currentAction.MoveTarget;
    38.                 float moveSpeed = currentAction.MoveSpeed;
    39.                 // perform movement. Set currentActionCompleted = true when finished.
    40.                 break;
    41.         }
    42.     }
    43. }
    44.  
     
    Meishin likes this.
  10. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,722
    nope. That error happens if you previously had the class as a MonoBehaviour and tried to attach it to an object though. If you did that - remove any of the broken attachments from your GameObjects. It does not need to be a MonoBehaviour because we will not be attaching it to any GameObjects directly. It will only be in your list of actions.
    You need to capitalize it properly. Move with a capital M.
     
    anrorolove likes this.
  11. Terraya

    Terraya

    Joined:
    Mar 8, 2018
    Posts:
    646
    In your Animator i would recommend not making a "boolean" which says "isRunning", just call the animation depending on your Velocity, if its > 0 .. you can run otherwise idle , same with running backwards and so on,

    Also, if your character is not moving to the next waypoint, its not becouse of the animator but your script