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

OrochiZ- 2020-10-25 17:19 原文

前言

移动端浏览器是没有实体键盘的,想要操作游戏就必须为其设置虚拟按键,通过虚拟按键(按钮)的标识与实体键盘的keyCode进行绑定,来达到想要的效果。
这个随笔只封装NES游戏手柄右边的按键,不包含方向键。方向键的封装在另一个章节。

1.按键UI

NES游戏手柄分为6个键,如图:

连发按键这里不做,我看到很多第三方网页模拟器也是没有的。不过虚拟键盘的A+B同按是很困难的,这里会为其增加一个宏按键。设计图如下:

封装的初衷在于用户只需要自己配置好外层容器的大小和id,其他的事交给插件去做。所以插件要自动适应容器大小,而且能动态添加dom元素到目标容器中。

虚拟按键设计思路:
(1)最外层的容器宽高100%,使用flex布局,它的子元素就是虚拟按键

(2)子元素分为3行,每个按键的宽度以百分比计算。

html容器结构:

<body>
    <!--  user_btn_box 为用户设置的容器-->
    <div id="user_btn_box">
        <!--  nes_btn_box 为插件添加的容器,配合js动态添加-->
        <div class="nes_btn_box">
            <span class="btn btn-select">SELECT</span>
            <span class="btn btn-start">START</span>
            <span class="btn btn-ab">B + A</span>
            <span class="btn btn-b">B</span>
            <span class="btn btn-a">A</span>
        </div>
    </div>
</body>

样式文件:

.nes_btn_box {
    width:100%;
    height:100%;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
    align-content: space-around;
    -webkit-tap-highlight-color: transparent;
}

.nes_btn_box .btn {
    height:30px;
    line-height: 30px;
    text-align: center;
    background-color: lightcoral;
    color: lightcyan;
    cursor: pointer;
    border-radius: 5px;
}
/* 点击时按钮的背景色 */
.nes_btn_box .btn.isTouch {
    background-color: linen;
}

.nes_btn_box .btn-select,
.nes_btn_box .btn-start {
    width:40%;
}
.nes_btn_box .btn-ab {
    width:80%;
    height:60px;
    line-height: 60px;
}

.nes_btn_box .btn-a,
.nes_btn_box .btn-b {
    width:40%;
    height:60px;
    line-height: 60px;
}

2.动态添加虚拟按键

插件其实就是一个构造函数,为其设置init方法,接收一系列参数来完成所需要的功能
(1)创建构造函数,接收参数
(2)为构造函数设置init方法,传入接收的参数
(3)通过传入的参数,创建dom元素,传入对应的容器中

插件代码:

function VirtualNesBtn(opt){
    //接收容器的标识
    this.el = opt.el

    //生成5个带随机数后缀的id
    this.id_arr = []
    this.id_arr.push('btn_select_' + Math.floor(Math.random() * 100000))
    this.id_arr.push('btn_start_' + Math.floor(Math.random() * 100000))
    this.id_arr.push('btn_ab_' + Math.floor(Math.random() * 100000))
    this.id_arr.push('btn_b_' + Math.floor(Math.random() * 100000))
    this.id_arr.push('btn_a_' + Math.floor(Math.random() * 100000))

    //设定5个按钮的文本名称
    this.name_arr = ['SELECT', 'START', 'B + A', 'B', 'A']
}

//初始化 创建虚拟按钮
VirtualNesBtn.prototype.init = function(opt){
    var me = this
    //创建5个按键
    var btn_select = document.createElement('span')
    var btn_start = document.createElement('span')
    var btn_ab = document.createElement('span')
    var btn_b = document.createElement('span')
    var btn_a = document.createElement('span')

    //为5个按键设置css类名
    btn_select.classList.add('btn','btn-select')
    btn_start.classList.add('btn','btn-start')
    btn_ab.classList.add('btn','btn-ab')
    btn_b.classList.add('btn','btn-b')
    btn_a.classList.add('btn','btn-a')

    //为5个按键设置id
    btn_select.id = me.id_arr[0]
    btn_start.id = me.id_arr[1]
    btn_ab.id = me.id_arr[2]
    btn_b.id = me.id_arr[3]
    btn_a.id = me.id_arr[04]

    //为5个按键设置 文本
    btn_select.innerText = me.name_arr[0]
    btn_start.innerText = me.name_arr[1]
    btn_ab.innerText = me.name_arr[2]
    btn_b.innerText = me.name_arr[3]
    btn_a.innerText = me.name_arr[4]

    //创建容器,并将5个按钮插入其中
    var nes_btn_box = document.createElement('div')
    nes_btn_box.classList.add('nes_btn_box')
    nes_btn_box.appendChild(btn_select)
    nes_btn_box.appendChild(btn_start)
    nes_btn_box.appendChild(btn_ab)
    nes_btn_box.appendChild(btn_b)
    nes_btn_box.appendChild(btn_a)

    //插入到目标容器中
    var target = document.querySelector(me.el)
    target.appendChild(nes_btn_box)

    //为按钮设置点击高亮效果
    me.set_tap_highlight()
}

