首页 > 技术文章 > 移动端NES网页模拟器(2)

OrochiZ- 2020-10-26 17:31 原文

前言

前面的章节已经封装了一个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>

推荐阅读