Search code examples
javascriptc#unity-game-engineunity-webgl

How to call asynchronous Javascript functions from Unity scripts?


I have been trying to follow Unity's documentation for creating JavaScript files/functions so that I can get access to the web browsers apis to interface with the browsers JavaScript engine. https://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html

I am making a Unity 2d game for WebGL platforms and I want to have users be able to connect their MetaMask wallet and get access to their wallet addresses through MetaMasks api.

Following the documentation, I created a .jslib file in my /Assets/Plugins directory, that has the following function inside

mergeInto(LibraryManager.library, {

  GetMetaMaskWallet: async function (x, y) {
    const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
    .catch((err) => {
      if (err.code === 4001) {
        // EIP-1193 userRejectedRequest error
        // If this happens, the user rejected the connection request.
        console.log('Please connect to MetaMask.');
      } else {
        console.error(err);
      }
    });
    const account = accounts[0];
    return account;
  },
  
});

Inside my Unity project, I have a empty game object with a C# script attached, that takes in a Button and attaches a click event listener to it. This click event function, calls the JavaScript function I have above

    [DllImport("__Internal")]
    private static extern string GetMetaMaskWallet();


    public Button connectMetaMaskButton;

    void Start () 
    {
        // 'Connect with MetaMask button'
        Button btn = connectMetaMaskButton.GetComponent<Button>();
        btn.onClick.AddListener(ConnectWallet);
    }

    void ConnectWallet()
    {
    string account = GetMetaMaskWallet();

        if (account != "") {
            connectMetaMaskButton.gameObject.SetActive(false);
        }
    }

For some reason, async functions do not seem to work in returning their expected values. When I Build and Run the WebGL project, and click the Connect with MetaMask button, the MetaMask prompt pops up and everything works as expected, but nothing seems to be returning back to my C# script.

I verified that async JS functions are not returning anything by copying some of the examples provided in the Unity docs, for example the:

AddNumbers: function (x, y) {
    return x + y;
},

and turning it to:

AddNumbers: async function (x, y) {
    return x + y;
},

This causes nothing to return. I am aware the AddNumbers example makes no sense in being an async function, but testing removing the async makes the function work and return the appropriate number I am expecting back to my C# script.

Does anyone have some insight or guidance on how I could get my MetaMask example working, or help on what i am doing wrong with async functions in my JavaScript examples.


Solution

  • Unity doesn't wait result of async JavaScript functions. To get the result, you can use 2 ways:

    1. function SendMessage. This is the recommended way by Unity.

    in index.html add myGameInstance declaration:

    var myGameInstance = null;
    ...
    createUnityInstance(canvas, config, (progress) =>
        {...}).then((unityInstance) => {
            myGameInstance = unityInstance;
    ...
    

    Unity:

    using System.Runtime.InteropServices;
    using UnityEngine;
    
    public class AsyncWrapper : MonoBehaviour
    {
       [DllImport("__Internal")]
        private static extern void AddNumbers(int x, int y);
        
        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Debug.Log("Start calculate");
                AddNumbers(100, 200);
            }
        }
        
        public void ShowResult(int value)
        {
            Debug.Log("result by message:" + value);
        }
    }
    

    Add AsyncWrapper script in scene with "AsyncWrapper" as name of GameObject.

    Javascript:

    mergeInto(LibraryManager.library, {
      AddNumbers: async function (x, y) {
          await new Promise(resolve => setTimeout(resolve, 2000));
          myGameInstance.SendMessage('AsyncWrapper', 'ShowResult', x + y);
        },
    });
    

    2. Use callbacks and dynamic function call from Javascript:

    Unity:

    using System.Runtime.InteropServices;
    using AOT;
    using UnityEngine;
    
    public class AsyncWrapper : MonoBehaviour
    {
        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate void intCallback(int value);
        
        [DllImport("__Internal")]
        private static extern void AddNumbers(int x, int y,
            [MarshalAs(UnmanagedType.FunctionPtr)] intCallback result);
        
        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Debug.Log("Start calculate");
                AddNumbers(100, 200, ShowResult);
            }
        }
    
        [MonoPInvokeCallback(typeof(intCallback))]
        private static void ShowResult(int value)
        {
            Debug.Log("result:" + value);
        }
    }
    

    JavaScript:

    mergeInto(LibraryManager.library, {
      AddNumbers: async function (x, y, onSuccess) {
        await new Promise(resolve => setTimeout(resolve, 2000));
        dynCall_vi(onSuccess, x + y);
      },
    });