//监听点击 设置点击高亮效果
VirtualNesBtn.prototype.set_tap_highlight = function(){
    var me = this
    var box = document.querySelector(me.el)

    box.addEventListener('touchstart',(evt) => {
        //阻止默认事件,防止快速点击时页面放大
        evt.preventDefault()
        //判断点击的目标元素是否是虚拟按钮之一
        if(me.id_arr.includes(evt.target.id)){
            evt.target.classList.add('isTouch')
        }
    })

    box.addEventListener('touchend',(evt) => {
        //判断点击的目标元素是否是虚拟按钮之一
        if(me.id_arr.includes(evt.target.id)){
            evt.target.classList.remove('isTouch')
        }
    })
}

3.将虚拟按钮的touch事件绑定到实体键盘事件中

游戏模拟器本身是通过监听键盘事件,在回调函数中判断事件对象的keyCode属性来判断用户按下的是哪个按键,虚拟按钮的touch事件对象中没有keyCode属性,我们可以手动添加这个属性,供回调函数使用。

注意:宏按键(B+A)要单独处理

模拟器相关代码如图:

前面的点击高亮的函数已经监听了touch事件,这里也需要监听touch事件,为了代码复用,要对刚才的代码进行优化。

创建实例时,接收4个属性,分别是:
el:容器的标识(必须),例如 '#user_box'
btn_down_fn:虚拟按钮按下时的回调(必须)
btn_ip_fn:虚拟按钮释放时的回调(必须)
keyCodes:[] 按顺序分别是 select start b a 对应的keyCode (可选)

对元素进行touch监听时,通过evt.target.id可以获取目标元素的id,为了方便根据id获得其绑定的keycode,封装一个方法。
获取keyCode后,进行一系列判断,并调用相关回调。

function VirtualNesBtn(opt){
    //接收容器的标识
    this.el = opt.el
    //接收回调
    this.btn_down_fn = opt.btn_down_fn //fn
    this.btn_up_fn = opt.btn_up_fn //fn

    //保存按钮信息
    this.btns_info = []

    //为5个按钮添加带数字后缀的 id
    this.btns_info = [{},{},{},{},{}]
    this.btns_info[0].id = 'btn_select_' + Math.floor(Math.random() * 100000)
    this.btns_info[1].id = 'btn_start_' + Math.floor(Math.random() * 100000)
    this.btns_info[2].id = 'btn_ab_' + Math.floor(Math.random() * 100000)
    this.btns_info[3].id = 'btn_b_' + Math.floor(Math.random() * 100000)
    this.btns_info[4].id = 'btn_a_' + Math.floor(Math.random() * 100000)

    //设定5个按钮的文本名称 name
    this.btns_info[0].name = 'SELECT'
    this.btns_info[1].name = 'START'
    this.btns_info[2].name = 'B + A'
    this.btns_info[3].name = 'B'
    this.btns_info[4].name = 'A'

    //配置5个按钮的 keycode 默认为 空格 回车 J K 
    this.btns_info[0].keyCode = opt.keyCodes && opt.keyCodes[0] || 32
    this.btns_info[1].keyCode = opt.keyCodes && opt.keyCodes[1] || 13
    this.btns_info[2].keyCode = 'macro_key'
    this.btns_info[3].keyCode = opt.keyCodes && opt.keyCodes[2] || 74
    this.btns_info[4].keyCode = opt.keyCodes && opt.keyCodes[3] || 75

}

