WebGL:與瀏覽器腳本交互
構建適用于 Web 的內容時,可能需要與網頁上的其他元素進行通信?;蛘撸赡芟M褂?Unity 當前在默認情況下未公開的 Web API 來實現功能。在這兩種情況下,都需要直接與瀏覽器的 JavaScript 引擎連接。Unity WebGL 提供了不同的方法來執(zhí)行此操作。
從 Unity 腳本調用 JavaScript 函數
在項目中使用瀏覽器 JavaScript 的建議方法是將 JavaScript 源代碼添加到項目中,然后直接從腳本代碼中調用這些函數。為此,請使用 .jslib 擴展名將包含 JavaScript 代碼的文件放置在 Assets 文件夾中的“Plugins”子文件夾下。插件文件需要有如下所示的語法:
mergeInto(LibraryManager.library, {
? Hello: function () {
? ? window.alert("Hello, world!");
? },
? HelloString: function (str) {
? ? window.alert(Pointer_stringify(str));
? },
? PrintFloatArray: function (array, size) {
? ? for(var i = 0; i < size; i++)
? ? console.log(HEAPF32[(array >> 2) + i]);
? },
? AddNumbers: function (x, y) {
? ? return x + y;
? },
? StringReturnValueFunction: function () {
? ? var returnStr = "bla";
? ? var bufferSize = lengthBytesUTF8(returnStr) + 1;
? ? var buffer = _malloc(bufferSize);
? ? stringToUTF8(returnStr, buffer, bufferSize);
? ? return buffer;
? },
? BindWebGLTexture: function (texture) {
? ? GLctx.bindTexture(GLctx.TEXTURE_2D, GL.textures[texture]);
? },
});
然后,可從 C# 腳本調用這些函數,如下所示:
using UnityEngine;
using System.Runtime.InteropServices;
public class NewBehaviourScript : MonoBehaviour {
? ? [DllImport("__Internal")]
? ? private static extern void Hello();
? ? [DllImport("__Internal")]
? ? private static extern void HelloString(string str);
? ? [DllImport("__Internal")]
? ? private static extern void PrintFloatArray(float[] array, int size);
? ? [DllImport("__Internal")]
? ? private static extern int AddNumbers(int x, int y);
? ? [DllImport("__Internal")]
? ? private static extern string StringReturnValueFunction();
? ? [DllImport("__Internal")]
? ? private static extern void BindWebGLTexture(int texture);
? ? void Start() {
? ? ? ? Hello();
? ? ? ? HelloString("This is a string.");
? ? ? ? float[] myArray = new float[10];
? ? ? ? PrintFloatArray(myArray, myArray.Length);
? ? ? ? int result = AddNumbers(5, 7);
? ? ? ? Debug.Log(result);
? ? ? ? Debug.Log(StringReturnValueFunction());
? ? ? ? var texture = new Texture2D(0, 0, TextureFormat.ARGB32, false);
? ? ? ? BindWebGLTexture(texture.GetNativeTexturePtr());
? ? }
}
簡單的數字類型可在函數參數中傳遞給 JavaScript,無需進行任何轉換。其他數據類型將作為 emscripten 堆(實際上就是 JavaScript 中的一個大型數組)中的指針進行傳遞。對于字符串,可使用?Pointer_stringify?helper 函數轉換為 JavaScript 字符串。要返回字符串值,必須調用?_malloc?來分配一些內存,并調用?stringToUTF8?helper 函數向其中寫入 JavaScript 字符串。如果字符串是返回值,則 il2cpp 運行時將負責為您釋放內存。對于原始類型的數組,emscripten?針對內存的不同大小的整數、無符號整數或浮點數表示形式,提供其堆的不同?ArrayBufferViews:__HEAP8、HEAPU8、HEAP16、HEAPU16、HEAP32、HEAPU32、HEAPF32、HEAPF64__。為了在 WebGL 中訪問紋理,emscripten 提供了?GL.textures?數組,該數組將本機紋理 ID 從 Unity 映射到 WebGL 紋理對象??稍?emscripten 的 WebGL 上下文?GLctx?中調用 WebGL 函數。
有關如何與 JavaScript 交互的更多信息,請參閱?emscripten 文檔。
另外,請注意,在 Unity 安裝文件夾中有幾個插件可供參考(位于?PlaybackEngines/WebGLSupport/BuildTools/lib?和?PlaybackEngines/WebGLSupport/BuildTools/Emscripten/src/library*?中)。
從 JavaScript 調用 Unity 腳本函數
有時需要從瀏覽器的 JavaScript 向 Unity 腳本發(fā)送一些數據或通知。建議的做法是調用內容中的游戲對象上的方法。如果要從嵌入在項目中的 JavaScript 插件執(zhí)行調用,可使用以下代碼:
unityInstance.SendMessage(objectName, methodName, value);
其中,__objectName__ 是場景中的對象名稱;__methodName__ 是當前附加到該對象的腳本中的方法名稱;__value__ 可以是字符串、數字,也可為空。例如:
unityInstance.SendMessage('MyGameObject', 'MyFunction');
unityInstance.SendMessage('MyGameObject', 'MyFunction', 5);
unityInstance.SendMessage('MyGameObject', 'MyFunction', 'MyString');
如果希望從嵌入頁面的全局作用域內執(zhí)行調用,請參閱下面的代碼可見性部分。
從 Unity 腳本調用 C 函數
由于 Unity 使用 emscripten 將源代碼從 C/C++ 代碼編譯為 JavaScript,因此您也可以使用 C/C++ 代碼編寫插件,并從 C# 調用這些函數。因此,您可以不使用上面示例中的 jslib 文件,而是在項目中使用 C/C++ 文件;它將自動使用您的腳本實現編譯,并且您可以從中調用函數,就像上面的 JavaScript 示例一樣。
如果使用 C++ (.cpp) 來實現該插件,則必須確保使用 C 鏈接來聲明函數以免發(fā)生名稱錯用問題:
# include <stdio.h>
extern "C" void Hello ()
{
? ? printf("Hello, world!\n");
}
extern "C" int AddNumbers (int x, int y)
{
? ? return x + y;
}
代碼可見性
從 Unity 5.6 開始,所有構建代碼都在自己的范圍內執(zhí)行。這種方法可以將內容嵌入任意頁面,而不會與嵌入頁面代碼發(fā)生沖突,并且可以在同一頁面上嵌入多個構建版本。
如果項目中包含?.jslib?插件形式的所有 JavaScript 代碼,則此 JavaScript 代碼的運行作用域將與編譯的構建相同,并且代碼應該與 Unity 先前版本中的工作方式非常相似(例如,以下對象和函數應該在 JavaScript 插件代碼中直接可見:Module、SendMessage、HEAP8、ccall 等)。
但是,如果計劃從嵌入頁面的全局作用域調用內部 JavaScript 函數,則必須在 WebGL 模板 index.html 中使用?unityInstance?變量。在 Unity 引擎實例化成功后執(zhí)行此操作,例如:
? var myGameInstance = null;
? ? script.onload = () => {
? ? ? createUnityInstance(canvas, config, (progress) => {...}).then((unityInstance) => {
? ? ? ? myGameInstance = unityInstance;
? ? ? ? …
然后可以使用?myGameInstance.SendMessage()?向構建發(fā)送消息,或使用?myGameInstance.Module?訪問構建 Module 對象。
方案如下:
1.在Unity場景中有一個GameObject,我們命名為A,
A上有C#腳本,里面有個方法
public void Func(string str)
{
//處理邏輯
}
2.在發(fā)布出的WebGL項目index.html中用JS調用此方法
<script>
var gameInstance = UnityLoader.Instantiate("gameContainer", "Build/WebAndUnity.json",{onProgress:UnityProgress});
function testSend()
? ? ? ? {
? ? ? ? ? ? ? ? gameInstance.SendMessage("A", "Func", "string");
? ? ? ? }
</script>
需要注意的就是gameInstance,先要初始化出一個gameInstance,如上代碼,再用gameInstance調用SendMessage方法。
這段代碼的大概意思就是:web前端通過unityloader創(chuàng)建一個unity的容器實例,再通過容器實例給游戲對象A發(fā)送一個調用Func函數的消息,并且傳入一個string參數。
注:可以傳遞的參數類型:int ,string,空。
Unity項目可以打包成WebGl,打包后的項目文件:
Build中是打包后的Js代碼;
Index.html是web項目的入口,里面可以調整web的自適應,也可以拿去嵌套;
TemplateData是打包時候選的webGl模板;
web端游戲可能Unity只負責做游戲部分,而官網由另外的團隊制作,之間就需要Unity和Js代碼之間的相互調用;
Unity調用JavaScript
聲明一下,這里說的都是Unity和外部JS代碼的互相調用,項目內調用有其他方法;
老版本提供一個過時的方法:
1.在WebGL項目中的Index.html中添加要調用的JS方法
functionUnity2JavaScript() {alert("UnityToWeb") }
2.Unity中調用
Application.ExternalCall("Unity2JavaScript");//可以有參數,沒有返回值//Application.ExternalCall("Unity2JavaScript",a,10,"aaaa");
Unity建議使用的方法:
1.在Plugins文件夾中,創(chuàng)建后綴為.jslib的文件,在其中寫需要調用的js代碼
mergeInto(LibraryManager.library, {Hello:function() {window.alert("Hello, world!");? },HelloString:function(str) {window.alert(Pointer_stringify(str));? },PrintFloatArray:function(array, size) {for(vari =0; i < size; i++)console.log(HEAPF32[(array >>2) + size]);? },AddNumbers:function(x, y) {returnx + y;? },StringReturnValueFunction:function() {varreturnStr ="bla";varbuffer =_malloc(lengthBytesUTF8(returnStr) +1);writeStringToMemory(returnStr, buffer);returnbuffer;? },BindWebGLTexture:function(texture) {GLctx.bindTexture(GLctx.TEXTURE_2D,GL.textures[texture]);? },});
2.Unity中調用——__Internal.jslib
using UnityEngine;using System.Runtime.InteropServices;publicclassNewBehaviourScript:MonoBehaviour {? ? [DllImport("__Internal")]? ? privatestaticexternvoidHello();? ? [DllImport("__Internal")]? ? privatestaticexternvoidHelloString(stringstr);? ? [DllImport("__Internal")]? ? privatestaticexternvoidPrintFloatArray(float[]array,intsize);? ? [DllImport("__Internal")]? ? privatestaticexternintAddNumbers(intx,inty);? ? [DllImport("__Internal")]? ? privatestaticexternstringStringReturnValueFunction();? ? [DllImport("__Internal")]? ? privatestaticexternvoidBindWebGLTexture(inttexture);voidStart(){? ? ? ? Hello();? ? ? ? ? ? ? ? HelloString("This is a string.");float[] myArray = newfloat[10];? ? ? ? PrintFloatArray(myArray, myArray.Length);intresult = AddNumbers(5,7);? ? ? ? Debug.Log(result);? ? ? ? ? ? ? ? Debug.Log(StringReturnValueFunction());? ? ? ? ? ? ? ? var texture = new Texture2D(0,0, TextureFormat.ARGB32,false);? ? ? ? BindWebGLTexture(texture.GetNativeTextureID());? ? }}
新方法多了可以返回值,但是每次修改必須打包才能測試;
JavaScript調用Unity
這里面有巨坑,天坑,人都坑傻了?。?!
官方文檔中有這幾行字

恰好我用的2020版本的Unity;
主要使用這個API——
SendMessage("游戲對象名","方法名","參數"); 這個和參數和lua調用c#差不多了,但是怎么調用這個api就很玄學了;
首先如果你調用這個方法需要在Unity的資源已經加載完成才可以,這個好解決,js加個button;
WebToUnity
其次在調用這個方法前需要先實例化UnityInstance變量;
vargameInstance =null;script.onload=() =>{gameInstance =createUnityInstance(document.querySelector("#unity-canvas"), {dataUrl:"Build/Test.data",frameworkUrl:"Build/Test.framework.js",codeUrl:"Build/Test.wasm",streamingAssetsUrl:"StreamingAssets",companyName:"DefaultCompany",productName:"UnityToWeb",productVersion:"0.1",? ? ? });};//以上的參數都可以在unity的playersetting界面找到;
最后調用時要在then中用lamda表達式
functionTestSend() {gameInstance.then((unityInstance) =>{unityInstance.SendMessage("Canvas","OnLogin","dqwreqweraf");? ? ? ? });}
完整的index.html
? ? ? ? ? ? ? Unity WebGL Player | UnityToWeb? ? ? ? ? ? ? ?
WebToUnity? ? ? ? ? ?
? ? ? ?
? ? ? ?? ? ? ? WebGL builds are not supported on mobile devices.? ? ?
? ? ? ?
? ? ? ? ? ? ? ?? ? ? ? ? var buildUrl = "Build";? ? ? var loaderUrl = buildUrl + "/Test.loader.js";? ? ? var config = {? ? ? ? dataUrl: buildUrl + "/Test.data",? ? ? ? frameworkUrl: buildUrl + "/Test.framework.js",? ? ? ? codeUrl: buildUrl + "/Test.wasm",? ? ? ? streamingAssetsUrl: "StreamingAssets",? ? ? ? companyName: "DefaultCompany",? ? ? ? productName: "UnityToWeb",? ? ? ? productVersion: "0.1",? ? ? };? ? ? var container = document.querySelector("#unity-container");? ? ? var canvas = document.querySelector("#unity-canvas");? ? ? var loadingBar = document.querySelector("#unity-loading-bar");? ? ? var progressBarFull = document.querySelector("#unity-progress-bar-full");? ? ? var fullscreenButton = document.querySelector("#unity-fullscreen-button");? ? ? var mobileWarning = document.querySelector("#unity-mobile-warning");? ? ? // By default Unity keeps WebGL canvas render target size matched with? ? ? // the DOM size of the canvas element (scaled by window.devicePixelRatio)? ? ? // Set this to false if you want to decouple this synchronization from? ? ? // happening inside the engine, and you would instead like to size up? ? ? // the canvas DOM size and WebGL render target sizes yourself.? ? ? // config.matchWebGLToCanvasSize = false;? ? ? if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {? ? ? ? container.className = "unity-mobile";? ? ? ? // Avoid draining fillrate performance on mobile devices,? ? ? ? // and default/override low DPI mode on mobile browsers.? ? ? ? config.devicePixelRatio = 1;? ? ? ? mobileWarning.style.display = "block";? ? ? ? setTimeout(() => {? ? ? ? ? mobileWarning.style.display = "none";? ? ? ? }, 5000);? ? ? } else {? ? ? ? canvas.style.width = "960px";? ? ? ? canvas.style.height = "600px";? ? ? }? ? ? loadingBar.style.display = "block";? ? ? var script = document.createElement("script");? ? ? script.src = loaderUrl;? var gameInstance = null;? ? ? script.onload = () => {gameInstance = createUnityInstance(document.querySelector("#unity-canvas"), {? ? ? ? dataUrl: "Build/Test.data",? ? ? ? frameworkUrl: "Build/Test.framework.js",? ? ? ? codeUrl: "Build/Test.wasm",? ? ? ? streamingAssetsUrl: "StreamingAssets",? ? ? ? companyName: "DefaultCompany",? ? ? ? productName: "UnityToWeb",? ? ? ? productVersion: "0.1",? ? ? });? ? ? };? ? function TestSend() {? ? ? ? ? ? gameInstance.then((unityInstance) => {? ? ? ? ? ? unityInstance.SendMessage("Canvas","OnLogin","dqwreqweraf");? ? ? ? ? ? ? ? ? ? });? ? ? ? ? }? ? ? ? document.body.appendChild(script);? ? ?
更新新坑
Unity和JavaScript代碼互相調用會有很嚴重的時序問題,比如需要在Unity場景加載完成開始調用的登錄等方法;
不知道Unity打包成WebGL底層怎么處理的,我在單例的構造里寫初始化會被多次執(zhí)行,如果在這里調用js方法后果就是反復橫跳導致棧內存爆炸;
解決辦法是在Unity生命周期函數中調用(Awake或者Start)js函數,目的是通知加載完成,然后這個js函數中給Unity傳登錄信息(這里的登錄信息可以是js從瀏覽器cookies獲取的,unity做不到);