- Home /
How do I use Coroutines remotely... and correctly ?
Hello, I tried to make a test coroutine to get used with coroutines, that would "pause" the script at a certain point until there ist gets a mouse input. Code looks like that :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Coroutines : MonoBehaviour {
// Use this for initialization
IEnumerator WaitForKeyDown(KeyCode keyCode){
while (!Input.GetKeyDown (keyCode))
{
Debug.Log("Returning null before");
yield return null;
Debug.Log ("Returning null after");
}
}
public void WaitKey(KeyCode keycode){
StartCoroutine (WaitForKeyDown(keycode));
}
}
I would like to call this coroutine from an Item scriptableobject (see code below). When the item is used (OnUse()) it processes every Effect (these are also scriptableobjects) in an array onto a target. But I would like to make a throwable item, so after the itemis used, I need the player to click an area to aim the projectile at. Which is why I need this "wait for input" coroutine
I know coroutines derive from monobehavior so I can't StartCoroutine them from scriptable object; I tried to make a simple monobehavior that simply holds the coroutine and contains a void that StartCoroutines the said coroutine. After that I put that script on a gameobject, referenced it in my Item which is supposed to call it and tried it. What happens is : -The item is used correctly on the right target -But it happens instantly, not waiting for the input -> the script is not paused -However, the coroutine does run : my log is populated with "Returning null" logs, and it stops when I press the input (number of "returning null" stops increasing)
Can someone explain to me what I'm doing wrong ? thanks in advance
//Targeted Item script
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu (menuName = "Item/TargetedItem")]
public class TargetedItem : Item {
public bool deleteOnUse;
public int amount_of_uses;
public string i_type =("TargetedItem");
public float range;
public float aoE;
private bool aiming = false;
public List<GameObject> hitObjects;
public override void OnUse(){
if (quantity == 0) {
} else {
Debug.Log ("Press space to sort of fire");
// wait for click
Coroutines coroutine;
coroutine = GameObject.FindWithTag ("Player").GetComponent<Coroutines>();
coroutine.WaitKey(KeyCode.Space);
Debug.Log (name +" quantity before effect : " + quantity);
quantity = quantity - 1;
targets.Add( GameObject.FindWithTag("Enemy"));
Debug.Log (targets [0].name);
Debug.Log ("Trying to apply effetcs");
foreach (Effects effect in effects) {
effect.OnHappen (targets);
Debug.Log ("Effetcs applied");
}
}
}
}
//
Click the error in console, it should point where the null is happening. Also, what is the button used to trigger the OnUse? If you use space bar, then that would explain the first case of returning without waiting.
Answer by Bunny83 · Sep 27, 2017 at 11:45 AM
First of all you have to understand what coroutines actually are and how they work. Coroutines run seemingly in parallel to the "normal" code flow.
Coroutines are actually objects, not methods. Those objects are statemachines which are created by the compiler based on the code inside your method.
When you call StartCoroutine you actually pass the coroutine object (the IEnumerator object that is returned from your generator method) to the Unity coroutine scheduler. It will immediately run the coroutine up to the first yield. However at the first yield StartCoroutine will return to the calling code and your "normal" code will continue. The coroutine object is stored internally by the coroutine scheduler and will automatically re-schedule the coroutine "when it's time". When this will be is determined by the value that you yielded. When you return "null", the scheduler will simply continue this coroutine the next frame.
Your coroutine and the "WaitKey" method are completely pointless because, as i said, the coroutine run independently from the code that called StartCoroutine.
So this line:
coroutine.WaitKey(KeyCode.Space);
Will start the coroutine but will return immediately and execute the rest of your OnUse method. The coroutine will still run in the background and is re-scheduled every frame until you finally press the button. At this time the coroutine will just finish. However your coroutine doesn't do anything after it left the while loop, so it just finishes and disappears.
The general rule is: If you have code you want to delay, that code has to be inside a coroutine. Since your "OnUse" isn't a coroutine, you can't "wait" inside that method.
There are several ways to solve that problem. The easiest way is to actually turn your code from OnUse into a coroutine. It doesn't matter where the actual coroutine code is declared or to which object it belongs to. You just need any MonoBehaviour to run a coroutine but the coroutine can be inside a ScriptableObject or a normal class (even a struct would work).
First i would suggest you turn your "Coroutines" class into a singleton. That way you don't have to use "FindWithTag" all the time.
public class Coroutines : MonoBehaviour
{
private static Coroutines m_Instance;
public static Coroutines Instance { get { return m_Instance;}}
private void Awake()
{
m_Instance = this;
}
IEnumerator WaitForKeyDown(KeyCode keyCode)
{
while (!Input.GetKeyDown (keyCode))
{
Debug.Log("Returning null before");
yield return null;
Debug.Log ("Returning null after");
}
}
public Coroutine WaitKey(KeyCode keycode)
{
return StartCoroutine (WaitForKeyDown(keycode));
}
}
Now inside your TargetedItem you can do:
public override void OnUse()
{
if (quantity != 0)
{
Coroutines.Instance.StartCoroutine(_Using());
}
}
private IEnumerator _Using()
{
yield return Coroutines.Instance.WaitKey(KeyCode.Space);
Debug.Log (name +" quantity before effect : " + quantity);
quantity = quantity - 1;
targets.Add( GameObject.FindWithTag("Enemy"));
Debug.Log (targets [0].name);
Debug.Log ("Trying to apply effetcs");
foreach (Effects effect in effects)
{
effect.OnHappen (targets);
Debug.Log ("Effetcs applied");
}
}
Note that the _Using coroutine is located inside the ScriptableObject but it's started on the MonoBehaviour "Coroutines" as we used StartCoroutine from that monobehaviour.
Also note that i changed the signature of your "WaitKey" method. It now returns the Coroutine instance that Unity creates when it's starting the coroutine. This Coroutine object can be used in another coroutine to wait for that coroutine to finish.
So the line
yield return Coroutines.Instance.WaitKey(KeyCode.Space);
inside our _Using coroutine will start the "WaitForKeyDown" coroutine and waits for it's completion.
Another way would be to use "callbacks". So if you don't want to declare a coroutine inside your ScriptableObject and you just want to wait for a keypress, you can do this:
IEnumerator WaitForKeyDown(KeyCode keyCode, System.Action aCallback)
{
while (!Input.GetKeyDown (keyCode))
{
yield return null;
}
if (aCallback != null)
aCallback();
}
public Coroutine WaitKey(KeyCode keycode, System.Action aCallback = null)
{
return StartCoroutine (WaitForKeyDown(keycode, aCallback));
}
I've just added an optional callback parameter to the WaitKey method. That way you can do:
public override void OnUse()
{
if (quantity != 0)
{
Coroutines.Instance.WaitKey(KeyCode.Space, ()=> {
// The code which should run when the key is pressed
});
// NOTE: code outside the callback will run immediately since we just started a coroutine.
}
}
Instead of a lambda expression / closure you can simply use a normal method
public override void OnUse()
{
if (quantity != 0)
{
Coroutines.Instance.WaitKey(KeyCode.Space, OnKeyPressed);
}
}
private void OnKeyPressed()
{
// executed when the key has been pressed
}
Answer by Aeskyphaz · Sep 27, 2017 at 06:16 PM
Thank you so much for your high detail and quality answer ! I think I can actually make use of both ways for diferent purpose You definitely covered coroutines better than any answer I could find about my issue. Was worth the post :)
I'll mark it as accepted
have a nice day