//初始化 创建虚拟按钮
VirtualNesBtn.prototype.init = function(opt){
    var me = this
    //创建5个按键
    var btn_select = document.createElement('span')
    var btn_start = document.createElement('span')
    var btn_ab = document.createElement('span')
    var btn_b = document.createElement('span')
    var btn_a = document.createElement('span')

    //为5个按键设置css类名
    btn_select.classList.add('btn','btn-select')
    btn_start.classList.add('btn','btn-start')
    btn_ab.classList.add('btn','btn-ab')
    btn_b.classList.add('btn','btn-b')
    btn_a.classList.add('btn','btn-a')

    //为5个按键设置id
    btn_select.id = me.btns_info[0].id
    btn_start.id = me.btns_info[1].id
    btn_ab.id = me.btns_info[2].id
    btn_b.id = me.btns_info[3].id
    btn_a.id = me.btns_info[4].id

    //为5个按键设置 文本
    btn_select.innerText = me.btns_info[0].name
    btn_start.innerText = me.btns_info[1].name
    btn_ab.innerText = me.btns_info[2].name
    btn_b.innerText = me.btns_info[3].name
    btn_a.innerText = me.btns_info[4].name

    //创建容器,并将5个按钮插入其中
    var nes_btn_box = document.createElement('div')
    nes_btn_box.classList.add('nes_btn_box')
    nes_btn_box.appendChild(btn_select)
    nes_btn_box.appendChild(btn_start)
    nes_btn_box.appendChild(btn_ab)
    nes_btn_box.appendChild(btn_b)
    nes_btn_box.appendChild(btn_a)

    //插入到目标容器中
    var target = document.querySelector(me.el)
    target.appendChild(nes_btn_box)

    //设置touch事件监听
    target.addEventListener('touchstart',(evt) => {
        //阻止默认事件,防止快速点击时页面放大
        evt.preventDefault()

        //判断点中的是否是虚拟按钮
        var is_nes_btn = me.btns_info.some(function(item){
            return item.id === evt.target.id
        })

        if(is_nes_btn){
            //添加高亮
            evt.target.classList.add('isTouch')
            //处理此次点击
            me.handleBtn(evt,'btn_down')
        }
    })

    target.addEventListener('touchend',(evt) => {
        //判断点中的是否是虚拟按钮
        var is_nes_btn = me.btns_info.some(function(item){
            return item.id === evt.target.id
        })

        if(is_nes_btn){
            //移除高亮
            evt.target.classList.remove('isTouch')
            //处理此次点击
            me.handleBtn(evt,'btn_up')
        }
    })
}

//对虚拟按键的id进行判断,返回要绑定的 keyCode
VirtualNesBtn.prototype.getCode = function(id){
    var me = this
    //1.根据id查到按钮信息在数组中的下标
    var index = me.btns_info.findIndex(function(item){
        return item.id === id
    })
    //2.根据下标找到对应的keyCode
    return me.btns_info[index].keyCode
}

//对按键进行处理
VirtualNesBtn.prototype.handleBtn = function(evt,type){
    var me = this
    //1.找到keycode
    var keyCode = me.getCode(evt.target.id)
    //2.根据keycode判是否是宏按键
    if(keyCode === 'macro_key'){
        //要触发2个按键
        var evt_tem = {}
        var evt_tem2 = {}
        evt_tem.keyCode = me.btns_info[3].keyCode
        evt_tem2.keyCode = me.btns_info[4].keyCode
        if(type === 'btn_down'){
            me.btn_down_fn && me.btn_down_fn(evt_tem)
            me.btn_down_fn && me.btn_down_fn(evt_tem2)
        }else{
            me.btn_up_fn && me.btn_up_fn(evt_tem)
            me.btn_up_fn && me.btn_up_fn(evt_tem2)
        }
    }else{
        //添加keyCode属性
        evt.keyCode = keyCode
        //不是宏按键则执行相应的回调
        if(type === 'btn_down'){
            me.btn_down_fn && me.btn_down_fn(evt)
        }else{
            me.btn_up_fn && me.btn_up_fn(evt)
        }
    }
}

使用示例:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>NES虚拟按键</title>
    <!-- 引入插件的css文件 -->
    <link rel="stylesheet" href="./css/nes_btn.css">
    <style>
        #user_btn_box{
            width:250px;
            height:250px;
            margin: 20px auto;
            border: 1px solid greenyellow;
        }
    </style>
</head>
<body>
    <!--  user_btn_box 为用户设置的容器-->
    <div id="user_btn_box"></div>
</body>
</html>
<!-- 引入插件 -->
<script src="./js/nes_btn.js"></script>
<script>
    function keydown(evt){
        console.log('keydown keyCode = ' + evt.keyCode)
    }
    function keyup(evt){
        console.log('keyup keyCode = ' + evt.keyCode)
    }
    window.onload = function(){
        //创建实例
        var nesBtn = new VirtualNesBtn({
            el:"#user_btn_box", //容器
            btn_down_fn:keydown,//虚拟按钮按下时的回调 参数evt
            btn_up_fn:keyup,//虚拟按钮弹起时的回调 参数evt
            keyCodes:[32,13,74,75] //按顺序分别是 select start b a
        })

        //实例初始化
        nesBtn.init()
    }
</script>

执行效果:

推荐阅读