移动端NES网页模拟器(1)
前言
移动端浏览器是没有实体键盘的,想要操作游戏就必须为其设置虚拟按键,通过虚拟按键(按钮)的标识与实体键盘的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>
执行效果: