移动端NES网页模拟器(3)

前言

前面2个章节已经封装好了摇杆和NES虚拟按键,现在配合jsnes这个包来完成一个移动端版的NES模拟器。
这是插件的github地址:bfirsh/jsnes
这个包可以直接拿来用,但是没有适配移动端。他通过事件监听,判断evt.keyCode属性来判断用户的输入信息。在移动端只要进行事件监听,生成一个带有keyCode属性的evt,然后将evt这个对象传递给相关的事件回调即可,唯一要做的就是要为不同的按钮匹配相应的keyCode,才能让游戏动起来。

1.jsnes牛刀小试

在下载完jsnes的包之后,打开example文件夹的html文件,就可以看到使用方法了

<!DOCTYPE html>

<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
		<title>Embedding Example</title>
		<!-- 模拟器的包 -->
		<script type="text/javascript" src="https://unpkg.com/jsnes/dist/jsnes.min.js"></script>
		<!-- 模拟器使用说明 -->
		<script type="text/javascript" src="nes-embed.js"></script>
		<script>window.onload = function(){nes_load_url("nes-canvas", "InterglacticTransmissing.nes");}</script>
	</head>
	<body>
		<div style="margin: auto; width: 75%;">
			<canvas id="nes-canvas" width="256" height="240" style="width: 100%"/>
		</div>
		<p>DPad: Arrow keys<br/>Start: Return, Select: Tab<br/>A Button: A, B Button: S</p>
	</body>
</html>

其中最重要的是nes-embed.js,这是截取一部分重要的代码出来

入口函数:封装了一个函数,传入canvas元素的标识和rom的url地址,通过ajax来获取到对应url的Rom数据

function nes_load_url(canvas_id, path){
        //
	nes_init(canvas_id);
	
	var req = new XMLHttpRequest();
	req.open("GET", path);
	req.overrideMimeType("text/plain; charset=x-user-defined");
	req.onerror = () => console.log(`Error loading ${path}: ${req.statusText}`);
	
	req.onload = function() {
		if (this.status === 200) {
                //装载Rom数据
		nes_boot(this.responseText);
		} else if (this.status === 0) {
			// Aborted, so ignore error
		} else {
			req.onerror();
		}
	};
	
	req.send();
}

键盘事件监听,调用keyboard()函数

document.addEventListener('keydown', (event) => {keyboard(nes.buttonDown, event)});
//'keyup'用来取消动作  对于AB键,没有他则无法进行下一轮操作  对于方向键,没有他则游戏角色会一直朝一个方向走下去
document.addEventListener('keyup', (event) => {keyboard(nes.buttonUp, event)});

keyboard()函数:对事件对象的keyCode进行判断,在这里自定义按键

function keyboard(callback, event){
	var player = 1; //1表示1p 2表示2p
	switch(event.keyCode){
		case 38: // UP  87代表W
			callback(player, jsnes.Controller.BUTTON_UP); break;
		case 40: // Down 83代表S
			callback(player, jsnes.Controller.BUTTON_DOWN); break;
		case 37: // Left
			callback(player, jsnes.Controller.BUTTON_LEFT); break;
		case 39: // Right
			callback(player, jsnes.Controller.BUTTON_RIGHT); break;
		case 65: // 'a' - qwerty, dvorak
		case 81: // 'q' - azerty
			callback(player, jsnes.Controller.BUTTON_A); break;
		case 83: // 's' - qwerty, azerty
		case 79: // 'o' - dvorak
			callback(player, jsnes.Controller.BUTTON_B); break;
		case 9: // Tab
			callback(player, jsnes.Controller.BUTTON_SELECT); break;
		case 13: // Return
			callback(player, jsnes.Controller.BUTTON_START); break;
		default: break;
	}
}

这是我自己的设定,用WSAD代表上下左右,JK代表BA,空格代表选择键 回车代表开始键

function keyboard(callback, event){
	var player = 1;
	//console.log(event.keyCode)
	switch(event.keyCode){
		case 87: // UP   -  W
			callback(player, jsnes.Controller.BUTTON_UP); break;
		case 83: // Down  --  S
			callback(player, jsnes.Controller.BUTTON_DOWN); break;
		case 65: // Left  -- A
			callback(player, jsnes.Controller.BUTTON_LEFT); break;
		case 68: // Right   -- D
			callback(player, jsnes.Controller.BUTTON_RIGHT); break;
		case 75: // K
			callback(player, jsnes.Controller.BUTTON_A); break;
		case 74: // J
			callback(player, jsnes.Controller.BUTTON_B); break;
		case 32: // 空格
			callback(player, jsnes.Controller.BUTTON_SELECT); break;
		case 13: // 回车
			callback(player, jsnes.Controller.BUTTON_START); break;
		default: break;
	}
}

另外这个插件有2个位置需要优化,其一:AudioContext需要兼容处理

//93行
var audio_ctx = new window.AudioContext();

改成

var contextClass =(window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.oAudioContext || window.msAudioContext); 
var audio_ctx = new contextClass();

其二:某些浏览器打开时提示:

The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page

