Ruffle
运行Flash
运行Flash所利用的是Ruffle
。
(Flash虽然已经于2020年12月31日结束了,但是Ruffle
可以继续运行Flash。)
官网:https://ruffle.rs
GitHub:https://github.com/ruffle-rs/ruffle
通过官网的介绍,我们知道,Ruffle可以通过应用程序、浏览器插件、JavaScript等多种方式运行Flash,我利用的是其JavaScript方式。
在官网有一个非常清晰的DEMO:
1 2 3 4 5 6 7 8 9 10 11 <script> window .RufflePlayer = window .RufflePlayer || {}; window .addEventListener("load" , (event) => { const ruffle = window .RufflePlayer.newest(); const player = ruffle.createPlayer(); const container = document .getElementById("container" ); container.appendChild(player); player.load("movie.swf" ); }); </script> <script src="path/ to/ruffle/ruffle.js"></script>
window.addEventListener('load', function () {});
和window.onload = function () {};
官网提供的DEMO,利用的是window.addEventListener('load', function () {});
,而不是window.onload = function () {};
,关于这两者的区别,我们在《基于JavaScript的前端开发入门:3.DOM和BOM》 讨论过。
window.onload
注册事件的只能写一次,如果有多个,会以最后一个window.onload
为准;addEventListener
没有限制。
DOMContentLoaded
和load
DOMContentLoaded
:浏览器已完全加载HTML,并构建了DOM树,但像<img>
和样式表之类的外部资源可能尚未加载完成。load
:浏览器不仅加载完成了HTML,还加载完成了所有外部资源:图片,样式等。
我们通过一个具体的例子,讨论其区别。 在DOMContentLoaded
中,获取<img>
标签中图片的尺寸,可能是0
,因为图片可能未加载完成,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 <script> function ready ( ) { alert('DOM is ready' ); alert(`Image size: ${img.offsetWidth} x${img.offsetHeight} ` ); } document .addEventListener("DOMContentLoaded" , ready); </script> <img id="img" src="https:/ /en.js.cx/ clipart/train.gif?speed=1 &cache=0 ">
为什么要在绑定DOMContentLoaded
或load
事件呢?
不绑定直接写在<script>
标签中可以吗? 一般情况下,<script>
标签中的内容,会在构建DOM之前运行。 (浏览器这么设计,是因为有些<script>
标签中的内容会修改DOM,甚至对其执行document.write
操作。)
而且,我们有document.getElementById("container")
这段代码,所以我们需要在构建DOM之后执行。 所以我们绑定了DOMContentLoaded
或load
事件。
例如,如下的代码,一定是先弹出Library loaded
,再弹出DOM ready
。
1 2 3 4 5 6 7 <script> document .addEventListener("DOMContentLoaded" , () => { alert("DOM ready" ); }); alert("Library loaded" ); </script>
Flash下载地址
分享两个链接:
这里有不少高质量的Flash游戏。
进度条
进度条利用的是LayUI。
官网:https://layui.github.io
GitHub:https://github.com/layui/layui
关于LayUI中进度条的操作方法,参考:https://layui.gitee.io/v2/docs/element/progress.html
本文不赘述,主要讨论具体应用。
一般进度条都是"假的",并不完全真实反应加载进度(部分关键节点会是准确的),更多只是出于用户友好的一种设计。在这里也是。
定义进度条:
1 2 3 4 5 6 7 <div id ="progressBarId" > <div style ="text-align: center;" > 加载中</div > <div class ="layui-progress layui-progress-big" lay-showpercent ="true" lay-filter ="progressBar" > <div class ="layui-progress-bar layui-bg-red" lay-percent ="0%" > </div > </div > <br /> </div >
定时任务,每隔200毫秒,进度条随机加一些,但最多到40%:
1 2 3 4 5 6 7 8 9 10 function checkLoad ( ) { if (per < 40 && player_tfqy.readyState == 0 && player_wlwz.readyState == 0 ){ per = per + Math .ceil(Math .random() * 3 ); element.progress('progressBar' , per + '%' ); }else { clearInterval(checkLoadInterval) } } checkLoadInterval = self.setInterval(checkLoad,200 )
这里利用的随机数方法是Math.ceil(Math.random() * 3)
。 在《基于JavaScript的前端开发入门:2.基础语法》 ,我们讨论过,有三种随机数方法
Math.ceil(Math.random()*100);
,向上取整,那么取到0的概率极小。Math.floor(Math.random()*100);
,可均衡获取0到99的随机整数。Math.round(Math.random()*100);
,基本均衡获取0到100的随机整数,但获取最小值0 0 0 和最大值100 100 1 0 0 的几率少一半,因为头尾的分布区间只有其他数字的一半。
加载完成之后,隐去进度条。
1 2 3 4 5 6 7 8 9 10 11 12 player_tfqy.load("/-/9/04/01/tfqy.swf" ).then(() => { per = per + 50 ; if (per >= 100 ){ element.progress('progressBar' , 100 + '%' ); layer.msg('加载完成' ) document .getElementById("progressBarId" ).setAttribute("style" ,"display:none" ); }else { element.progress('progressBar' , per + '%' ); } }).catch((e ) => { layer.msg(`Ruffle failed to load the file: ${e} ` ); });
如上述代码,每加载完成一个,进度加50,如果进度大于等于100,表示加载完成。 根据我们上文的讨论,定时任务最多将进度条加到40,完成一个最多是90,所以要大于等于100,一定是两个都加载好了。而且如果两个都加载好了,一般 都会大于等于100。
为什么说一般? 因为存在一个线程安全的问题。 我们是两个Flash文件同时加载,而且在加载过程中,有共享的变量per
。 比如,其中一个读取了per
,这时候per
是20,加上50,等于70,但变量还没写进去。第二个又读取了per
,per
是20,加上50,等于70。然后第一个和第二个都把结果写进去,per
等于70。两个文件都加载完成了,确只有70。
解决方法,可以顺序执行。本人JavaScript水平有限,根据一些资料,JavaScript是单线程,这个也可能是多虑了。
切换暂停
我们看到,在该页面存在两个小游戏。现在需要的是,切换到一个的时候,将另一个暂停。
绑定click
事件即可。
1 2 3 4 5 document .getElementsByClassName('tab' )[0 ].addEventListener("click" , () => { if (player_wlwz.isPlaying){ player_wlwz.pause(); } })
虚拟键盘
两种方案
在《上古神器(基于Ruffle)》 ,有如下的叙述:
《上古神器》一共有4部。前3部是通过键盘 W
A
S
D
等来操作,暂时只支持PC和Mac,不支持移动设备。
但是在《牧场物语(基于IodineGBA)》 和《口袋妖怪(基于IodineGBA)》 ,又有针对移动设备的虚拟键盘。
虚拟键盘的方案有两种:
点击虚拟键盘后,手动调用键盘的keydown
、keyup
等事件对应的方法。
例如,键盘的keydown
,对应方法funcTest()
,那么就在点击虚拟键盘后,手动调用用funcTest()
方法。
点击虚拟键盘后,手动注入键盘的keydown
、keyup
等事件。
第一种方案
在《牧场物语(基于IodineGBA)》 和《口袋妖怪(基于IodineGBA)》 ,采用的就是第一种方案。
如果我们看JSNES的源码,会发现也是这种方法。在js/ui.js
有代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 $('#joystick_btn_A' ).bind('touchstart' , function (e ) { self.nes.keyboard.keyDown({ keyCode: 75 }); e.preventDefault(); }); $('#joystick_btn_B' ).bind('touchend' , function (e ) { self.nes.keyboard.keyUp({ keyCode: 76 }); e.preventDefault(); });
因为修改过JSNES的键盘映射关系,所以具体的keyCode
的值和原始的不一样。
基于Ruffle,第一种方案,以现在的能力不现实。Ruffle是用Rust这门语言写的,完全不了解,而且源码确实看不太明白。
第二种方案
至于第二种方案,手动注入键盘的keydown
、keyup
等,注入确实是成功了,但是游戏没有对应的响应,原因待确认。
记录一下当时的一些方法。
注入函数:
1 2 3 4 5 6 function fireKeyEvent (element, evtType, keyChar ) { element.focus(); var KeyboardEventInit = {key :keyChar, code :"" , location :0 , repeat :false , isComposing :false }; var evtObj = new KeyboardEvent(evtType, KeyboardEventInit); element.dispatchEvent(evtObj); }
调用示例:
1 fireKeyEvent(document ,"keydown" ,"d" );
监听方法:
1 2 3 document .onkeydown=function (e ) { }
某个键按下事件:onkeydown 某个键被按下或者按住:onkeypress 某个按下的键被松开:onkeyup
可能的原因:isTrusted: false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <html > <body > <script > function fireKeyEvent (element, evtType, keyChar) { element.focus(); var KeyboardEventInit = {key:keyChar, code:"" , location:0 , repeat:false , isComposing:false }; var evtObj = new KeyboardEvent(evtType, KeyboardEventInit); element.dispatchEvent(evtObj); } window .onkeydown=function (e ) { console .log(e) } </script > <button onclick ="fireKeyEvent(window,'keydown','d');" > click2Keydown</button > </body > </html >
我们敲键盘,与点击那个Button,都能监听到键盘事件,说明注入是成功了。但是通过点击Button注入的事件,isTrusted: false
。
JSWQX
iframe
该部分其实很简单。
其"更好的实现",虽然没有开源,但是通过浏览器的F12功能,进行分析之后,完全可以把项目完整的复制下来。
进行简单的修改后,即可以进行部署,本文采取的集成到Hexo博客中进行部署,所以需要在Hexo的配置文件skip_render
配置上wqx/*
(注意,前面没有/
),表示不进行渲染。
然后通过iframe
标签,嵌入到博客的网页中。
1 2 3 4 5 <div style ="text-align:center" > <iframe src ="/wqx/index.html" class ="div-iframe" frameborder ="0" > <p > 加载失败</p > </iframe > </div >
style="text-align:center"
,用于控制居中。
frameborder="0"
,表示不要边框。
缩放策略
因为JSWQX的宽度是400px,但是在很多移动设备上,最大宽度没有400px,所以需要进行自适应的缩放。
主页面:
1 2 3 4 5 6 7 8 9 10 11 .div-iframe { width : 600px ; height : 600px } @media screen and (max-width: 900px ) { .div-iframe { width : 340px ; height : 340px } }
默认的宽和高是600px,但是当宽度小于900px时,采取340px的宽和高。
子页面
1 2 3 4 5 6 7 8 9 .zoom-body { zoom : 1.5 } @media screen and (max-width: 400px ) { .zoom-body { zoom : 0.85 } }
直接在body标签上配置class="zoom-body"
。
子页面判断max-width
依据是iframe
的width,在这里即div-iframe
的width
。
模拟器的原理
如果我们看JSWQX
的源码,会看到这么一部分。
文件部分内容如下:
1 2 3 4 5 6 7 8 9 10 function M65C02Context ( ) { this .ram = null ; this .memmap = null ; this .io_read_map = null ; this .io_write_map = null ; this .io_read = null ; this .io_write = null ; this .cycles = 0 ; ......
根据文件名、提交注释、文件内容,以及在我向大神请教后,确认这是一个CPU。
这就是很多模拟器的原理,通过编程语言,模拟出CPU、内存等硬件,然后把ROM放在上面跑。
题外话,通过JavaScript,在浏览器上模拟出了一个CPU,这是真的大神。
JSNES
同JSWQX,JSNES,也是基于开源项目。
我只是在其基础上进行了一些简单的修改:
因为样式方面,同时适配移动设备横屏、移动设备竖屏以及PC(Mac)的难度较大,索性禁用了移动设备横屏。
具体是如果发现是移动设备横屏,提示请竖屏。
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <script> (function rotate ( ) { var orientation=window .orientation; var pd = null ; function createPd ( ) { if (document .getElementById('preventTran' ) === null ){ var imgData = 'data:image/png;base64,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' ; pd = document .createElement('div' ); pd.setAttribute('id' ,'preventTran' ); pd.style.position = 'fixed' ; pd.style.left = '0' ; pd.style.top = '0' ; pd.style.width = '100%' ; pd.style.height = '100%' ; pd.style.overflow = 'hidden' ; pd.style.backgroundColor = '#2e2e2e' ; pd.style.textAlign = 'center' ; pd.style.zIndex = '99999' ; document .getElementsByTagName('body' )[0 ].appendChild(pd); var img = document .createElement('img' ); img.src = imgData; pd.appendChild(img); img.style.margin = '60px auto 30px' var br = document .createElement('br' ); var p = document .createElement('p' ); p.style.width = '100%' ; p.style.height = 'auto' ; p.style.fontSize = '22px' ; p.style.color = '#626262' ; p.style.lineHeight = '34px' ; p.style.textAlign = 'center' ; p.innerHTML = '为了您的良好体验' ; p.appendChild(br); p.innerHTML += '请将手机竖屏操作' ; pd.appendChild(p); } } if (orientation==90 ||orientation==-90 ){ if (pd == null && document .getElementById('preventTran' ) === null ) createPd(); document .getElementById('preventTran' ).style.display = 'block' ; } window .onorientationchange=function ( ) { if (pd == null && document .getElementById('preventTran' ) == null ) createPd(); document .getElementById('preventTran' ).style.display='none' ; rotate(); }; })(); </script>
IodineGBA
基于开源项目:https://github.com/Browncha023/GBA
我只是在原项目上进行了一些简单的修改,同样也是通过iframe
标签嵌入。
开启声音
根据浏览器的最新规则,声音需要手动开启。
The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. https://goo.gl/7K7WLu
规则所附的链接,提供了几个方法,其中一个方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 window .onload = function ( ) { var context = new AudioContext(); } document .querySelector('button' ).addEventListener('click' , function ( ) { context.resume().then(() => { console .log('Playback resumed successfully' ); }); });
本文采取的就是这种方法。
根据Chrome浏览器提示框的堆栈信息,能找到AudioContext
是由哪个变量所定义的,然后可以在相关事件的方法上,调用其resume
方法。
另附,开启和关闭声音的方法:
1 2 3 4 5 6 7 function XAudioJSWebAudioContextHandleSwitch ( ) { if (XAudioJSWebAudioContextHandle.state === 'running' ) { XAudioJSWebAudioContextHandle.suspend(); } else if (XAudioJSWebAudioContextHandle.state === 'suspended' ) { XAudioJSWebAudioContextHandle.resume(); } }
iframe实时传值
因为我们的GBA模拟器实际上是通过iframe标签嵌入到我们的博客页面的,如果在博客页面敲键盘,即GBA模拟器没有获得焦点,这时候GBA模拟器是不会有响应的。
解决方法为:
iframe实时传值,利用的是window对象下的postMessage方法。
GBA模拟器页面在收到值后,手动调用原本键盘事件对应的方法。
我们主要讨论iframe实时传值。
假设现在有两个页面,iframePage.html
是index.html
的子页面.
1 2 3 4 5 <body style ="border:5px solid #333;" > <h1 > this is index</h1 > <iframe src ="./iframePage.html" id ='myframe' > </iframe > </body >
1 2 3 4 <body style ="border:5px solid #333;" > <h1 > this is iframePage</h1 > </body >
我们以父页面向子页面传值为例。
发送消息
1 2 3 4 iFrame = document .getElementById('myframe' ) iFrame.contentWindow.postMessage('MessageFromIndex1' ,'*' );}
postMessage
是挂载在window对象上的,所以在iframe
加载完毕后,用iFrame.contentWindow
获取到iframe
的window
对象,然后调用postMessage
方法,相当于给子页面发送了一条消息。
postMessage
方法第一个参数是要发送的数据,可以是任何原始类型的数据。
postMessage
方法第二个参数可以设置要发送到哪个url
。
接受消息
我们只需要在子页面监听message事件,并且设置好回调函数即可。
1 2 3 4 5 6 7 function receiveMessageFromIndex ( event ) { console .log('receiveMessageFromIndex' , event ) } window .addEventListener("message" , receiveMessageFromIndex, false );
补充
上文的例子是父页面给子页面发送消息,子页面给父页面发送消息的方法类似。
子页面利用window.parent
获取父页面对象。
1 window .parent.postMessage( {msg : 'MessageFromIframePage' }, '*' );
postMessage的官方文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
虚拟键盘
虚拟键盘基于weel-keypad
实现的。
官网:https://keypad.weel.cool
GitHub:https://github.com/wallenweel/weel-keypad
关于其操作方法,本文不赘述,主要讨论具体应用,关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 let keyMap = { W:87 , O:79 , P:90 , A:65 , S:83 , D:68 , K:75 , L:76 , Enter:13 , Z:90 , X:88 , C:67 , N:78 , M:77 , Shift:16 } let kypd = new Keypad({ name: 'wasd' , onstart: function (key ) { let keyName = key[1 ] if (keyMap[keyName]){ let keyCode = keyMap[keyName]; gbaEle.contentWindow.postMessage('onkeydown-' + keyCode,'*' ); } }, onend: function (key ) { let keyName = key[1 ] if (keyMap[keyName]){ let keyCode = keyMap[keyName]; gbaEle.contentWindow.postMessage('onkeyup-' + keyCode,'*' ); } } }, { wasd: [ [[' ' ], ['W' ], [' ' ], ['O' ], ['P' ], [' ' ]], [['A' ], ['S' ], ['D' ], ['K' ], ['L' ], ['Enter' ]], [['Z' ], ['X' ], ['C' ], ['N' ], ['M' ], ['Shift' ]] ] })
gbaEle.contentWindow.postMessage
就是我们上文讨论的iframe实时传值。