美文网首页
与unity通信的web大屏应用 开发总结

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

作者: xuelulu | 来源:发表于2020-10-19 18:29 被阅读0次

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

    这样的

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


    项目开发初期,年少气盛,秉承着万能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
    ...
    
    打印结果

    相关文章

      网友评论

          本文标题:与unity通信的web大屏应用 开发总结

          本文链接:https://www.haomeiwen.com/subject/gtjkmktx.html