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

C# Inline-Functions not working in conjunction with unity's Invoke() ?

Discussion in 'Scripting' started by unity2life, Aug 5, 2020.

  1. unity2life

    unity2life

    Joined:
    Jun 6, 2020
    Posts:
    19
    The below lines of valid code result in the following unity runtime-error:

    Trying to Invoke method: UIController.LoadGameScene couldn't be called.


    Code (CSharp):
    1.    
    2.     public void HandlePlay()
    3.     {
    4.         // trigger animation, which lasts 1 sec:
    5.         GameObject.Find("Notepad").GetComponent<Animator>().SetBool("fadeOut", true);
    6.  
    7.         // inline-function, which calls a method with parameters:
    8.         void LoadGameScene() {SceneManager.LoadScene("Game");};
    9.         // delayed method call enabling parameters:
    10.         Invoke("LoadGameScene", 1);
    11.     }
    12.    
    I don't understand why this doesn't work?

    By the way, I'm not looking for a working alternative as with StartCoroutine() etc., I know there's some other ways to do this ...
     
  2. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,179
    Yeah, Invoke is just a wrapper around calling a method through reflection. So that won't be able to invoke local functions. Also, you should probably not use Invoke! It's string-based, aka. hard to debug and bug-prone, as well as slow.

    To achieve what you're doing, we've got an InvokeAfter extension that runs a coroutine in the background:

    Code (csharp):
    1.  
    2.    
    3.     public void HandlePlay()
    4.     {
    5.         // trigger animation, which lasts 1 sec:
    6.         GameObject.Find("Notepad").GetComponent<Animator>().SetBool("fadeOut", true);
    7.  
    8.         // inline-function, which calls a method with parameters:
    9.         void LoadGameScene() {SceneManager.LoadScene("Game");};
    10.         // delayed method call enabling parameters:
    11.         this.InvokeAfter(1f, LoadGameScene);
    12.     }
    13.  
    Extension method is:

    Code (csharp):
    1.  
    2.     /// <summary>
    3.     /// Invokes an action after a certain time, using a coroutine.
    4.     /// </summary>
    5.     /// <param name="behaviour">Behaviour to use as runner for the internal coroutine</param>
    6.     /// <param name="afterTime">How long before the action should be invoked</param>
    7.     /// <param name="action">Action to invoke</param>
    8.     /// <returns>The Coroutine containing the waiting and action, so you can stop it if neccessary</returns>
    9.     public static Coroutine InvokeAfter(this MonoBehaviour behaviour, float afterTime, Action action) {
    10.         if (behaviour != null) {
    11.             return behaviour.StartCoroutine(InvokeAfter(action, afterTime, behaviour));
    12.         }
    13.         return null;
    14.     }
    15.  
    16.     private static IEnumerator InvokeAfter(Action action, float time, MonoBehaviour behaviour) {
    17.         yield return new WaitForSeconds(time);
    18.         if (behaviour != null) {
    19.             action();
    20.         }
    21.     }
    22.  
     
    unity2life likes this.
  3. unity2life

    unity2life

    Joined:
    Jun 6, 2020
    Posts:
    19
    Ah ok, I understand, thx dude.
    Well, of course using coroutines is a working alternative as I already stated.
    If u do Invoke() by reflection it would be a very nice thing if u supply a future Invoke() with parameters also, but I understand, using reflection is always time-consuming. However in my case, I want to wait anyways for some given time, so time doesn't really matter ;-).

    InvokeAfter() is probably a new feature, cause I don't get it ? I use unity version 2019.3.15f1 ?
     
  4. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,179
    I posted the extension method too. If that's unfamiliar to you, then you can google it!
     
    unity2life likes this.
  5. unity2life

    unity2life

    Joined:
    Jun 6, 2020
    Posts:
    19
    Ah, thx, now I got it, yes the term "extension method" was new to me, I'm completely new to unity and c# ;-)
     
  6. alexzzzz

    alexzzzz

    Joined:
    Nov 20, 2010
    Posts:
    1,447
    I would just use async/await

    Code (CSharp):
    1. async void Foo()
    2. {
    3.     Debug.Log("Hello, World!");
    4.     await Task.Delay(1000);
    5.     Debug.Log("after 1000 milliseconds");
    6. }
     
    unity2life likes this.
  7. unity2life

    unity2life

    Joined:
    Jun 6, 2020
    Posts:
    19
    Thx, mega :) !!! Very short and simple !
     
  8. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,179
    I've tried that - as in using async over Coroutines in general, ended up with a bunch of issues. Primarily that the async method is not stopped when the object is destroyed, and that cancelling async methods is just an utter pain in the ass compared to just getting a reference to the coroutine and calling Stop on it.

    Async is a great replacement for other ways of doing actually asynchronous things, as all the ways in which async is a bit annoying is ways that asynchronous things have to be annoying. Taking on that cost when you're doing something fundamentally synchronous - like waiting 1 second and then doing something on the main thread - isn't worth it.
     
    unity2life likes this.
  9. unity2life

    unity2life

    Joined:
    Jun 6, 2020
    Posts:
    19
    Ok, very interesting to read this and also thx for ur time on this issue.
    Even if this solution seems to be very easy and simple to a c#-noob like me, I also wondered if this could interfere with unity's lifecycle somehow ... .
     
  10. alexzzzz

    alexzzzz

    Joined:
    Nov 20, 2010
    Posts:
    1,447
    Imagine, you have a gameobject with a script like this
    Code (CSharp):
    1. using System.Threading.Tasks;
    2. using UnityEngine;
    3.  
    4. public class Foo : MonoBehaviour
    5. {
    6.     private async void Update()
    7.     {
    8.         if (Input.GetKeyDown(KeyCode.D))
    9.         {
    10.             Destroy(gameObject);
    11.         }
    12.  
    13.         if (Input.GetKeyDown(KeyCode.H))
    14.         {
    15.             Debug.Log("Hello,");
    16.             await Task.Delay(5000);
    17.             Debug.Log("World!");
    18.         }
    19.     }
    20. }

    If you press H and then D, the object and all its components will be destroyed but the code after await will still be executed and you'll see it printing "World!" after the delay. If this is exactly what you want, then it's fine.

    However, you may want to stop all the pending executions if the object gets destroyed. In that case use Invoke, InvokeRepeating or coroutines. They are implicitly bound to the object they are called from, and won't run if the object is destroyed.
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class Bar : MonoBehaviour
    4. {
    5.     private void Update()
    6.     {
    7.         if (Input.GetKeyDown(KeyCode.D))
    8.         {
    9.             Destroy(gameObject);
    10.         }
    11.  
    12.         if (Input.GetKeyDown(KeyCode.H))
    13.         {
    14.             Debug.Log("Hello,");
    15.             Invoke(nameof(PrintWorld), 5);
    16.         }
    17.     }
    18.  
    19.     private void PrintWorld()
    20.     {
    21.         Debug.Log("World!");
    22.     }
    23. }
    This script won't print "World!" if you destroy the object.

    PS
    Or you'll have to check if the object still exists manually like this
    Code (CSharp):
    1. using System.Threading.Tasks;
    2. using UnityEngine;
    3.  
    4. public class Foo : MonoBehaviour
    5. {
    6.     private async void Update()
    7.     {
    8.         if (Input.GetKeyDown(KeyCode.D))
    9.         {
    10.             Destroy(gameObject);
    11.         }
    12.  
    13.         if (Input.GetKeyDown(KeyCode.H))
    14.         {
    15.             Debug.Log("Hello,");
    16.             await Task.Delay(5000);
    17.  
    18.             if (this != null)
    19.             {
    20.                 Debug.Log("World!");
    21.             }
    22.             else
    23.             {
    24.                 Debug.Log("Gameobject or component is destroyed!!!");
    25.             }
    26.         }
    27.     }
    28. }
    Most of the .NET programmers will find the condition (this != null) pretty weird though :)
     
    Last edited: Aug 5, 2020
    unity2life likes this.
  11. unity2life

    unity2life

    Joined:
    Jun 6, 2020
    Posts:
    19
    Well in my simple case I think I can live with this behaviour, since the GameObject gets destroyed only after the Delay-Task has finished by loading the new scene? Thanks for your time and help!