與unity通信的web大屏應用 開發(fā)總結(jié)

現(xiàn)今的可視化網(wǎng)頁也越來越趨向于炫酷風了。
這樣的

這樣的

參考:阿里云上海城市數(shù)據(jù)可視化概念設計稿
等等
果真是每個設計師都有一顆做游戲的心??。
不過設計再炫酷,前端開發(fā)所需的知識其實都是差不多的。
本文總結(jié)一下我在智慧城市項目中的前端開發(fā)經(jīng)歷。


項目開發(fā)初期,年少氣盛,秉承著萬能JS的初學者心態(tài):哼!unity能做的,那我前端也能做。
于是乎,上網(wǎng)找了一下前端三維開發(fā)的知識,了解了前端的三維開發(fā)小能手threeJs。
參考了一些測評,了解了BS端的threeJs和CS端的unity。
區(qū)別:threeJs相比于unity開發(fā)更底層,復雜需求下沒有unity開發(fā)快。
不過unity打包出來的web資源包確實很大。有時候若需求特別復雜,建模比較多,包會特別大,如果不作優(yōu)化,網(wǎng)絡下載都有一段時間。


因為是回憶型總結(jié),看代碼工程也比較大,所以總有遺落,所以本文持續(xù)更新總結(jié)至本條消失,才為更新完。


一、unity資源包

首先,介紹一下unity資源包的結(jié)構(gòu)。

這個unity資源包其實就是一個unity的web demo。

  • Build文件夾



    其中,UnityLoader.js是加載unity的壓縮文件,在項目中引入調(diào)用即可加載unity。

  • TemplateData文件夾



    這里面包含了unity展示前加載中的樣式和圖片,具體代碼在UnityProgress.js(并不復雜)中

// UnityProgress.js
// 自己加的export,這樣就可以導出一個可用的方法了
/*export */function UnityProgress(unityInstance, progress) {
  if (!unityInstance.Module)
    return;
  if (!unityInstance.logo) {
    unityInstance.logo = document.createElement("div");
    unityInstance.logo.className = "logo " + unityInstance.Module.splashScreenStyle;
    unityInstance.container.appendChild(unityInstance.logo);
  }
  if (!unityInstance.progress) {    
    unityInstance.progress = document.createElement("div");
    unityInstance.progress.className = "progress " + unityInstance.Module.splashScreenStyle;
    unityInstance.progress.empty = document.createElement("div");
    unityInstance.progress.empty.className = "empty";
    unityInstance.progress.appendChild(unityInstance.progress.empty);
    unityInstance.progress.full = document.createElement("div");
    unityInstance.progress.full.className = "full";
    unityInstance.progress.appendChild(unityInstance.progress.full);
    unityInstance.container.appendChild(unityInstance.progress);
  }
  unityInstance.progress.full.style.width = (100 * progress) + "%";
  unityInstance.progress.empty.style.width = (100 * (1 - progress)) + "%";
  if (progress == 1)
    unityInstance.logo.style.display = unityInstance.progress.style.display = "none";
}

如果想要使加載狀態(tài)更加炫酷,可以仿照此方法替換它。

  • index.html
    這是demo的入口文件,這個文件主要可以給我們參考官方的調(diào)用UnityLoader來加載unity的方法,幫助把unity加載進我們自己的項目。
// index.html
<!DOCTYPE html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Unity WebGL Player | GeoCity_NewHangZhou</title>
    <link rel="shortcut icon" href="TemplateData/favicon.ico">
    <link rel="stylesheet" href="TemplateData/style.css">
    <script src="TemplateData/UnityProgress.js"></script>
    <script src="Build/UnityLoader.js"></script>
    <script>
      var unityInstance = UnityLoader.instantiate("unityContainer", "Build/out_dv_web.json", {onProgress: UnityProgress});
    </script>
  </head>
  <body>
    <div class="webgl-content">
      <div id="unityContainer" style="width: 3840px; height: 2160px"></div>
      <div class="footer">
        <div class="webgl-logo"></div>
        <div class="fullscreen" onclick="unityInstance.SetFullscreen(1)"></div>
        <div class="title">GeoCity_NewHangZhou</div>
      </div>
    </div>
  </body>
</html>
  • pdf文檔:則是我們負責任的unity開發(fā)同事,自愿撰寫的unity和web通信的方法和接口。( ?? ω ?? )?

二、unity的基本加載方式

index.html中所示:
var unityInstance = UnityLoader.instantiate("unityContainer", "Build/out_dv_web.json", {onProgress: UnityProgress});
首先把UnityLoader.js加載進來,

<script src="<%= BASE_URL + VUE_APP_ASSETS %>/out_dv_web/Build/UnityLoader.js"></script>

再實例化頁面中指定id的元素,

