美文网首页
前端实现与ue4通信的游戏手柄demo

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

作者: 千茉紫依 | 来源:发表于2021-01-24 12:30 被阅读0次

最近公司的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);
    },

相关文章

网友评论

      本文标题:前端实现与ue4通信的游戏手柄demo

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