与unity通信的web大屏应用 开发总结

现今的可视化网页也越来越趋向于炫酷风了。
这样的

这样的

参考:阿里云上海城市数据可视化概念设计稿
等等
果真是每个设计师都有一颗做游戏的心🐮。
不过设计再炫酷,前端开发所需的知识其实都是差不多的。
本文总结一下我在智慧城市项目中的前端开发经历。


项目开发初期,年少气盛,秉承着万能JS的初学者心态:哼!unity能做的,那我前端也能做。
于是乎,上网找了一下前端三维开发的知识,了解了前端的三维开发小能手threeJs。
参考了一些测评,了解了BS端的threeJs和CS端的unity。
区别:threeJs相比于unity开发更底层,复杂需求下没有unity开发快。
不过unity打包出来的web资源包确实很大。有时候若需求特别复杂,建模比较多,包会特别大,如果不作优化,网络下载都有一段时间。


因为是回忆型总结,看代码工程也比较大,所以总有遗落,所以本文持续更新总结至本条消失,才为更新完。


一、unity资源包

首先,介绍一下unity资源包的结构。

这个unity资源包其实就是一个unity的web demo。

  • Build文件夹



    其中,UnityLoader.js是加载unity的压缩文件,在项目中引入调用即可加载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";
}

如果想要使加载状态更加炫酷,可以仿照此方法替换它。

  • index.html
    这是demo的入口文件,这个文件主要可以给我们参考官方的调用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开发同事,自愿撰写的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可以选择直接在入口文件中加载进来,也可以选择在指定位置动态加载进来,也可以选择把方法重写在methods中等。(示例为 在指定位置动态加载后传入参数实例化的方法)
这种全局方法来调用UnityLoader的方式是一种,若你的项目是三维模型贯穿始终的,这种在初始就把UnityLoader加载进来的方式是一种不错的选择。
加载的方式和时机有多种,下文再详细解说。

三、与unity之间的基本通信规则

  1. web调用unity方法
接口文档pdf

根据接口文档所描述的,前端可以通过unity实例来调用它的方法SendMessage来操作unity。

this.gameInstance = window.initUnity(UnityProgress);
...
this.gameInstance.SendMessage('WebManager', obj.method, obj.params);
// 调用截图中示例的方法和参数
this.gameInstance.SendMessage(
  'WebManager',
  'SendMsgToU3d_Control',
  JSON.stringify({ control: { rotatespeed: 360, zoomspeed: 600, panspeed: 100 } })
);

unity和web之间通信参数都使用字符串,具体原因期待大神解答。

  1. unity调用web方法
微信截图_20201019164014.png

有时unity也需要回传一些数据,这时web就需要设置一些回调函数。
unity内部可以直接调用挂载在window上的全局方法。

window.nextScene = this.nextScene;

四、避免错误的方法

以下情况基于我的项目,若有优化unity,可能不会出现,但是做一层保险总是没错的😉。

  1. 在unity尚未加载完全时,调用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的调用函数。
    switch (str) { // unity传约定的参数
      case '1': {
        // 初始化场景完成,unity调用告知web
        this.gameInstance.SendMessage = this.SendMessageTemp;
        break;
      }
    ...
  }
  ...
}

五、不同的优化方法

  1. 队列式调用unity方法
    我调用unity方法然后unity启动动画,unity有一个延时保护,以防我一瞬间调用大量方法,出现接口阻塞。
    所以当我用程序去一瞬间对unity作一堆操作后,unity存在可能只会执行第一个(这个具体看unity小伙伴想怎么控制)。
    基础解决:
    设置一个unityList调用队列;
    当需要和unity通信时,往unityList中push有规则的对象;
    监听unityList的变化,间隔时间SendMessage。
data: {
  unityList: [], // 调用队列
  unityListInterval: null, // 存放定时器,可销毁
  ...
},
watch: {
   // 此处可以深层监听unityList的变化,也可以监听长度(每次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开发者讨论定
      }
    } 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
    }
  })
});
...

特殊情况:
如果有特殊几个方法执行时间相对较长,可特殊处理:

this.unityListInterval = setInterval(() => {
  const obj = this.unityList[0];
  if (obj) {
    // 根据具体名字设置时间,有多个就用switch区分
    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开发者讨论定

进一步也可以停掉interval定时器,减少资源消耗

data: {
  unityList: [], // 调用队列
  unityListInterval: null, // 存放定时器,可销毁
  timer: null,
  ...
},
watch: {
   // 此处可以深层监听unityList的变化,也可以监听长度(每次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) {
          // 根据具体名字设置时间,有多个就用switch区分
          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定时器结束,取消阻止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开发者讨论定
    }
  },
...
this.unityList.push({
  method: 'SendMsgToU3d_EnterArea',
  params: JSON.stringify({
    data: {
      area: 12
    }
  })
});
...

进阶解决:
把unity调用封装成对象调用内部方法,后续更新。

  1. 对于不需要在一开始就加载UnityLoader.js的项目,可以动态加载UnityLoader。
    但是因为UnityLoader不会export一个对象,所以可以动态添加script标签加载进来,再调用全局方法。
// 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"); //创建一个script标签
  script.id = id; // 设置id方便确保只有一个
  script.type = "text/javascript";
  if (typeof callback !== "undefined") {
    if (script.readyState) { // 若不存在监听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
...
打印结果
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342