<script>
  window.initUnity = function(UnityProgress) {
    return UnityLoader.instantiate(
      'gameContainer',
      '<%= BASE_URL + VUE_APP_ASSETS %>/out_dv_web/Build/out_dv_web.json',
      { onProgress: UnityProgress }
    );
  };
  window.Hls = Hls;
</script>

其中,UnityProgress可以選擇直接在入口文件中加載進來,也可以選擇在指定位置動態(tài)加載進來,也可以選擇把方法重寫在methods中等。(示例為 在指定位置動態(tài)加載后傳入?yún)?shù)實例化的方法)
這種全局方法來調(diào)用UnityLoader的方式是一種,若你的項目是三維模型貫穿始終的,這種在初始就把UnityLoader加載進來的方式是一種不錯的選擇。
加載的方式和時機有多種,下文再詳細解說。

三、與unity之間的基本通信規(guī)則

  1. web調(diào)用unity方法
接口文檔pdf

根據(jù)接口文檔所描述的,前端可以通過unity實例來調(diào)用它的方法SendMessage來操作unity。

this.gameInstance = window.initUnity(UnityProgress);
...
this.gameInstance.SendMessage('WebManager', obj.method, obj.params);
// 調(diào)用截圖中示例的方法和參數(shù)
this.gameInstance.SendMessage(
  'WebManager',
  'SendMsgToU3d_Control',
  JSON.stringify({ control: { rotatespeed: 360, zoomspeed: 600, panspeed: 100 } })
);

unity和web之間通信參數(shù)都使用字符串,具體原因期待大神解答。

  1. unity調(diào)用web方法
微信截圖_20201019164014.png

有時unity也需要回傳一些數(shù)據(jù),這時web就需要設置一些回調(diào)函數(shù)。
unity內(nèi)部可以直接調(diào)用掛載在window上的全局方法。

window.nextScene = this.nextScene;

四、避免錯誤的方法

以下情況基于我的項目,若有優(yōu)化unity,可能不會出現(xiàn),但是做一層保險總是沒錯的??。

  1. 在unity尚未加載完全時,調(diào)用unity的方法和它通信,會導致unity報錯。
    為了讓控制臺的error不再爆紅。
    解決:
