How can I make the name of the Coroutine dynamic?
I use this to make targets die automatically after a few seconds:
void InitiateKill(int i)
{
//i is the number of the target
StartCoroutine(TargetDie(i, timeAlive/1000));
//some other stuff
}
When the target is killed before this timer ends, I obviously get an error because it can't kill the target again.
That's why I want to stop the coroutine of that specific target, but I don't know how.
I tried:
Coroutine b[i] = StartCoroutine(TargetDie(i, timeAlive/1000));
But that gives a syntax error. b[i] can't be used for Coroutines.
How to do this the proper way?
Update:
This is (the relevant part of) my TargetDie function:
IEnumerator TargetDie(int i, float delayTime)
{
yield return new WaitForSeconds(delayTime);
Destroy(targets[i]);
}
When the player kills the target, I do:
void Damage(int i)
{
// at this time, the first Coroutine, started in InitiateKill, should stop, because otherwise it tries to destroy the target twice
StartCoroutine(TargetDie(i, 0));
}
So most simple way, move the coroutine on the object itself.
public class DieScript: MonoBehaviour
{
private Manager manager;
public void StartDeathProcess(Manager manager)
{
this.manager = manager;
StartCoroutine(DieAsync(manager));
}
private IEnumerator DieAsync(Manager manager)
{
yield return new WaitForSeconds(timer);
Destroy(this.gameObject);
}
public void Dmaage() // This is register as listener for death of the object
{
this.manager.PropagateDeath(this);
Destroy(this.gameObject);
}
}
public class Manager:MonoBehaviour
{
private List<DieScript> die;
void InitiateKill(int i)
{
die[i].StartDeathProcess(this);
}
}
Trying to keep the control on the controller is about to bring more pain than solution.
You could have a list of IEnumerator to keep track of the running coroutines. But you still need a message from the object to inform the controller that it is dead. So you are missing this part.
You need a script on the target so the controller knows about this type.
Controller runs the coroutine with a reference to that script asking each frame are you dying. When the target is meant to die, it sets a boolean to inform. Using Destroy keeps the object until end of frame so it would work out.
But this is doom to fail later on. It is kinda against programming concept to have a controller doing everything. You should see it more as a bypass for information.
You can simply check for null before destroying it so that you won't get any error:
if (targets != null)
Destroy(targets[i]);
But below is the recommended way if you want to stop the old coroutine.
You can use Dictionary to handle it. Use the int i as the key and Coroutine as the value.
When you call InitiateKill, add the int i to the dictionary and when you start the coroutine, add the Coroutine as the value in the dictionary too.
When Damage is called, check if that int value exist in the Dictionary. If it does, use it to retrieve the Coroutine value and stop the old coroutine. If it doesn't exit, start a new coroutine and then add it to that dictionary.
The dictionary should look like this:
Dictionary<int, Coroutine> dyingTarget = new Dictionary<int, Coroutine>();
Your new InitiateKill function which adds to the Dictionary:
void InitiateKill(int i)
{
//i is the number of the target
Coroutine crt = StartCoroutine(TargetDie(i, timeAlive / 1000));
//Add to Dictionary
dyingTarget.Add(i, crt);
}
Your new Damage function now checks if the item is already in the dictionary then retries it, stops the coroutine and removes it from the Dictionary.
void Damage(int i)
{
// at this time, the first Coroutine, started in InitiateKill, should stop, because otherwise it tries to destroy the target twice
StopIfAlreadyRunning(i);
Coroutine crt = StartCoroutine(TargetDie(i, 0));
//Add to Dictionary
dyingTarget.Add(i, crt);
}
void StopIfAlreadyRunning(int i)
{
Coroutine crtOut;
//Retrieve and stop old coroutine if it exist then removes it
if (dyingTarget.TryGetValue(i, out crtOut))
{
StopCoroutine(crtOut);
dyingTarget.Remove(i);
}
}
The new TargetDie function which removes it from the Dictionary after killing it. It also checks for null before destroying it:
IEnumerator TargetDie(int i, float delayTime)
{
yield return new WaitForSeconds(delayTime);
if (targets != null)
Destroy(targets[i]);
//Remove from Dictionary
dyingTarget.Remove(i);
}
Related
How would you find the time since a certain variable was changed? Take for example a boolean variable, how would you find the time since it was last changed? I want to use the boolean variable as a trigger (activating the trigger when it's true), but only after an exact, constant time (such as 0.5s) has passed since it was changed to true (it can only be changed from false to true).
Here is the code I have:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class hitRegistration : MonoBehaviour
{
AudioSource hitSound;
private bool hitState = false;
// Use this for initialization
void Start()
{
hitSound = gameObject.GetComponent<AudioSource>();
}
void OnMouseOver()
{
Debug.Log("Mouse is over game object.");
if (Input.GetKey(KeyCode.X) && hitState == false)
{
hitSound.Play();
hitState = true;
}
}
private void OnMouseExit()
{
Debug.Log("Mouse is no longer over game object.");
if (hitState == true)
{
// sound clip gets cut if the cursor leaves before its finished.
Destroy(gameObject);
}
}
// Update is called once per frame
void Update()
{
}
}
"OnMouseOver()" Is simply a function that is called when the mouse is placed over the game object in question. I want to delay destroying the game object until a certain time has passed.
First off, as noted in a comment, you are probably trying to solve this problem the wrong way and you are probably asking an "XY" question -- a question where you are asking a question about a proposed bad solution instead of asking a question about the actual problem you face.
To answer the question you actually asked, for better or worse: there is no way to associate behaviours with reading or writing a variable in C#, but you can associate behaviours with a property:
private bool hitState; // The "backing store".
private bool HitState
{
get
{
return hitState;
}
set
{
hitState = value;
}
}
You would then use HitState rather than hitState throughout the rest of your class.
Now you can add whatever logic you want that happens when the property is read or written:
private DateTime hitStateTime = default(DateTime);
private bool hitState; // The "backing store".
private bool HitState
{
get
{
return hitState;
}
set
{
hitState = value;
hitStateSet = DateTime.Now;
}
}
Now you know when it was set. And so on.
Unless you really need to keep track of how much time has passed on each single frame, one way to do what you are asking for is to use Unity Coroutines.
A coroutine is a method that runs in parallel with the main thread. To solve your question, you can first create a coroutine in the same script, that waits and then does the thing you want to have delayed. A couroutine in Unity is a method that takes up to one parameter and has an IEnumerator return type. You use yield return WaitForSeconds(t); inside the coroutine to have it delay for t seconds.
Then, once it's time to die, check if the mouse is currently hovering over the object with isHovered (set in your OnMouseOver/OnMouseExit methods). If it is, keep a note that it's time to die. If it isn't, then it can die immediately.
IEnumerator WaitToDie(float delaySeconds)
{
yield return new WaitForSeconds(delaySeconds);
// If the mouse is on the object, let OnMouseExit know we're ready to die
if (isHovered)
{
readyToDie = true;
}
// Otherwise, just die
else
{
Destroy(gameObject)
}
}
And then inside your OnMouseOver code, run the coroutine after starting the sound
void OnMouseOver()
{
isHovered = true;
Debug.Log("Mouse is over game object.");
if (Input.GetKey(KeyCode.X) && !hitState)
{
hitState = true;
hitSound.Play();
// we want to delay for half a second before processing the hit.
float delaySeconds = 0.5;
IEnumerator coroutine = WaitToDie(delaySeconds);
StartCoroutine(coroutine);
}
}
And in your OnMouseExit, let everything know that you're done hovering and check if it's past time to die or not.
private void OnMouseExit()
{
isHovered = false;
Debug.Log("Mouse is no longer over game object.");
if (readyToDie) {
Destroy(gameObject);
}
}
Altogether this code will have the object die when both the mouse is off the object AND the time has elapsed.
As a sidenote, I think you might want to revisit how you are checking for a hit, unless your really want to trigger from the player holding X and then moving the mouse over the object. If you intend to trigger any time X is pressed down while the mouse is on top, you might want to put the check in Update and check Input.GetKey(KeyCode.X) && !hitState && isHovered
keep a seperate variable(DateTime) and call it lastUpdate. then be sure to set it to DateTime.Now, each time the bool you're tracking is updated. then when you need to see how long its been you can just subtract:
DateTime lengthOfTime = DateTime.Now-lastUpdate;
from lengthOfTime you can now access how many days, hours, minutes, and/or seconds have passed.
im on my phone so take it easy on my pseudo-code.
good luck
If I use one-layer yield return, StopCoroutine() can successfully stop my coroutine. See the code example below...
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestStopcoroutine : MonoBehaviour {
IEnumerator co = null;
// Use this for initialization
void Start () {
co = FunA();
StartCoroutine(co);
}
private IEnumerator FunA() {
Debug.Log("Enter FunA...");
yield return RepeatPrint();
Debug.Log("FunA end...");
}
private IEnumerator RepeatPrint() {
for (int i = 0; i < 5; i++) {
Debug.Log(i);
yield return new WaitForSeconds(1);
}
}
/// <summary>
/// Set this function to a button on UI Canvas
/// </summary>
public void OnCancelButtonClick() {
if (co != null) {
StopCoroutine(co);
Debug.Log("Stop Coroutine...");
co = null;
}
}
}
this output is...
// Enter FunA...
// 0
// 1
// 2
// 3
// Stop Coroutine...
However, if I add one layer (i.e.FunB()), FunA() will be stopped but the inside coroutine(FunB()) will not be stopped. See the example code below:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestStopcoroutine : MonoBehaviour {
IEnumerator co = null;
void Start () {
co = FunA();
StartCoroutine(co);
}
private IEnumerator FunA() {
Debug.Log("Enter FunA...");
yield return FunB();
Debug.Log("FunA end...");
}
private IEnumerator FunB () {
Debug.Log("Enter FunB...");
yield return RepeatPrint();
Debug.Log("FunB end...");
}
private IEnumerator RepeatPrint() {
for (int i = 0; i < 5; i++) {
Debug.Log(i);
yield return new WaitForSeconds(1);
}
}
/// <summary>
/// Set this function to a button on UI Canvas
/// </summary>
public void OnCancelButtonClick() {
if (co != null) {
StopCoroutine(co);
Debug.Log("Stop Coroutine...");
co = null;
}
}
}
this output is...
// Enter FunA...
// Enter FunB...
// 0
// 1
// 2
// Stop Coroutine...
// 3
// 4
// FunB end...
Therefore, I am wondering why StopCoroutine() cannot successfully stop multi-layer yield return coroutine??
For your second code, the log should indeed end at Stop Coroutine.....
There are three possibilities to why it is showing the current output: (Very likely in order why this is happening)
1. You are setting Time.timeScale to 0. Search in your whole project and make sure that you're not doing this: Time.timeScale = 0;. This can pause or upause a coroutine function that's waiting with WaitForSeconds. If you did, temporary remove or comment it out and see if it's the issue.
2. Your project is corrupted and there is now a bug in it. Sometimes, a bug can randomly happen in a Unity project and the only way to fix that is to create a new project and manually move the resources from the old project to the new one.
Create a new project and test your modified code below.
3. A bug with Unity itself. Since you're using Unity 5.6.4p3, there is chance this is bug with Unity. If doing what's in #1 and #2 did not solve your issue then simply update Unity to the latest version (Unity 2018.xxx). This is more likely to fix your issue and don't forget to test with a new project instead of importing the old one.
Use the modified code from your question below to test #2 and #3. It uses the Invoke function to stop the coroutine after one second.
IEnumerator co = null;
void Start()
{
co = FunA();
Invoke("OnCancelButtonClick", 1f);
StartCoroutine(co);
}
private IEnumerator FunA()
{
Debug.Log("Enter FunA...");
yield return FunB();
Debug.Log("FunA end...");
}
private IEnumerator FunB()
{
Debug.Log("Enter FunB...");
yield return RepeatPrint();
Debug.Log("FunB end...");
}
private IEnumerator RepeatPrint()
{
for (int i = 0; i < 5; i++)
{
Debug.Log(i);
yield return new WaitForSeconds(1);
}
}
/// <summary>
/// Set this function to a button on UI Canvas
/// </summary>
public void OnCancelButtonClick()
{
if (co != null)
{
StopCoroutine(co);
Debug.Log("Stop Coroutine...");
co = null;
}
}
The expected output:
Enter FunA...
Enter FunB...
0
Stop Coroutine...
To address KYL3R's statement in his answer that stopping the main Coroutine won't affect any other coroutine, that has already been started. This is partially true but totally depends on how and where the coroutine was started.
There are two ways to start a coroutine function:
1. Start a coroutine with the StartCoroutine function from any function like a function with a void return type.
void Start()
{
StartCoroutine(RepeatPrint());
}
or
IEnumerator Start()
{
yield return StartCoroutine(RepeatPrint());
}
2. Start a coroutine function without the StartCoroutine function by yielding the coroutine function you want to start. This must be done inside a coroutine function or a function with IEnumerator return type. It can't be done in a normal function like a void function.
IEnumerator Start()
{
yield return RepeatPrint();
}
When you start a coroutine with StartCoroutine then start children coroutines after with StartCoroutine but then killed the parent coroutine, the children coroutine functions will and should continue to run untouched.
Now, when you start a coroutine with StartCoroutine then start children coroutines after with yield return YourCoroutine() without using the StartCoroutine function but then killed the parent coroutine, the children coroutine functions will and should terminate or stop immediately so as the parent one.
It's not surprising that most Unity users don't know because it is not documented but it's something that's very important to know when using coroutine in Unity.
Coroutines run independent from each other. So stopping the main Coroutine won't affect any other, that has already been started.
It seems you have 2 options:
A: Stop the nested Coroutine from inside the first level
Store a Reference to RepeatPrint and use that to stop it using StopCoroutine.
edit: actually "from inside the first level" is not necessary, just use the correct Reference.
B: Stop ALL Coroutines in your MonoBehaviour
May work if you have no other Coroutines running
I'm using a Coroutine to set up a repeating delay as follows.
In my Awake I have
StartCoroutine(RandomMove());
And then further down
IEnumerator RandomMove()
{
while (true)
{
// print(Time.time);
yield return new WaitForSeconds(foo);
// print(Time.time);
}
}
where 'foo' is a random float value that I change with every iteration.
Lets say foo is 10 seconds and part way thru the delay I need to reset the delay so it starts 'counting down' from 10 again.
How would I accomplish this? Should I use a Timer instead?
I don't like either of the two existing answers. Here's what I'd do:
Kill and Restart the coroutine:
We'll start with this part of the killer_mech's answer:
Coroutine myCoroutine;
void Awake() {
myCoroutine = StartCoroutine(RandomMove());
}
But we're going to handle the rest differently. killer_mech never did anything with the reference, other than to keep ovewriting it.
Here's what we're doing instead:
public void resetRandomMove() {
StopCoroutine(myCoroutine);
myCoroutine = StartCoroutine(RandomMove());
}
Call this any time you need to reset it.
I would suggest you first store Coroutine in a variable.
Coroutine myCoroutine;
void Awake()
{
myCoroutine = StartCoroutine(RandomMove());
}
and change the coroutine function as
IEnumerator RandomMove()
{
// print(Time.time);
yield return new WaitForSeconds(foo);
// print(Time.time);
// Call your new coroutine here
myCoroutine = StartCoroutine(RandomMove());
}
this way you will have a coroutine variable for every iteration. If you need to stop the coroutine just say :
StopCoroutine(myCoroutine);
in your function at required time.This will allow you to stop a coroutine in middle before the end of countdown. Also at the end of coroutine it will start new coroutine with updated reference After finishing your task just call back again with
myCoroutine = StartCoroutine(RandomMove());
Hope this resolves your problem. Yes you can do it with timer also with a boolean flag the same thing but I think using coroutine is much simpler.
.
Hmmm it could something like this also . Just for my own .
void Start() {
StartCoroutine(RepeatingFunction());
}
IEnumerator RepeatingFunction () {
yield return new WaitForSeconds(repeatTime);
StartCoroutine( RepeatingFunction() );
}
As i understand the question. InvokeRepeating() is also a choice.
Maybe it is because you are each frame waiting for new assigned seconds?
Why don't you make the random before yielding the wait, and store the CustomYieldInstruction instead of yielding a new instance, since it disposes the one that was before, that creates memory problems. You won't notice that if you yield return a WaitForSeconds with a constant value, but maybe with a random one creates ambiguity and resets the timer (see this Unity's optimization page, on the Coroutine's section). A quick example:
public float foo;
public float min;
public float max;
void Awake()
{
StartCoroutine(Countdown());
}
IEnumerator Countdown()
{
while(true)
{
foo = Random.Range(min, max);
WaitForSeconds wait = new WaitForSeconds(foo);
yield return wait;
}
}
Also, #ryeMoss's solution seems a good one, to stop and restart the coroutine if 'foo' changes.
Hope it helps.
In this function, I wanted to get a random index between 0 and the max size of my list.
I then use that random index so I can pick a random Node in the list.
I go through an if statement and check if other Objects aren't using that random Node I chose.
If no other Objects are using that random Node, I return that Node so the Object that called this method can use it.
However, if that Node is currently being used by another Object, then I wan't to go through the function again, until it gets a Node it can use, so I return the function itself.
The result is an overflow error because it's infinitely called (game still works). My first thought was to use a delay (coroutine), so the function doesn't get called so frequently; but the problem is that I need the return type of type DodgeNode.
public class DodgeLocations : MonoBehaviour {
public List<DodgeNode> nodes;
private DodgeNode randomNode;
private int randomIndex;
public DodgeNode SelectRandomNode(){
randomIndex = Random.Range(0, nodes.Count);
randomNode = nodes[randomIndex];
// If the random node is not currently taken (which means if an enemy isn't currently attacking it)
if (!randomNode.IsTaken ()) {
// Then the random node is now taken; and other enemies can't touch that node, until the current enemy finishes attacking it
randomNode.IsTaken (true);
return randomNode;
} else {
return SelectRandomNode (); // If the node is taken, ask again if there's another node that's free to attack
}
}
}
I brainstormed how it would look like if I used a coroutine and I thought about this.
public class DodgeLocations : MonoBehaviour {
public List<DodgeNode> nodes;
private DodgeNode randomNode;
private int randomIndex;
IEnumerator SelectRandomNode(){
yield return new WaitForSeconds(2f);
randomIndex = Random.Range(0, nodes.Count);
randomNode = nodes[randomIndex];
// If the random node is not currently taken (which means if an enemy isn't currently attacking it)...
if (!randomNode.IsTaken ()) {
// Then the random node is now taken; and other enemies can't touch that node, until the current enemy finishes attacking it
randomNode.IsTaken (true);
yield return randomNode;
} else {
yield return SelectRandomNode (); // If the node is taken, ask again if there's another node that's free to attack
}
}
}
Of course this is wrong because I can't return something of type DodgeNode with a function that has a return type of IEnumerator. However, I still want to use WaitForSeconds, so I do need the IEnumerator return type.
The problem here is that I want to return a DodgeNode, but at the same I need the function to be of return type IEnumerator for the WaitForSeconds to work.
In response to Serlite, here's where I'm using this function (deleted a lot of irrelevant code):
public class Bat : Enemy {
private DodgeNode nodeToTarget; // Node that bat want's to attack
private Vector3 startPoint; // Bat's original position
private Vector3 endPoint; // Bat's end position
void Start(){
startCoroutine(AttackPlayerNode());
}
IEnumerator AttackPlayerNode(){
while (true) {
nodeToTarget = dodgeLocations.SelectRandomNode();
endPoint = nodeToTarget.transform.position;
yield return new WaitForSeconds (2f);
yield return StartCoroutine(MoveToPoint(startPoint, endPoint));
nodeToTarget.IsFree(); // This makes the Node free for other object to use it
}
}
}
Found my "Solution"
Disclaimer: I'm a beginner programmer/student
I got a piece of paper and tried to write out all of my thought process and ended up with an alternate "solution". Rather than trying to attempt to call WaitForSeconds in SelectRandomNode(), I decided to make SelectRandomNode() return null if all Nodes were occupied. In the IEnumerator AttackPlayerNode(), I have this code:
// If the bat doesn't have a node to target
while(nodeToTarget == null){
yield return new WaitForSeconds(0.5f);
nodeToTarget = dodgeLocations.SelectRandomNode();
}
Since I'm returning null, this while loop will keep going until a Node is open. This will still generate a overflow error (it should I think), but I am now using WaitForSeconds which will make the check for an open node less frequent, which prevents the overflow error (to my understanding).
It's probably a very ugly/temporary solution, but I can always go back for optimization in the future! This was haunting me for a full day and am glad I can focus on other elements of my game now.
public class DodgeLocations : MonoBehaviour {
public List<DodgeNode> nodes;
private DodgeNode randomNode;
// Returns a randomly chosen node
public DodgeNode SelectRandomNode(){
int randomIndex = Random.Range(0, nodes.Count);
randomNode = nodes[randomIndex];
if (!randomNode.isTaken) {
randomNode.IsTaken (true);
return randomNode;
} else {
return null; // <--- What was changed
}
}
}
public class Bat : Enemy {
private DodgeNode nodeToTarget; // Node that bat want's to attack
private Vector3 startPoint; // Bat's original position
private Vector3 endPoint; // Bat's end position
void Start(){
startCoroutine(AttackPlayerNode());
}
IEnumerator AttackPlayerNode(){
while (true) {
// If the bat doesn't have a node to target
while(nodeToTarget == null){
yield return new WaitForSeconds(0.5f); // Prevent overflow error
nodeToTarget = dodgeLocations.SelectRandomNode1();
}
endPoint = nodeToTarget.transform.position;
yield return new WaitForSeconds (2f);
yield return StartCoroutine(MoveToPoint(startPoint, endPoint));
nodeToTarget.IsFree(); // This makes the Node free for other object to use it
nodeToTarget = null;
}
}
As you probably have already suspected, you're simply doing it wrong.
Using recursion for simple looping is wrong. At worst, your method should instead look something like this:
public DodgeNode SelectRandomNode(){
while (true)
{
randomIndex = Random.Range(0, nodes.Count);
randomNode = nodes[randomIndex];
// If the random node is not currently taken (which means if an enemy isn't currently attacking it)
if (!randomNode.IsTaken ()) {
// Then the random node is now taken; and other enemies can't touch that node, until the current enemy finishes attacking it
randomNode.IsTaken (true);
return randomNode;
}
}
}
Better would be to identify eligible nodes before you start picking them randomly:
public DodgeNode SelectRandomNode(){
DodgeNode[] eligible = nodes.Where(n => !n.IsTaken()).ToArray();
randomIndex = Random.Range(0, eligible.Length);
randomNode = nodes[randomIndex];
randomNode.IsTaken(true);
return randomNode;
}
Naturally, if it's possible for there to not be any eligible nodes, you'd need to handle that case appropriately. It's not clear from your question what "appropriate" would be in this scenario.
It's not clear from the bare example you've provided why you store randomIndex as an instance field instead of local variable. If you really need for that to be the index relative to the original collection, you'll need to do a little more work to track the original index (see the Select() overload that passes the index along with the enumeration item). But the basic idea is the same.
If you'd rather not recreate the eligible array each time you need to pick a node, then you should maintain two collections: the "not taken" collection, and the "taken" collection. Then you just move nodes from one to the other as needed. If these collections are relatively small (hundreds, or maybe only thousands of items), these can just be regular List<T> objects. Larger collections may be costly to remove elements (due to shifting the remaining elements), in which case you might prefer using LinkedList<T>.
Aside: you seem to have declared IsTaken() with two overloads, one to return the current value, and one to set it. This is poor design. Ideally, it should just be a property, so you can leave out the () that's required for method invocations. If a property doesn't work for you for some reason, then the setting method should have a different name, like SetIsTaken()
I found an alternative "solution" and have edited my post above.
CTRL+F the following text in bold: Found my "Solution"
So I have a Unity coroutine method, in which I have some objects. These objects represent values that are being gathered from a server somewhere, and they send out an Updated event when they are ready.
I was wondering what the best way is to wait for all the values to be updated, inside a coroutine in Unity.
public IEnumerator DoStuff()
{
foreach(var val in _updateableValues)
{
if (val.Value != null) continue;
else **wait for val.Updated to be fired then continue**
}
//... here I do stuff with all the values
// I use the WWW class here, so that's why it's a coroutine
}
What would be the best way of doing something like this?
Thanks!
There is no builtin direct method to wait for the event itself, but you can use a synchronous nested coroutine to wait for a flag set by the event:
//Flag
bool eventHappened;
//Event subscriber that sets the flag
void OnEvent(){
eventHappened=true;
}
//Coroutine that waits until the flag is set
IEnumerator WaitForEvent() {
yield return new WaitUntil(eventHappened);
eventHappened=false;
}
//Main coroutine, that stops and waits somewhere within it's execution
IEnumerator MainCoroutine(){
//Other stuff...
yield return StartCoroutine(WaitForEvent());
//Oher stuff...
}
With that in mind, creating a generic coroutine that waits for an UnityEvent is easy:
private IEnumerator WaitUntilEvent(UnityEvent unityEvent) {
var trigger = false;
Action action = () => trigger = true;
unityEvent.AddListener(action.Invoke);
yield return new WaitUntil(()=>trigger);
unityEvent.RemoveListener(action.Invoke);
}
I thing that a better way is to check the sever every frame and not to wait for the an amount of time without any thinking.
public IEnumerator DoStuff()
{
/* wait until we have the value we want */
while( value != desiredValue)
yield return null
//after this loop, start the real processing
}
This is my little fix.
I use the WaitUntil directly where I need it. I found though, that WaitUntil doesn't want a boolean but expects a boolean predicate function - so I provide this in my example below.
The example is real running code from a game. It runs in the very first scene, where I start the music before I do - the slightly lengthly - loading of the other stuff.
//
public class AudioSceneInitializer : MonoBehaviour
{
[SerializeField] private GlobalEventsSO globalEvents;
//globalEvents is a scriptable object that - in my case - holds all events in the game
[SerializeField] private AudioManager audioManager;
//The audio manager which in my case will raise the audio ready event
[SerializeField] public GameObject audioGO;// contains AudioSources used by AudioManager
private bool audioReady = false;
// Start is called before the first frame update
IEnumerator Start()
{
if (audioManager != null)
//better check, if we don't produce the event, we wait forever
{
//subscribe to event
globalEvents.audioManagerReadyEvent += OnAudioReady;
//do something to raise the event - else we wait forever
audioManager.onInitialization(this);//"this" needed to hand over the audioGO to the AudioManager
// waits until the flag is set;
yield return new WaitUntil(IsAudioReady);
}
//now that we have music playing, we load the actual game
SceneManager.LoadSceneAsync(1, LoadSceneMode.Additive);
}
//Event subscriber that sets the flag
void OnAudioReady()
{
//unsubscribing, if - as in my case - it happens only once in the game
globalEvents.audioManagerReadyEvent -= OnAudioReady;
audioReady = true;
}
//predicate function that WaitUntil expects
private bool IsAudioReady()
{
return audioReady;
}
public void OnDisable()
{
audioManager.onCleanup();
}
}
A spin-lock is a solution, however not a very CPU-gentle one. In a spin-lock, you would just wait for the variable to have a certain value, otherwise sleep for a few milliseconds.
public IEnumerator DoStuff()
{
/* wait until we have the value we want */
while( value != desiredValue)
yield return new WaitForSeconds(0.001f);
//after this loop, start the real processing
}
Maybe you might want to think about restructuring your code, sothat no spin-lock is requiered, but a more interupt/event-based based approach can be implemented. That means, if you update a value and something has to take place after it happened, kick it off directly after changing that value. In C#, there's even an interface INotifyPropertyChanged for that design pattern (see MSDN), but you can easily design that yourself, too, e.g. by firing an event when that certain value has changed. We'd need more information on what exactly you want to react here, if you want a better solution than a spinlock, but this should give you some ideas.