- Home /
Making object wait when moving between waypoints
Hi,
I'm trying to make an object wait at its current location for a second or two before moving on the its next location. Applying a coroutine didn't achieve the desired effect, maybe I'm just using it wrong. Please advise. Thanks.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyProperties : MonoBehaviour
{
[Header("Movement Properties")]
public Transform[] waypoints;
public float speed;
public int curWaypoint;
public bool patrol = true;
public Vector3 target;
public Vector3 moveDirection;
public Vector3 velocity;
public float entryWait = 3f;
public float positionWait = 1f;
void Awake()
{
StartCoroutine(EntryWait());
}
void Update()
{
if (curWaypoint < waypoints.Length)
{
target = waypoints[curWaypoint].position;
moveDirection = target - transform.position;
velocity = GetComponent<Rigidbody>().velocity;
if (moveDirection.magnitude < 1)
{
curWaypoint = Random.Range(0, 14);
}
else
{
velocity = moveDirection.normalized * speed;
}
}
else
{
if (patrol)
{
curWaypoint = 0;
}
else
{
velocity = Vector3.zero;
}
}
GetComponent<Rigidbody>().velocity = velocity;
}
private IEnumerator EntryWait()
{
curWaypoint = 7;
yield return new WaitForSeconds(entryWait);
}
private IEnumerator PositionWait()
{
yield return new WaitForSeconds(positionWait);
}
}
Answer by ADiSiN · May 21, 2020 at 05:55 PM
Hi!
So, it's important to understand what Coroutines do and what they don't.
You should always remember that they are not creating delay for your Update() function - look at this script:
void Update()
{
StartCoroutine(WaitBeforeDo());
Debug.Log("I am doing something");
}
IEnumerator WaitBeforeDo()
{
Debug.Log("Wait before do");
yield return new WaitForSeconds(5f);
}
Theoretically we want to create delay before debugging "I am doing something", but...oh, look at the output in the console: As we can see we not only continously calling the "I am doing something", but also "Wait before do", because it gets called each frame as well.
Therefore you shouldn't think of coroutine as time freezer for your main executive function, but better as an function with option to switch variables not at the same frame, but with given delay.
Now look at this code:
bool isDoingSomething = true;
float timer = 0f;
void Update()
{
if (isDoingSomething)
{
Debug.Log("I am doing something");
timer += 1 * Time.deltaTime;
if (timer > 2f)
{
isDoingSomething = false;
StartCoroutine(WaitBeforeDo());
}
}
}
IEnumerator WaitBeforeDo()
{
// Action BEFORE delay
Debug.Log("Wait before do");
// Delay itself
yield return new WaitForSeconds(3f);
// Action AFTER delay
isDoingSomething = true;
timer = 0f;
}
And let's take a look at the console: Great, now it's working.
The "I am doing something" gets called multiple times, but when we receive "Wait before do" it freezes for couple of seconds and then starts again, but only because our Coroutine swaps isDoingSomething boolean after delay, not because of the delay itself.
As you can see we now have much more variables in our code itself, so the first thing for Coroutines to be useful is proper design of your executive functions - you should have variables that will be adjusted by the Coroutine before/after given delay or otherwise you call functions with delay, but nothing changes after the delay.
So, the overall look that you would like to achieve could look like this:
bool isMoving = true;
void Update()
{
if (isMoving)
{
// Move code;
if(/* We reach destination or any conditions that you want to achieve before waiting*/)
{
isMoving = false; // Stops move code execution;
StartCoroutine(PositionWait()); // Calls the Coroutine
}
}
}
IEnumerator PositionWait()
{
yield return new WaitForSeconds(positionWait); // The Coroutine will wait for given seconds
isMoving = true; // Only after given seconds gone it will switch boolean to true so the Update() functions will starts execute code inside if (isMoving) statement
}
I hope it helps.
Hi,
Thanks for the help. I sort of understand that (a little bit) so I've re-written my code as such
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyAI_V1 : $$anonymous$$onoBehaviour
{
[Header("$$anonymous$$ovement Properties")]
public Transform[] waypoints;
public float speed;
public int curWaypoint;
public Vector3 target;
public Vector3 moveDirection;
public Vector3 velocity;
public float entryWait = 3f;
public float positionWait = 1f;
public bool is$$anonymous$$oving = true;
public bool patrol = true;
void Awake()
{
curWaypoint = 7;
$$anonymous$$oveToTarget();
}
void Update()
{
if (is$$anonymous$$oving)
{
$$anonymous$$oveToTarget();
}
else
{
velocity = Vector3.zero;
}
if (moveDirection.magnitude < 1)
{
is$$anonymous$$oving = false;
StartCoroutine(PositionWait());
}
GetComponent<Rigidbody>().velocity = velocity;
}
void $$anonymous$$oveToTarget()
{
target = waypoints[curWaypoint].position;
moveDirection = target - transform.position;
velocity = GetComponent<Rigidbody>().velocity;
velocity = moveDirection.normalized * speed;
}
private IEnumerator PositionWait()
{
yield return new WaitForSeconds(positionWait);
curWaypoint = Random.Range(0, 8);
is$$anonymous$$oving = true;
}
}
I got the object to stop for the desired duration, however the object wriggles as it's moving to its next target. From the inspector panel I can see that the script is still choosing a random target while it's moving off. Is there a way for the Random.Range function to fire off only once? Or do I have to change the variable in 'if (moveDirection.magnitude < 1)' to something else? Please advise. Thank you.
Ok, I think I sort of solved the issue by implementing the random.range inside the 'if (moveDirection.magnitude < 1)' statement ins$$anonymous$$d of the IEnumerator function. Thanks for the help.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyAI_V1 : $$anonymous$$onoBehaviour
{
[Header("$$anonymous$$ovement Properties")]
public Transform[] waypoints;
public float speed;
public int curWaypoint;
public Vector3 target;
public Vector3 moveDirection;
public Vector3 velocity;
public float entryWait = 3f;
public float positionWait = 1f;
public bool is$$anonymous$$oving = true;
public bool patrol = true;
void Awake()
{
curWaypoint = 7;
$$anonymous$$oveToTarget();
}
void Update()
{
if (is$$anonymous$$oving)
{
$$anonymous$$oveToTarget();
}
else
{
velocity = Vector3.zero;
}
if (moveDirection.magnitude < 1)
{
is$$anonymous$$oving = false;
curWaypoint = Random.Range(0, 14);
StartCoroutine(PositionWait());
}
GetComponent<Rigidbody>().velocity = velocity;
}
void $$anonymous$$oveToTarget()
{
target = waypoints[curWaypoint].position;
moveDirection = target - transform.position;
velocity = GetComponent<Rigidbody>().velocity;
velocity = moveDirection.normalized * speed;
}
private IEnumerator PositionWait()
{
yield return new WaitForSeconds(positionWait);
is$$anonymous$$oving = true;
}
}
Great that it worked out for you!
The implementations can be different, for example, there could potentially be situation where you are already called Coroutine PositionWait(), but moveDirection.magnitude < 1 remained true therefore your Coroutine gonna be called multiple times.
As an option to avoid that you can do it this way:
void Update()
{
if (is$$anonymous$$oving)
{
$$anonymous$$oveToTarget();
if (moveDirection.magnitude < 1)
{
is$$anonymous$$oving = false;
curWaypoint = Random.Range(0, 14);
StartCoroutine(PositionWait());
}
}
else
{
velocity = Vector3.zero;
}
GetComponent<Rigidbody>().velocity = velocity;
}
Or this way:
void Update()
{
if (is$$anonymous$$oving)
{
$$anonymous$$oveToTarget();
}
else
{
velocity = Vector3.zero;
}
GetComponent<Rigidbody>().velocity = velocity;
}
void $$anonymous$$oveToTarget()
{
target = waypoints[curWaypoint].position;
moveDirection = target - transform.position;
velocity = GetComponent<Rigidbody>().velocity;
velocity = moveDirection.normalized * speed;
if (moveDirection.magnitude < 1)
{
is$$anonymous$$oving = false;
curWaypoint = Random.Range(0, 14);
StartCoroutine(PositionWait());
}
}
In both options you are checking magnitude only when is$$anonymous$$oving true, but since you disable it after, then this piece of code for sure will not be executed until Coroutine PositionWait() will swap is$$anonymous$$oving to true.
Also, just for information since sometimes people don't know - the Random.Range is EXCLUSIVE when integer and INCLUSIVE when float with max value.
So, your Random.Range(0,14) never gonna take 14, but 13 as maximum.
Here is more info about that one: https://docs.unity3d.com/ScriptReference/Random.Range.html
Thank you for the advice. Your code looks cleaner than $$anonymous$$e, I'll implement it right now. Thanks again.
Your answer
Follow this Question
Related Questions
Can different variables referencing the same object return different instance IDs? 0 Answers
I need help with a waypoint system. 1 Answer
Vector2.Lerp not working properly. Horizontal jittering when not moving horizontally at all! 1 Answer
How do I make my enemy stop at specific waypoints? 0 Answers
PacMan IA of ghost using nodes/waypoints 0 Answers