mounted: {
  ...
  window.nextScene = this.nextScene;
  this.gameInstance = window.initUnity(UnityProgress);
  if (this.gameInstance) this.SendMessageTemp = this.gameInstance.SendMessage; // 存起來
  this.gameInstance.SendMessage = () => {}; // 先置為空方法,避免報錯
  ...
},
methods: {
  nextScene(str) { // web全局方法,提供給unity的調(diào)用函數(shù)。
    switch (str) { // unity傳約定的參數(shù)
      case '1': {
        // 初始化場景完成,unity調(diào)用告知web
        this.gameInstance.SendMessage = this.SendMessageTemp;
        break;
      }
    ...
  }
  ...
}

五、不同的優(yōu)化方法

  1. 隊列式調(diào)用unity方法
    我調(diào)用unity方法然后unity啟動動畫,unity有一個延時保護,以防我一瞬間調(diào)用大量方法,出現(xiàn)接口阻塞。
    所以當我用程序去一瞬間對unity作一堆操作后,unity存在可能只會執(zhí)行第一個(這個具體看unity小伙伴想怎么控制)。
    基礎解決:
    設置一個unityList調(diào)用隊列;
    當需要和unity通信時,往unityList中push有規(guī)則的對象;
    監(jiān)聽unityList的變化,間隔時間SendMessage。
data: {
  unityList: [], // 調(diào)用隊列
  unityListInterval: null, // 存放定時器,可銷毀
  ...
},
watch: {
   // 此處可以深層監(jiān)聽unityList的變化,也可以監(jiān)聽長度(每次push和pop都會導致length變化,理論上不存在unityList變化長度不變的情況)
  'unityList.length'(newVal, oldVal) {
    if (newVal > 0) {
      if (this.gameInstance && this.unityListInterval === null) { // unity實例存在且不存在定時器時
        this.unityListInterval = setInterval(() => {
          const obj = this.unityList.shift(); // 先進先出
          if (obj) {
            this.gameInstance.SendMessage('WebManager', obj.method, obj.params);
          }
          if (this.unityList.length === 0 && this.unityListInterval !== null) {
            clearInterval(this.unityListInterval);
            this.unityListInterval = null; // 必須,clear之后也不會歸空,必須手動歸空
          }
        }, 200); // 具體時長與unity開發(fā)者討論定
      }
    } else if(this.unityListInterval !== null){
      clearInterval(this.unityListInterval);
      this.unityListInterval = null; // 必須,clear之后也不會歸空,必須手動歸空
    }
  },
  ...
},
methods: {
...
this.unityList.push({
  method: 'SendMsgToU3d_EnterArea',
  params: JSON.stringify({
    data: {
      area: 12
    }
  })
});
...

特殊情況:
如果有特殊幾個方法執(zhí)行時間相對較長,可特殊處理:

this.unityListInterval = setInterval(() => {
  const obj = this.unityList[0];
  if (obj) {
    // 根據(jù)具體名字設置時間,有多個就用switch區(qū)分
    if(this.timer === null && obj.method === 'doLongTime') {
      this.timer = setTimeout(() => {
        this.unityList.shift(); // 先進先出
        this.gameInstance.SendMessage('WebManager', obj.method, obj.params);
        clearTimeout(this.timer);
        this.timer = null;
      }, 500); // 該方法的約定時長
    } else {
      this.unityList.shift();
      this.gameInstance.SendMessage('WebManager', obj.method, obj.params);
    }
  }
  if (this.unityList.length === 0 && this.unityListInterval !== null) {
    clearInterval(this.unityListInterval);
    this.unityListInterval = null; // 必須,clear之后也不會歸空,必須手動歸空
  }
}, 200); // 具體時長與unity開發(fā)者討論定

進一步也可以停掉interval定時器,減少資源消耗

data: {
  unityList: [], // 調(diào)用隊列
  unityListInterval: null, // 存放定時器,可銷毀
  timer: null,
  ...
},
watch: {
   // 此處可以深層監(jiān)聽unityList的變化,也可以監(jiān)聽長度(每次push和pop都會導致length變化,理論上不存在unityList變化長度不變的情況)
  'unityList.length'(newVal, oldVal) {
    if (newVal > 0) {
      this.setUnityListInterval();
    } else if(this.unityListInterval !== null){
      clearInterval(this.unityListInterval);
      this.unityListInterval = null; // 必須,clear之后也不會歸空,必須手動歸空
    }
  },
  ...
},
methods: {
  setUnityListInterval() {
    if (this.gameInstance && this.unityListInterval === null) { // unity實例存在且不存在定時器時
      this.unityListInterval = setInterval(() => {
        const obj = this.unityList[0];
        if (obj) {
          // 根據(jù)具體名字設置時間,有多個就用switch區(qū)分
          if (this.timer === null && obj.method === "doLongTime") {
            if (this.unityListInterval !== null) clearInterval(this.unityListInterval); // 停止interval,但不置空定時器,使unityList在此期間變化時不重新setInterval
            this.timer = setTimeout(() => {
              this.unityList.shift();
              this.gameInstance.SendMessage(
                "WebManager",
                obj.method,
                obj.params
              );
              this.timer = null;
              if (this.unityListInterval !== null) this.unityListInterval = null; // timeout定時器結(jié)束,取消阻止setInterval。
              this.setUnityListInterval(); // 對剩余的項進行操作
            }, 500); // 該方法的約定時長
          } else {
            this.unityList.shift(); // 先進先出
            this.gameInstance.SendMessage("WebManager", obj.method, obj.params);
          }
        }
        if (this.unityList.length === 0 && this.unityListInterval !== null) {
          clearInterval(this.unityListInterval);
          this.unityListInterval = null; // 必須,clear之后也不會歸空,必須手動歸空
  }
      }, 200); // 具體時長與unity開發(fā)者討論定
    }
  },
...
this.unityList.push({
  method: 'SendMsgToU3d_EnterArea',
  params: JSON.stringify({
    data: {
      area: 12
    }
  })
});
...

進階解決:
把unity調(diào)用封裝成對象調(diào)用內(nèi)部方法,后續(xù)更新。

  1. 對于不需要在一開始就加載UnityLoader.js的項目,可以動態(tài)加載UnityLoader。
    但是因為UnityLoader不會export一個對象,所以可以動態(tài)添加script標簽加載進來,再調(diào)用全局方法。
// common.js
export const loadScript = function (id, url, callback) {
  let scriptTag = document.getElementById(id);
  let headEl = document.getElementsByTagName("head")[0];
  if (scriptTag) headEl.removeChild(scriptTag); // 若已存在則刪除
  let script = document.createElement("script"); //創(chuàng)建一個script標簽
  script.id = id; // 設置id方便確保只有一個
  script.type = "text/javascript";
  if (typeof callback !== "undefined") {
    if (script.readyState) { // 若不存在監(jiān)聽load,則無法保證對引入變量的操作成功
      script.onreadystatechange = function () {
        if (
          script.readyState === "loaded" ||
          script.readyState === "complete"
        ) {
          script.onreadystatechange = null;
          callback();
        }
      };
    } else {
      script.onload = function () {
        callback();
      };
    }
  }
  script.src = url;
  headEl.appendChild(script);
};
// Home.vue
import { loasScript } from '@/common.js';
...
loadScript('unityScript', '@/.../UnityLoader.js', function() {
  console.log(UnityLoader) // 輸出正確
  // 對UnityLoader作操作
})
console.log(UnityLoader) // 報錯undefined
...
打印結(jié)果
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容