美文网首页
前端实现与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