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