前端实现与ue4通信的游戏手柄demo

最近公司的vr项目换成了ue4引擎来搭建场景, 老大布置任务: 在手机上实现一个虚拟手柄来与场景通信, 于是便有了这个预研的测试demo, 这个demo实现了1.可控帧率,满足游戏需求, 2.摇杆控制,按住会持续发送消息,3. 双指缩放事件与 UE4 同步, 编写使用的前端库是nipplejs
演示地址: gusuziyi.github.io/rockerforue4/,
仓库地址:https://github.com/gusuziyi/rockerForUE4.git
简单的手柄设置可以参考官网 https://yoannmoi.net/nipplejs/#demo

用官网的demo跑起来之后, 还有一些实际应用的问题要解决:

可控帧率

由于摇杆在使用时产生的数据过多, 不能不加处理全部发给服务器, 会造成渲染引擎卡死, 这时候要使用定时器做基本的节流优化.

关于节流的知识点,请先戳这里: 浅谈js防抖和节流

  • 这里我在通信入口统一设置一个定时器, 并根据帧率frame算出发送间隔Math.round(1000 / frame)
 //时间校准, 通信函数入口
    timerControl(data) {
      //发送队列不为空,拒绝发送并等待发送完成
      if (this.timer) {
        return;
      }
      //第一次发送,直接发送
      if (!this.oldTime) {
        this.oldTime = +new Date();
        return this.beforeSend(0, data);
      }
      //发送间隔未达标,执行节流
      const now = +new Date();
      if (now - this.oldTime < this.intervalTime) {
        return this.beforeSend(this.intervalTime + this.oldTime - now, data);
      }
     //其他情况,直接发送
      return this.beforeSend(0, data);
    },
  • 而beforeSend函数则是一个根据节流函数改变的帧率控制函数,主要是利在发送之后生成一个this.timer, 然后在timerControl函数中判断 此变量来达到节流的目的
   // 帧率控制
    beforeSend(time, data) {
      if (time === 0) {
        this.msg = noticeServer(this.operaterCMD, data);
        this.timer = setTimeout(() => {
          clearTimeout(this.timer);
          this.timer = null;
        }, this.intervalTime);
      } else {
        this.timer = setTimeout(() => {
          this.msg = noticeServer(this.operaterCMD, { X, Y });
          clearTimeout(this.timer);
          this.timer = null;
        }, time);
      }
    }
  • 服务端的插值算法
    前端节流之后, 在服务端也要做相应的插值算法, 通过缓存一帧的方式, 来进一步节流,这里给出一个示例demo
    // 插值算法,保证不卡顿
    insertValueA(y, x) {
    // 第一帧,缓存下来,不绘制
      if (!this.topA || !this.leftA) {
        this.topA = this.oldTopA + "px";
        this.leftA = this.oldLeftA + "px";
        return;
      }
    // 下一帧, 分9次绘制出下一帧与上一帧的变化量,这里的n=9要根据前后端的帧率协定来控制
      let n = 9;
      let stepY = (y - this.oldTopA) / n;
      let stepX = (x - this.oldLeftA) / n;
      let insertValueATimer = setInterval(() => {
        this.topA = +this.topA.slice(0, this.topA.indexOf("px")) + stepY + "px";
        this.leftA =  +this.leftA.slice(0, this.leftA.indexOf("px")) + stepX + "px";
        n--;
        if (n === 0) {
          clearInterval(insertValueATimer);
          insertValueATimer = null;
        }
      }, 20);
    // 缓存下一帧
      this.oldTopA = y;
      this.oldLeftA = x;
    },
缩放手势监听
  • 缩放手势在安卓为touch事件,在IOS上为gesture事件,所以在注册监听时,要同时注册两个事件
  • 由于缩放是至少2个手指才能完成的动作, 所以在start中要监听到两个以上的点才触发,注意start中不要写e.preventDefault(),这会导致单指点击按钮失效
  • 由于在手指缩放时要屏蔽其他动作,所以要在缩放时为事件添加一个开关istouch, 在start时,开启,在end时关闭, 若在缩放时有其他手指事件被触发, 只需判断istouch的状态即可
 const that = this;
 ['touchstart', 'gesturestart'].forEach(i => {
        document.addEventListener(
          i,
          function(e) {
            if (e.touches.length >= 2) {
              that.istouch = true;
              start = e.touches; // 得到第一组两个点
            }
          },
          { passive: false }
        );
      });
  • 获取是放大还是缩小指令, 要根据每次手指移动后两个触点的长度来判断
    完整的手指缩放监听函数:
    //双指缩放
    setGesture() {
      const that = this;
      function getDistance(p1, p2) {
        const x = p2.pageX - p1.pageX;
        const y = p2.pageY - p1.pageY;
        return Math.sqrt(x * x + y * y);
      }
      let start = [];
      ['touchstart', 'gesturestart'].forEach(i => {
        document.addEventListener(
          i,
          function(e) {
            if (e.touches.length >= 2) {
              that.istouch = true;
              start = e.touches; // 得到第一组两个点
            }
          },
          { passive: false }
        );
      });
      ['touchmove', 'gesturemove'].forEach(i => {
        document.addEventListener(
          i,
          function(e) {
            e.preventDefault();
            if (e.touches.length >= 2 && that.istouch) {
              const now = e.touches; // 得到第二组两个点
              const scale =
                getDistance(now[0], now[1]) / getDistance(start[0], start[1]);
              that.operaterCMD = 'scale';
              that.timerControl({ scale: scale.toFixed(2) });
            }
          },
          { passive: false }
        );
      });
      ['touchend', 'gestureend'].forEach(i => {
        document.addEventListener(
          i,
          function() {
            if (that.istouch) {
              that.istouch = false;
            }
          },
          { passive: false }
        );
      });
    }
摇杆按住持续发送指令
  • 在nipplejs 中翻遍文档和issue, 发现只有摇杆移动事件,并没有按住持续发送指令的功能,所以只能自己实现
  • 在摇杆start和end事件中为摇杆添加一个active状态,类似以下伪代码:
       this[摇杆.name]
          .on('start', () => {
            this[摇杆.name].active = true;
          })
          .on('move', this.onMove)
          .on('end', () => {
            this[摇杆.name].active = false;
          });
  • 然后为摇杆添加一个持续按住的定时器Interval, 然后在摇杆的move事件中不断调用并重新赋值,一旦move事件结束,则该定时器会持续触发,此时将定时器与摇杆的active状态绑定,即可实现松开摇杆时取消事件发送.也就间接实现了摇杆按住持续发送指令功能
    //摇杆移动
    onMove(e, data, m) {
      this.rockerKeepPress();
      const X = +data.vector.x.toFixed(2);
      const Y = +data.vector.y.toFixed(2);
      this.cachePosition = [X, Y];
      this.timerControl({ X, Y });
    },
    //摇杆持续按住
    rockerKeepPress() {
      if (this.onPressTimer) {
        clearInterval(this.onPressTimer);
      }
      this.onPressTimer = setInterval(() => {
        if (this[摇杆.name].active) {
          this.timerControl({
            X: this.cachePosition[0],
            Y: this.cachePosition[1],
          });
        } else {
          clearInterval(this.onPressTimer);
          this.onPressTimer = null;
        }
      }, this.intervalTime);
    },
最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容