查阅资料得知:谷歌浏览器从71版本开始就开启了谷歌的安全策略,因此导致的声音不能自动播放,必须在用户有了操作之后才可以播放音乐。可以设置一个按钮,在按钮的点击事件中调用

document.querySelector('#btn_load').onclick = function(){
    //加载游戏
    nes_load_url("nes-canvas", "./roms/超级玛丽.nes")
    //隐藏加载按钮
    this.style.display = 'none'
}

2.NES模拟器完整代码

HTML文件

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- nes_btn样式文件 -->
    <link rel="stylesheet" href="./css/nes_btn.css">
    <!-- 该页面样式文件 -->
    <link rel="stylesheet" href="./css/index.css">
    <title>NES网页模拟器</title>
</head>
<body>
    <div id="show_box">
        <h3 id="name">热血格斗</h3>
        <div class="canvas_box">
            <h3 id="btn_load">点此加载游戏</h3>
            <canvas id="nes-canvas" width="256" height="240" style="width: 100%"/>
        </div>
    </div>
    <div id="control_box">
        <!-- 摇杆容器 -->
        <div id="direction"></div>
        <!-- 按键容器 -->
        <div id="user_btn_box"></div>
    </div>
</body>
</html>
<script src="./js/jsnes.min.js"></script>
<script src="./js/nes-embed.js"></script>
<!-- nes_btn插件 -->
<script src="./js/nes_btn.js"></script>
<!-- 摇杆插件 -->
<script src="./js/nipplejs.min.js"></script>
<script src="./js/joystick.js"></script>
<script>
    window.onload = function(){
        //监听加载按钮
        document.querySelector('#btn_load').onclick = function(){
            //加载游戏
            nes_load_url("nes-canvas", "./roms/超级玛丽.nes")
            //隐藏加载按钮
            this.style.display = 'none'
        }

        //实例化摇杆
        var joystick = new Joystick({
            el:"#direction",//容器
            color:" royalblue",//摇杆颜色
            size:100,//摇杆大小
            isFourBtn:false,//8键模式
            keyCodes:[87, 83, 65, 68],//绑定 上下左右 到 WSAD键
            btn_down_fn:(event) => {keyboard(nes.buttonDown, event)},//按下时的回调
            btn_up_fn:(event) => {keyboard(nes.buttonUp, event)},//释放时的回调
            relative:true,//默认为true 会将容器设置为相对定位
        })
        joystick.init()

        //实例化NES按钮
        var nesBtn = new VirtualNesBtn({
            el:"#user_btn_box", //容器
            btn_down_fn:(event) => {keyboard(nes.buttonDown, event)},//虚拟按钮按下时的回调 参数evt
            btn_up_fn:(event) => {keyboard(nes.buttonUp, event)},//虚拟按钮弹起时的回调 参数evt
            keyCodes:[32,13,74,75] //按顺序分别是 select start b a
        })

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

css/nes_btn.css

*{
    -webkit-touch-callout:none;
    -webkit-user-select:none;
    -khtml-user-select:none;
    -moz-user-select:none;
    -ms-user-select:none;
    user-select:none;
}
html,body{
    padding:0;
    margin: 0;
}
#show_box{
    position: relative;
    width:80%;
    max-width: 512px;
    text-align: center;
    margin: 20px auto;
}
#show_box .canvas_box{
    background: url(../imgs/unload_bg.jpg);
    background-size: 100% 100%;
}
#show_box #btn_load{
    position: absolute;
    color:#FF4500;
    left:50%;
    top:50%;
    transform: translate(-50%,-50%);
    cursor: pointer;
}
#control_box {
    width:100%;
    padding-top: 30px;
    padding-bottom: 40px;
    bottom:0;
    left:0;
    position: fixed;
    overflow: hidden;
    display: flex;
    justify-content: space-around;
}
#control_box #direction{
    width:40vw;
    height:40vw;
    position: relative;
}
#control_box #user_btn_box{
    width:48%;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
    align-content: space-around;
}

css/index.css

*{
    -webkit-touch-callout:none;
    -webkit-user-select:none;
    -khtml-user-select:none;
    -moz-user-select:none;
    -ms-user-select:none;
    user-select:none;
}
html,body{
    padding:0;
    margin: 0;
}
#show_box{
    position: relative;
    width:80%;
    max-width: 512px;
    text-align: center;
    margin: 20px auto;
}
#show_box .canvas_box{
    background: url(../imgs/unload_bg.jpg);
    background-size: 100% 100%;
}
#show_box #btn_load{
    position: absolute;
    color:#FF4500;
    left:50%;
    top:50%;
    transform: translate(-50%,-50%);
    cursor: pointer;
}
#control_box {
    width:100%;
    padding-top: 30px;
    padding-bottom: 40px;
    bottom:0;
    left:0;
    position: fixed;
    overflow: hidden;
    display: flex;
    justify-content: space-around;
}
#control_box #direction{
    width:40vw;
    height:40vw;
    position: relative;
}
#control_box #user_btn_box{
    width:48%;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
    align-content: space-around;
}

js/nes_btn.js

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)
        }
    }
}

js/joystick.js

//此插件依赖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
}

posted @ 2020-10-26 23:03  ---空白---  阅读(1420)  评论(0编辑  收藏  举报