今天測試了一下發布 WebGL 的過程, 通過 Unity3D 創建, 相當麻煩, 它不僅對API有限制, 對測試Debug有限制, 也對服務器有要求, 並且現在的瀏覽器都很注重安全策略, 這些都增加了復雜度...
流程大概如下:
1. 做個簡單場景, 放到 BuildSettings 里面去
2. 如果有代碼, 檢查是不是有不能使用的API或是引用不能用的命名空間, 比如 System.Threading 這些, 即使引用了打包也不報錯, 然后發布之后運行拋異常, 它就不動了.
3. Build 出來的工程不能直接拖到瀏覽器運行, 360 / Firefox / Google Chrome 試過了都不讓運行, 安全策略的問題
4. 打開IIS服務, 創建本地服務器, 把生成的WebGL的工程拖進去, 綁定端口
5. 添加Web.config文件, 添加各種文件流支持, 要不然瀏覽器會報Unexpected Token錯誤
6. 使用各種瀏覽器直接 localhost:端口 打開都沒有問題
最簡單的工程坑還是挺多, 按順序看下來:
2.1 WebGL多線程不能用, 所以Threading有關都不能用.
2.2 部署在服務器上, 所以文件讀寫都不能用, StreamingAssets的地址在本地變成了 [ http:/localhost:61281/StreamingAssets ] , 所以只能老實用WebRequest來進行下載了
2.3 Resources文件夾下的東西還是能正常讀取, 它的資源應該是會在加載時就全部下載了, 所以很大的話基本沒有用戶體驗了, 不過小工程還是能用
5.1 沒有Web.config的話似乎任何傳輸都不正確, 就是資源 跨域/傳輸 之類的問題了
其實還有很多問題, 中文輸入法跟隨啊, Shader啊.......
先從搭建IIS開始:
必須用服務器, 先打開本地的IIS服務, win10比win7快了至少10倍:
啟動完成后繼續打開管理工具, 可以設置IIS了:
在設置中設置本地硬盤映射, 直接設置到WebGL的輸出目錄:
添加一個綁定端口, 免得多個地址沖突:
設置好了之后, 需要在根目錄添加 Web.config 文件支持資源類型文件傳輸:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <staticContent> <remove fileExtension=".mem" /> <remove fileExtension=".data" /> <remove fileExtension=".unity3d" /> <remove fileExtension=".jsbr" /> <remove fileExtension=".membr" /> <remove fileExtension=".databr" /> <remove fileExtension=".unity3dbr" /> <remove fileExtension=".jsgz" /> <remove fileExtension=".memgz" /> <remove fileExtension=".datagz" /> <remove fileExtension=".unity3dgz" /> <remove fileExtension=".json" /> <remove fileExtension=".unityweb" /> <remove fileExtension=".obj" /> <remove fileExtension=".mjs" /> <remove fileExtension="." /> <remove fileExtension=".assetbundle" /> <mimeMap fileExtension=".mem" mimeType="application/octet-stream" /> <mimeMap fileExtension=".data" mimeType="application/octet-stream" /> <mimeMap fileExtension=".unity3d" mimeType="application/octet-stream" /> <mimeMap fileExtension=".jsbr" mimeType="application/octet-stream" /> <mimeMap fileExtension=".membr" mimeType="application/octet-stream" /> <mimeMap fileExtension=".databr" mimeType="application/octet-stream" /> <mimeMap fileExtension=".unity3dbr" mimeType="application/octet-stream" /> <mimeMap fileExtension=".jsgz" mimeType="application/x-javascript; charset=UTF-8" /> <mimeMap fileExtension=".memgz" mimeType="application/octet-stream" /> <mimeMap fileExtension=".datagz" mimeType="application/octet-stream" /> <mimeMap fileExtension=".unity3dgz" mimeType="application/octet-stream" /> <mimeMap fileExtension=".json" mimeType="application/json; charset=UTF-8" /> <mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" /> <mimeMap fileExtension=".obj" mimeType="application/octet-stream" /> <mimeMap fileExtension=".mjs" mimeType="text/javascript; charset=UTF-8" /> <mimeMap fileExtension="." mimeType="application/octet-stream" /> <mimeMap fileExtension=".assetbundle" mimeType="application/octet-stream" /> </staticContent> </system.webServer> </configuration>
我猜測在創建 Stream 的時候服務器會指定類型, 沒有指定的就不知道怎么傳了, 一般默認肯定是二進制傳吧, 怎么這么無聊...
這里有個特殊的就是無后綴的文件, 直接 "." 代表即可. "application/octet-stream" 就是二進制了吧...
這樣就能在本地瀏覽器打開了, 放在 Resources 中的 Txt 資源文件可以正常讀取:
text2.text = Resources.Load<TextAsset>("Test").text; // Test Resources
放在 StreamingAssets 下的 Txt 文件不能通過IO讀取, 使用 UnityWebRequest 進行獲取, 讀取地址經過 System.Uri 加工:
private void Start() { StartCoroutine(GetData(Application.streamingAssetsPath + "/Test.txt", (_handle) => { text1.text = _handle.text; // Test streamingAssetsPath })); } IEnumerator GetData(string loadPath, System.Action<DownloadHandler> succ) { var uri = new System.Uri(loadPath); UnityWebRequest www = UnityWebRequest.Get(uri.ToString()); yield return www.SendWebRequest(); if(www.isNetworkError || www.isHttpError) { Debug.Log(www.error); } else { Debug.Log(www.downloadHandler.text); succ.Invoke(www.downloadHandler); } }
不過如果在Build的時候選擇 [ Build & Run ] 的話, 它會臨時起一個服務器來跑打包出來的工程, 不需要IIS也是可以的......
(2020.07.22)
最近想要測試一下資源遠程加載的方案, 於是把加載邏輯添加了通過 UnityWebRequest 方式獲取 AssetBundle 的邏輯, 然而這又是一個坑, 在編輯器下, 可以從網站上獲取到 AssetBundle, 可是如果發布到服務器上, 並且資源在其它服務器, 就產生了一個跨域問題, 然后資源是無法傳輸的, 雖然理解這是 http 服務器設計的問題, 可是我網上查了半天也沒解決, 真是神奇了...
首先把打出的包放到服務器上 :
然后使用服務器的路徑來下載包 :
看到 Sprite 的 AssetBundle 可以正確通過網址下載來, 並且正確加載出來了, 可是發布到服務器后 (資源服務器 localhost:12354, 運行服務器 localhost:44599), 因為跨域問題, 無法獲取了 :
XMLHttpRequest cannot load http://localhost:12354/unityassets/AssetBundleManifest. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:44599' is therefore not allowed access.
然后找跨域的相關描述, 是需要服務器添加一個 Access-Control-Allow-Origin 相關的返回?
在360瀏覽器里面可以查看到各個 http 請求的信息, F12 -> NetWork -> XHR -> xxxxx
找到很多論壇都說添加一個 customheader 就行了, 下面這樣 (https://enable-cors.org/server_iis7.html) :
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <httpProtocol> <customHeaders> <add name="Access-Control-Allow-Origin" value="*" /> </customHeaders> </httpProtocol> </system.webServer> </configuration>
那么加到原有的 Web.config 文件里 :
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <httpProtocol> <customHeaders> <add name="Access-Control-Allow-Origin" value="*" /> </customHeaders> </httpProtocol> <staticContent> <remove fileExtension=".mem" /> <remove fileExtension=".data" /> <remove fileExtension=".unity3d" /> <remove fileExtension=".jsbr" /> <remove fileExtension=".membr" /> <remove fileExtension=".databr" /> <remove fileExtension=".unity3dbr" /> <remove fileExtension=".jsgz" /> <remove fileExtension=".memgz" /> <remove fileExtension=".datagz" /> <remove fileExtension=".unity3dgz" /> <remove fileExtension=".json" /> <remove fileExtension=".unityweb" /> <remove fileExtension=".obj" /> <remove fileExtension=".mjs" /> <remove fileExtension="." /> <remove fileExtension=".assetbundle" /> <mimeMap fileExtension=".mem" mimeType="application/octet-stream" /> <mimeMap fileExtension=".data" mimeType="application/octet-stream" /> <mimeMap fileExtension=".unity3d" mimeType="application/octet-stream" /> <mimeMap fileExtension=".jsbr" mimeType="application/octet-stream" /> <mimeMap fileExtension=".membr" mimeType="application/octet-stream" /> <mimeMap fileExtension=".databr" mimeType="application/octet-stream" /> <mimeMap fileExtension=".unity3dbr" mimeType="application/octet-stream" /> <mimeMap fileExtension=".jsgz" mimeType="application/x-javascript; charset=UTF-8" /> <mimeMap fileExtension=".memgz" mimeType="application/octet-stream" /> <mimeMap fileExtension=".datagz" mimeType="application/octet-stream" /> <mimeMap fileExtension=".unity3dgz" mimeType="application/octet-stream" /> <mimeMap fileExtension=".json" mimeType="application/json; charset=UTF-8" /> <mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" /> <mimeMap fileExtension=".obj" mimeType="application/octet-stream" /> <mimeMap fileExtension=".mjs" mimeType="text/javascript; charset=UTF-8" /> <mimeMap fileExtension="." mimeType="application/octet-stream" /> <mimeMap fileExtension=".assetbundle" mimeType="application/octet-stream" /> </staticContent> </system.webServer> </configuration>
還是報錯, 我就納悶了, 然后有些人在 Unity 請求代碼里面添加了一些頭, 我也添加之后測試仍然報錯, 無用 :
var unityWebRequest = UnityWebRequest.GetAssetBundle(url, 0); unityWebRequest.SetRequestHeader("Access-Control-Allow-Credentials", "true"); unityWebRequest.SetRequestHeader("Access-Control-Allow-Headers", "Accept, X-Access-Token, X-Application-Name, X-Request-Sent-Time"); unityWebRequest.SetRequestHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); unityWebRequest.SetRequestHeader("Access-Control-Allow-Origin", "*"); var request = unityWebRequest.SendWebRequest(); // ...
換了個錯誤再來一遍, 變成了認證錯誤之類的....
繼續再找, 看到一個說 IIS 跨域還要安裝一個 CORS Module 的東西的 :
I had a similar issue recently. Most tutorial/documentation only suggests adding custom headers in the configuration. But this does not tell IIS to handle the CORS Pre-flight request by itself. To do so, you must install the CORS Module in IIS and add some configuration in the web.config file, as explained here: IIS CORS module Configuration Reference
好吧, 進入微軟找 CORS Module (https://www.iis.net/downloads/microsoft/iis-cors-module), 下載安裝之后, 再打包一次, Unity 代碼也使用最簡單的看看 :
public void SendWebRequest() { if(request == null) { var unityWebRequest = this.hash.HasValue ? UnityWebRequest.GetAssetBundle(url, hash.Value, 0) : UnityWebRequest.GetAssetBundle(url, 0); request = unityWebRequest.SendWebRequest(); request.completed += OnLoaded; } }
結果居然可以讀取了, 反正不知道是不是安裝了 CORS Module, 能用就行了 :
沒想到部署個 WebGL 測試也這么多幺蛾子, 這些服務器就不能給個省心的邏輯嗎, 要啥功能給個界面式的功能列表也好啊, 如果明天用阿帕奇服務器, 又是查資料查半天, 心累...
然后看一下各個默認文件夾在運行時的位置 :
Debug.Log("Application.dataPath : " + Application.dataPath); // http://localhost:44599 Debug.Log("Application.persistentDataPath : " + Application.persistentDataPath); // /idbfs/9ed0d20bc957a25b21e872bbf1c2f671 Debug.Log("Application.streamingAssetsPath : " + Application.streamingAssetsPath); // http://localhost:44599/StreamingAssets Debug.Log("Application.temporaryCachePath : " + Application.temporaryCachePath); // /tmp Debug.Log("Caching.currentCacheForWriting.path : " + Caching.currentCacheForWriting.path); // /idbfs/9ed0d20bc957a25b21e872bbf1c2f671/UnityCache/Shared Debug.Log("System.Environment.CurrentDirectory : " + System.Environment.CurrentDirectory); // /
Application.dataPath 和 Application.streamingAssetsPath 是服務器的相對路徑, 所以服務器資源可以正常讀取, 而其它的應該都是本地路徑, 至於這個路徑在哪, 可能是瀏覽器的緩存路徑, 我用360瀏覽器, 直接到下面去找看看 :
我看論壇他們是說用的 IndexedDB 存儲臨時文件的, 不知道用什么開來看...
發現瀏覽器自帶了查看器的樣子, F12 -> Appliction -> IndexedDB -> xxxx
不知道這些是不是本地緩存, 我在初始界面就有一個加載代碼 :
void Start() { // 屏幕右上角 AssetBundleMaster.Core.ABM_ResourceLoader.Instance.LoadAsync<Sprite>("Sprites/Pic002PNG", (_pic) => { image.sprite = _pic; Debug.Log("Loaded Sprite " + _pic + " : " + Time.frameCount); }); // 屏幕左下角 rawImage.texture = AssetBundleMaster.Core.ABM_ResourceLoader.Instance.Load<Texture2D>("Textures/Pic001"); Debug.Log("Loaded Texture2D " + rawImage.texture + " : " + Time.frameCount); }
看看刪除 IndexedDB 相關文件夾后第一次運行的情況, 因為屏幕左下角的讀取使用的是同步讀取 (UnityWebRequest 發送請求后馬上返回) :
這樣結果左下角是肯定沒有圖片的, 因為 UnityWebRequest 是遠程請求, 必定不能馬上得到結果...
這時候發起的資源請求有3個 (使用Hash128作為參數的請求) :
assetBundleCreateRequest = UnityWebRequest.GetAssetBundle(loadPath, assetBundleManifest.GetAssetBundleHash(this.assetName), 0).SendWebRequest();
[RuntimeInitializeOnLoadMethod] private static void StartUpRun() { AssetBundleMaster.Core.ABM_AssetLoadManager.Instance.OnModuleLoaded(() => { Debug.Log("OnModuleLoaded At : " + Time.frameCount); AssetBundleMaster.Core.ABM_SceneLoader.Instance.LoadScene("Scenes/S1"); }); }
1. 場景 : s1.assetbundle
2. 右上角圖片 : common.assetbundle
3. 左下角圖片 : pic001.assetbundle -- 因為是異步請求, 同步回調沒有獲得圖片, 可是也進行了下載
這時看到 IndexedDB 中顯示的也是這三個文件 :
然后我關閉瀏覽器, 從新再打開網址 :
再打開一次的話, 異步加載的左下角圖片, 居然在同步回調里面就能獲取圖片了, 這難道就是本地緩存的威力嗎? ( 2020.07.28 -- 經測試並不一定能在同步回調中返回, 應該跟IO速度有關, 並不是進行了同步加載 ). 我再點擊一下按鈕加載一張新的圖片覆蓋左下角, 看看 IndexedDB中是不是有了新的圖片了 :
左下角的圖片改變了, 看看本地緩存 :
資源變成4個了, 關閉瀏覽器從新加載網頁看看, 如果不是本地緩存的話, 不操作應該還是只會加載3個資源.
重新加載后還是4個資源, 說明 FILE_DATA 這個數據庫這就是本地緩存無疑了. 當我們的 AssetBundleManifest 里面獲取的 Hash128 跟本地不一樣的時候, 就會去下載最新包了. 當然如果是 WebGL 的話 AssetBundleManifest 直接放服務器的 StreamingAssets 文件夾下就行了, 而資源一般會放到CDN服務器上, 所以前面搞了半天跨域的問題. 當然遠程資源+緩存的模式, 也能作為PC, Android, IOS之類的平台加載邏輯也是可行的, 並且有 Cache.expirationDelay 這些自動刪除邏輯在, 用不到的緩存資源能自動刪除, 省了更新刪除邏輯了...
測試一下看看, 給另一個按鈕添加清除緩存的功能 :
clearBtn.onClick.AddListener(() => { if(Caching.ClearCache()) { Debug.Log("ClearCache Succ"); } else { Debug.Log("ClearCache FAILED"); } });
ClearCache FAILED ......
清除緩存失敗, 這又是什么神操作, 找了下資料, 說要 Unload 掉所有已經加載的 AssetBundle 之后才能行, 修改代碼來硬核一點的試試 :
void Start() { clearBtn.onClick.AddListener(() => { StartCoroutine(ClearCache()); }); } IEnumerator ClearCache() { AssetBundle.UnloadAllAssetBundles(true); yield return new WaitForSeconds(1.0f); System.GC.Collect(0); yield return new WaitForSeconds(1.0f); yield return Resources.UnloadUnusedAssets(); yield return new WaitForSeconds(1.0f); if(Caching.ClearCache()) { Debug.Log("ClearCache Succ"); } else { Debug.Log("ClearCache FAILED"); } }
居然還是不行!!! 震驚!! 刪除了 AssetBundle 之后, UI 都變黑了 :
這就無語了, 雖然不是很大問題......
(2020.11.19)
補充一下在Windows下的緩存測試, 緩存路徑在 C:\Users\XXXX\AppData\LocalLow\Unity\DefaultCompany_工程名稱 文件夾下, 設定了 expirationDelay 之后, 需要程序下載對象, 然后超過過期時間程序還在運行, 它才能幫你刪除對應的緩存(在程序退出的時候), 也就是一個運行期間的功能, 不是 Cookie 那樣通過系統維護的東西. 也就是說運行時間需要超過設定時間, 才會刪除緩存, 非常搞笑......
(2020.07.28)
今天才發現 IndexedDB 其實就是鍵值對, 里面存儲的數據能夠跟 AssetBundle 對應得上 :
今天新打的包在瀏覽器的緩存中查看, 它的 __data 大小跟服務器上的原始文件 s1.assetbundle 大小是一樣的 :
這樣就知道它在瀏覽器中的緩存是以 [/idbfs/用戶ID/UnityCache/Shared/文件名/文件哈希值/__data] 的key來做存儲的, 還不知道它的用戶ID是怎樣來的, 不過默認路徑根目錄可以通過 Caching.currentCacheForWriting.path 來獲得, 得到的是 :
/idbfs/bc9cc5aa7eef26ca01726eef3f44ae31/UnityCache/Shared
這個路徑, 如果可以調用 IndexedDB 的API的話, 也是可以自己查詢的...