移动端NES网页模拟器(2)
前言
前面的章节已经封装了一个NES的虚拟按钮,这个章节来封装他的方向键。
在一些NES网页网页模拟器中,方向键要么使用按钮模式,要么使用摇杆模式,各有不足。例如按钮模式无法滑动,用户点了半天才知道点空了。而且无法很难做出 '左上' '右上' 这种操作。而用摇杆模式的不多,依旧没有8键模式,而且小问题也不少。而git上面的开源项目基本上没有注释,新手用起来不顺手。这个章节旨在封装一个 "我自认为好用的摇杆"。
他有以下几个特点:
(1)能点击,能滑动
(2)支持8键模式
(3)傻瓜式配置,用户只需提供容器标识,按键码,相关回调即可
(4)文档详细
1.nipplejs探秘
nippleJS 是一个 JavaScript 库,用来创建虚拟摇杆触摸功能的接口。但是他的文档少的可伶,这里将使用心得和图解一并贴上。
代码以及心得:
<script src="./js/nipplejs.min.js"></script>
<script>
//1.传入容器并将模式设为static后,插件会为容器插入一个元素nipple_x_x,这个元素,这个元素宽高为0,使用绝对定位(方便用户定位到正中心),默认在容器的左上角
//2.nipple_x_x这个元素有2个子元素,分别是back和front,使用绝对定位,水平垂直居中,白色背景。但是它的父元素nipple_x_x宽高为0,导致它们的中心点默认在用户容器的左上角
//3.back就是摇杆的背景容器,宽高默认100*100,摇杆能滑动多远取决于它
//4.front是摇杆指示器,宽高默认50*50
//5.为了能将摇杆置于用户容器的中心,必须为摇杆容器nipple_x_x元素设置新的样式,让他在用户容器中水平垂直居中。
//6.为了完成5的目的,将用户容器设为相对定位,并在传入的opt中为摇杆容器设置定位信息
var opts = {
zone:document.querySelector('#user_direction_box'), //用户设置的容器
mode: 'static',//模式,static就是摇杆中心固定
position:{left:'50%',top:'50%'},//让摇杆容器定位到用户设置容器的正中心
color:"red",//摇杆的颜色 包括back和front 默认白色,通过背景色实现
size:200,//摇杆容器back元素的大小,front是他的一半
}
var manager = nipplejs.create(opts)
//7.摇杆实例提供三个事件回调,分别是start/move/end
manager.on('start',function(evt, data){
console.log('当前事件为start 触发时间为 ' + new Date().getTime())
//console.log(evt)
//console.log(data)
//data.position属性是摇杆容器中心点相对于屏幕的位置信息,在static模式下,该值是固定的
//frontPosition属性是当前触点相对于摇杆中心点的位置信息,最大距离就是back容器的大小
//start事件每次点击只执行一次
//从打印的data信息来看,此事件无法得知用户点击时相对于摇杆中心点的具体方向
})
manager.on('move',function(evt, data){
//当用户点击摇杆时,虽未发生滑动,但也会触发当前事件 比start事件晚了2-4ms
console.log('当前事件为move 触发时间为 ' + new Date().getTime())
//console.log(evt)
console.log(data.distance)
console.log(data.direction)
//data.angle记录了当前触点方向于水平向右方向之间的角度信息,以逆时针方向增大。radian表示弧度,degree表示角度。右=0 上=90 左=180 下=270
//如果想要8个方向的方向键,可以在此封装
//data.distance属性记录了当前事件发生时摇杆的滑动距离 最大值为为back容器的大小
//data.direction属性记录了当前摇杆操作的方向信息 {x: "right", y: "down", angle: "down"} 其中angle为主方向,只能是4个方向
//如果data.distance过小,将不会有direction属性,应该是有一个临界值(估计是摇杆容器back的5%),超过这个值才算 ‘点击了方向键’,反过来说,没有direction属性的话此次操作无效
//data.position属性记录当前摇杆位置相对于屏幕的位置信息(摇杆的滑动幅度要小于等于手势滑动幅度。因为摇杆被限制在back容器中)
//data.force记录了触点距离摇杆中心绝对距离的信息 以back容器的大小为1个单位
//摇杆在滑动过程中,这个事件还会持续触发
})
//这个事件肯定是手指释放才触发
manager.on('end',function(evt, data){
//用户手速再快,相比start事件,此事件再快也需要50ms后才能触发
console.log('当前事件为end 触发时间为 ' + new Date().getTime())
//console.log(evt)
//console.log(data.position)
//frontPosition属性是当前触点相对于摇杆中心点的位置信息,最大距离就是back容器的大小
//data.position属性是摇杆容器中心点相对于屏幕的位置信息,在static模式下,该值是固定的
})
//鉴于start事件无法获取方向信息,建议使用move事件来监听用户点击
//根据用户传入的配置信息决定4键还是8键模式
//如果需要的是4键方向,则直接使用move事件的data.angle属性
//如果需要的是8键方向,则需要对move事件中data.angle.degree的角度值进行判定,输出最终的方向
//如果滑动过程中方向发生改变,则需要取消相应的按键操作,所以要对上一次方向进行记录,才能与最新的方向进行比较,比较完更新这个信息,当手势释放时重新设为null
//按键方向可以是数量可能是1,也可能是2 可以将其对应的keyCode放入数组遍历
//游戏模拟器需要的是一个挂在keyCode属性的对象,所以封装一个方法专门对keycode进行包装处理
</script>
nipplejs创建的元素:
start事件打印:
move事件打印:
end事件打印:
2.插件封装
对nipplejs有了一定了解之后,我们引用它来封装一个摇杆插件
//此插件依赖nipplejs.min.js
//by https://gitee.com/lianlizhou
function Joystick(opts){
//默认配置
var position = {left:"50%", top:"50%"}
//保存传入的配置信息
this.el = opts && opts.el
this.color = opts && opts.color || 'white'
this.size = opts && opts.size || 100
this.isFourBtn = opts && opts.isFourBtn ? true : false //默认4键模式 否则8键模式(左上/左下/右上/右下)
this.keyCodes = opts && opts.keyCodes || [87, 83, 65, 68] //按顺序是上下左右 默认WSAD
this.btn_down_fn = opts && opts.btn_down_fn //fn 按下时的回调
this.btn_up_fn = opts && opts.btn_up_fn //fn 释放时的回调
this.relative = opts && opts.relative || true //默认将容器设置为相对定位
//生成配置信息 这里配置的参数将传给nipplejs.min.js
this.opts = {
zone:document.querySelector(this.el), //用户设置的容器
mode: 'static',//模式,static就是摇杆中心固定
position:{left:'50%',top:'50%'},//让摇杆容器定位到用户设置容器的正中心
color:this.color,//摇杆的颜色 包括back和front nipplejs默认白色,通过背景色实现
size:this.size,//摇杆容器back元素的大小,front是他的一半,默认100
}
//记录上一次按键方向
this.direction = null //手势释放时重新设为null
}
Joystick.prototype.init = function(){
var me = this
//如果用户不阻止,则将用户容器设为相对定位 要在实例创建前设置
if(me.relative) document.querySelector(me.el).style.position = 'relative'
//创建nipplejs实例
var manager = nipplejs.create(me.opts)
//事件监听
manager.on('start',function(evt,data){
})
manager.on('move',function(evt,data){
//数据交给其他方法处理
me.onMove && me.onMove(data)
})
manager.on('end',function(evt,data){
me.onEnd && me.onEnd()
})
//阻止默认事件,防止快速点击时页面缩放
document.querySelector(me.el).addEventListener('touchstart',function(evt){
evt.preventDefault()
})
}
Joystick.prototype.onMove = function(data){
var me = this
//通过distance属性是否存在判断此次操作是否有效
if(!data.distance) return
//获取最新方向信息
var now_direction = me.getDirection(data)
//处理方向信息
me.handleDirection(now_direction,me.direction) //新方向 上一个方向
//更新按键方向
me.direction = now_direction
}
Joystick.prototype.onEnd = function(){
var me = this
//1.获取要处理的keyCode数组 并调用方法将相关按键释放
me.handleCodeArr('up',me.getCodeArr(me.direction)) //up or down
//2.重置方向信息
me.direction = null
}
Joystick.prototype.getDirection = function(data){
var me = this
//判断是4键模式还是8键模式
if(me.isFourBtn){
//4键模式 直接返回
return data.direction.angle
}else{
//8键模式 根据角度值返回对应的方向
return me.transformDirection(data.angle.degree)
}
}
//用于8键模式 将角度转换成方向
Joystick.prototype.transformDirection = function(degree){
//8个方向平方360度 每个方向45度
//右上 22.5 - 76.5
//上 76.5 - 112.5
//左上 112.5 - 157.5
//左 157.5 - 202.5
//左下 202.5 - 247.5
//右下 247.5 - 292.5
//右 >292.5 <=22.5
if(degree > 292.5){
//右
return 'right'
}else if(degree > 247.5){
//右下
return 'right_down'
}else if(degree > 202.5){
//左下
return 'left_down'
}else if(degree > 157.5){
//左
return 'left'
}else if(degree > 112.5){
//左上
return 'left_up'
}else if(degree > 76.5){
//上
return 'up'
}else if(degree > 22.5){
//右上
return 'right_up'
}else{
//右
return 'right'
}
}
//将相关方式信息转换为keyCode,并放入数组中
Joystick.prototype.handleDirection = function(new_direction,old_direction){
var me = this
//old_direction可能为null 但new_direction绝对有值
//当old_direction时,说明用户刚开始点击,此时需要将相应的keyCode传给btn_down_fn执行
if(old_direction === null){
var code_arr = me.getCodeArr(new_direction)
me.handleCodeArr('down',code_arr)
}
//当old_direction不为null,说明用户正在滑动 如果此时新旧方向不一致,则要更新按键状态
if(old_direction !== null && new_direction !== old_direction){
var old_arr = me.getCodeArr(old_direction)
var new_arr = me.getCodeArr(new_direction)
//找出已经发生改变的方向 例如 右上 -> 右下 需要将'上'取消掉,同时将'下'按下
//遍历新数组的元素,对比该元素是否存在旧数组中,如果不存在,即可得到 按下的 code_arr
var down_arr = new_arr.filter( code => {
return !old_arr.includes(code)
})
me.handleCodeArr('down',down_arr)
//遍历旧数组的元素,对比该元素是否存在新数组中,如果不存在,即可得到 释放的 code_arr
var up_arr = old_arr.filter( code => {
return !new_arr.includes(code)
})
me.handleCodeArr('up',up_arr)
}
}
//将方向信息转换为keyCode后,以数组形式返回
Joystick.prototype.getCodeArr = function(direction){
var me = this
switch(direction){
case 'up':return [me.keyCodes[0]];break;
case 'down':return [me.keyCodes[1]];break;
case 'left':return [me.keyCodes[2]];break;
case 'right':return [me.keyCodes[3]];break;
case 'right_up':return [me.keyCodes[3], me.keyCodes[0]];break;
case 'right_down':return [me.keyCodes[3], me.keyCodes[1]];break;
case 'left_up':return [me.keyCodes[2], me.keyCodes[0]];break;
case 'left_down':return [me.keyCodes[2], me.keyCodes[1]];break;
default:break;
}
}
Joystick.prototype.handleCodeArr = function(type,arr){
//type为up or down
//arr为需要处理的包含keyCode的数组
var me = this
var fn = me.btn_down_fn //默认为按下时的回调
if(type !== 'down'){
//如果不是down 说明是手势释放 需要调用释放按键的回调
fn = me.btn_up_fn
}
//遍历数组中的keyCode 逐个处理
for(var i=0;i<arr.length;i++){
//对keyCode进行包裹后
fn && fn(me.package(arr[i]))
}
}
//对keyCode进行封装
Joystick.prototype.package = function(keyCode){
var evt = {}
evt.keyCode = keyCode
return evt
}
3.使用说明
先引入插件和依赖
<script src="./js/nipplejs.min.js"></script>
<script src="./js/joystick.js"></script>
准备容器
<div id="four_box" class="box"></div>
<div id="eight_box" class="box"></div>
实例化
<script>
function touchDwon(evt){
console.log(`keyCodes = ${evt.keyCode} 的键被按下`)
}
function touchUp(evt){
console.log(`keyCodes = ${evt.keyCode} 的键被释放`)
}
//实例1
var joystick = new Joystick({
el:"#four_box",//容器
color:"red",//摇杆颜色
size:100,//摇杆大小
isFourBtn:true,//4键模式
keyCodes:[87, 83, 65, 68],//绑定 上下左右 到 WSAD键
btn_down_fn:touchDwon,//按下时的回调
btn_up_fn:touchUp,//释放时的回调
})
joystick.init()
//实例2
var joystick2 = new Joystick({
el:"#eight_box",//容器
color:" royalblue",//摇杆颜色
size:100,//摇杆大小
isFourBtn:false,//8键模式
keyCodes:[87, 83, 65, 68],//绑定 上下左右 到 WSAD键
btn_down_fn:touchDwon,//按下时的回调
btn_up_fn:touchUp,//释放时的回调
relative:true,//默认为true 会将容器设置为相对定位
})
joystick2.init()
</script>