前几天用JS实现扫雷和贪吃蛇(通过HTML的DOM节点实现基本界面,界面背景简单,交互简单)。比较复杂的是植物大战僵尸,不同的关卡设置单独的函数,僵尸和植物使用不同的类名实现。
超级玛丽通过canvas实现背景,交互很复杂,功能很多,JS代码完全是有汇编语言反编译成C语言,然后把C语言转换成JS实现的。完全使用原生JS实现超级玛丽或者魂斗罗还没有实现。思路:类似植物大战僵尸,设置英雄和不同的敌人是不同的类,不同的关卡设置不同的函数实现。难点:界面交互很多,游戏角色动画较多。理论上 CodeCombat 可以实现类似的效果,那么红白机游戏也可以实现。膜拜反编译汇编语言的大神。
下面是游戏的效果图:
现在没有时间玩,周末可以尝试通个关(这是8090的回忆啊,暴露年龄了)。
下面上源码:HTML 部分很简单,游戏主要在canvas上实现
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>超级玛丽</title>
<link rel="stylesheet" type="text/css" href="./css/index.css">
</head>
<body>
<h1>Javascript反编译超级玛丽</h1>
<!-- 使用canvas实现游戏背景 -->
<canvas id="canvas" width="256" height="240"></canvas>
<!-- 用户操作 -->
<div>
Key: ASDW / IO / Enter
<input id="chkSwapAB" type="checkbox">
<label for="chkSwapAB">Swap AB</label>
</div>
<div>
Zoom:
<button id="btnSize1X">1x</button>
<button id="btnSize2X">2x</button>
<button id="btnSizeScreen" disabled>Full Screen</button>
<input id="chkPxStyle" type="checkbox" disabled>
<label for="chkPxStyle">Pixelated (webkit only)</label>
</div>
<div>
Gear:
<input id="rangeSpeed" type="range" value="0">
<span id="labelSpeed">1</span>x
</div>
<div>
<input id="chkMute" type="checkbox">
<label for="chkMute" disabled>Mute</label>
<button id="btnBenchmark">Benchmark</button>
</div>
<div>
</div>
<!-- 性能分析(开发使用,实际游戏可以注释这部分源码) -->
<p>performance statistic:</p>
<table>
<tr>
<td></td>
<td>Now (ms)</td>
<td>Avg (ms)</td>
</tr>
<tr>
<td>FPS</td>
<td id="itemFPSNow"></td>
<td id="itemFPSAvg"></td>
</tr>
<tr>
<td>Game Logic</td>
<td id="itemGameNow"></td>
<td id="itemGameAvg"></td>
</tr>
<tr>
<td>APU</td>
<td id="itemAPUNow"></td>
<td id="itemAPUAvg"></td>
</tr>
<tr>
<td>PPU</td>
<td id="itemPPUNow"></td>
<td id="itemPPUAvg"></td>
</tr>
<tr>
<td>Render</td>
<td id="itemRenderNow"></td>
<td id="itemRenderAvg"></td>
</tr>
</table>
<script src="./js/smb-min.js"></script>
<script src="./js/ui.js"></script>
</body>
</html>
CSS部分设置界面基本背景
body {
font-family: monospace;
background-color: #eee;
}
#canvas:-webkit-full-screen {
height: 100vmin !important;
width: 106.67vmin !important;
}
#canvas {
margin-bottom: 20px;
}
#rangeSpeed {
width: 400px;
}
table {
border-collapse: collapse;
}
td {
border: solid #999 1px;
padding: 2px 20px;
}
下面是 UI 部分 JS:现在通过键盘的ASDW等键操作游戏,如果设置手柄,那么改一下键盘事件的鼠标事件操作(毕竟键盘操作不如手柄操作好用)。
// Super Mario Bros. JS Version
// recompiled by EtherDream
// translate by MichaelAn
// util 工具函数
function now() {
// performance 接口可以获取到当前页面中与性能相关的信息(这里返回一个时间点)
return performance.now();
}
function toFix(v) {
// | 位运算符
return (v * 100 | 0) / 100;
}
// 内存复制32位?
function memcpy32(dstBuf, dstPos, srcBuf, srcPos, len) {
var src = srcBuf.subarray(srcPos >> 2, (srcPos + len) >> 2);
dstBuf.set(src, dstPos >> 2);
}
function char(str) {
return str.charCodeAt(0);
}
// 创建二进制缓存区(
var gameBuf = new ArrayBuffer(1024 * 1024 * 16);
// 根据原始二进制缓存区创建不同数据类型分的数组
// Uint8Array 数组类型表示一个8位无符号整型数组,创建时内容被初始化为0。创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素。
// Uint32Array 创建32位无符号整形数组
// Float32Array 创建32位浮点型数组
var HEAPU8 = new Uint8Array(gameBuf);
var HEAPU32 = new Uint32Array(gameBuf);
var HEAPF32 = new Float32Array(gameBuf);
// fill static data 填充静态数据
asm_smb_init_mem(gameBuf);
// init asm.js module 开始游戏(汇编反编译实现)
var gameMod = asm_smb_mod(self, {}, gameBuf);
gameMod.init();
// 下面是游戏的参数
var fpsAll = 0;
var gameTimeAll = 0;
var apuTimeAll = 0;
var ppuTimeAll = 0;
var renderTimeAll = 0;
var lastTime = 0;
var nFPS = 0;
var nSec = -1;
var nGameFrame = 0;
var nAudioFrame = 0;
var nSpeed = 1;
var nRemain = 0;
// image 图像尺寸
var PIC_W = 256;
var PIC_H = 240;
var PIC_LEN = PIC_W * PIC_H * 4;
var picPtr = gameMod.getImageBuf();
// 获取界面中的canvas,根据图片的尺寸设置canvas的尺寸
var imageCtx = canvas.getContext('2d');
var imgData = imageCtx.createImageData(PIC_W, PIC_H);
var pixelData = imgData.data;
if (pixelData.buffer) {
// H5
var imgDstU32 = new Uint32Array(pixelData.buffer);
} else {
// IE: fill alpha data
for (var i = 0; i < PIC_LEN; i += 4) {
pixelData[i + 3] = 0xff;
}
}
// audio 设置声音打开
var isMute = true;
var audioCtx;
(function() {
var _AudioContext = window.AudioContext || window.webkitAudioContext;
if (!_AudioContext) {
return;
}
audioCtx = new _AudioContext();
var scriptNode = audioCtx.createScriptProcessor(2048, 1, 1);
var SAMPLES_PER_FRAME = 44100 / 60;
var AUDIO_SIZE = 2048 * 4;
var AUDIO_BUF_SIZE = 4096 * 4;
var audioBufPtr = gameMod.getAudioBuf();
var lastPos = 0;
scriptNode.onaudioprocess = function(e) {
var outBuff = e.outputBuffer;
var dstBuf = outBuff.getChannelData(0);
var currPos = nAudioFrame * SAMPLES_PER_FRAME * 4;
var newBytes = currPos - lastPos;
// if (newBytes < 0) debugger;
if (newBytes === 0) {
return;
}
var size = newBytes;
if (size > AUDIO_SIZE) {
if (size >= AUDIO_SIZE * 2) {
console.log('audio drop frame');
lastPos += (size - AUDIO_SIZE);
}
size = AUDIO_SIZE;
}
var pos = lastPos % AUDIO_BUF_SIZE;
var remain = AUDIO_BUF_SIZE - pos;
if (remain > size) {
memcpy32(dstBuf, 0, HEAPF32, audioBufPtr + pos, size);
} else {
memcpy32(dstBuf, 0, HEAPF32, audioBufPtr + pos, remain);
memcpy32(dstBuf, remain, HEAPF32, audioBufPtr, size - remain);
}
lastPos += size;
};
scriptNode.connect(audioCtx.destination);
chkMute.disabled = false;
})();
function audioSetMute(enabled) {
if (!audioCtx) {
return;
}
if (enabled) {
audioCtx.suspend();
} else {
audioCtx.resume();
}
isMute = enabled;
}
// 处理渲染性能分析
function render() {
requestAnimationFrame(render);
var t1 = now();
nRemain += nSpeed;
while (nRemain > 0) {
gameMod.updateGameFrame();
nRemain--;
}
var t2 = now();
if (!isMute) {
gameMod.updateAudioFrame();
nAudioFrame++;
}
var t3 = now();
gameMod.updateImageBuf();
var t4 = now();
if (imgDstU32) {
// H5
memcpy32(imgDstU32, 0, HEAPU32, picPtr, PIC_LEN);
} else {
// IE
for (var i = 0; i < PIC_LEN; i += 4) {
pixelData[i ] = HEAPU8[picPtr + i ];
pixelData[i + 1] = HEAPU8[picPtr + i + 1];
pixelData[i + 2] = HEAPU8[picPtr + i + 2];
// alpha alway 255
}
}
imageCtx.putImageData(imgData, 0, 0);
var t5 = now();
var gameTime = t2 - t1;
var apuTime = t3 - t2;
var ppuTime = t4 - t3;
var renderTime = t5 - t4;
gameTimeAll += gameTime;
apuTimeAll += apuTime;
ppuTimeAll += ppuTime;
renderTimeAll += renderTime;
nGameFrame++;
nFPS++;
if (t5 - lastTime >= 1000) {
// skip first frame
nSec++;
if (nSec > 0) {
fpsAll += nFPS;
itemFPSNow.textContent = nFPS;
itemFPSAvg.textContent = toFix(fpsAll / nSec);
}
itemGameNow.textContent = toFix(gameTime);
itemAPUNow.textContent = toFix(apuTime);
itemPPUNow.textContent = toFix(ppuTime);
itemRenderNow.textContent = toFix(renderTime);
itemGameAvg.textContent = toFix(gameTimeAll / nGameFrame);
itemAPUAvg.textContent = toFix(apuTimeAll / nGameFrame);
itemPPUAvg.textContent = toFix(ppuTimeAll / nGameFrame);
itemRenderAvg.textContent = toFix(renderTimeAll / nGameFrame);
nFPS = 0;
lastTime = t5;
}
}
var JOY_A = 0;
var JOY_B = 1;
var JOY_SELECT = 2;
var JOY_START = 3;
var JOY_UP = 4;
var JOY_DOWN = 5;
var JOY_LEFT = 6;
var JOY_RIGHT = 7;
var PAD_MAP = {};
// gamepad 设置游戏的快捷键
(function() {
PAD_MAP[char('W')] = JOY_UP;
PAD_MAP[char('D')] = JOY_RIGHT;
PAD_MAP[char('S')] = JOY_DOWN;
PAD_MAP[char('A')] = JOY_LEFT;
PAD_MAP[char(' ')] = JOY_SELECT;
PAD_MAP[char('\r')] = JOY_START;
// 监听键盘输入(如果使用手柄,获取手柄的事件并绑定函数)
document.addEventListener('keydown', function(e) {
var key = PAD_MAP[e.keyCode];
if (key !== undefined) {
gameMod.setKeyState(key, 1);
}
});
document.addEventListener('keyup', function(e) {
var key = PAD_MAP[e.keyCode];
if (key !== undefined) {
gameMod.setKeyState(key, 0);
}
});
})();
// config 配置
(function() {
var cvsSty = canvas.style;
chkSwapAB.onclick = function() {
if (this.checked) {
PAD_MAP[char('O')] = JOY_A;
PAD_MAP[char('I')] = JOY_B;
} else {
PAD_MAP[char('I')] = JOY_A;
PAD_MAP[char('O')] = JOY_B;
}
};
chkSwapAB.onclick();
function setCanvasScale(rate) {
cvsSty.width = (PIC_W * rate) + 'px';
cvsSty.height = (PIC_H * rate) + 'px';
}
btnSize1X.onclick = function() {
setCanvasScale(1);
};
btnSize2X.onclick = function() {
setCanvasScale(2);
};
btnSize2X.onclick();
var rFS = canvas.requestFullScreen ||
canvas.webkitRequestFullscreen ||
canvas.mozRequestFullScreen;
if (rFS) {
btnSizeScreen.disabled = false;
btnSizeScreen.onclick = function() {
rFS.call(canvas);
};
}
// 处理浏览器兼容性
if (/WebKit/.test(navigator.userAgent)) {
chkPxStyle.disabled = false;
chkPxStyle.checked = true;
chkPxStyle.onclick = function() {
cvsSty.imageRendering = this.checked ? 'pixelated' : '';
};
chkPxStyle.onclick();
}
rangeSpeed.onchange = function() {
nSpeed = toFix(Math.pow(2, this.value / 10));
labelSpeed.textContent = nSpeed;
};
rangeSpeed.value = 0;
chkMute.onchange = function() {
audioSetMute(this.checked);
};
chkMute.onchange();
btnBenchmark.onclick = function() {
audioSetMute(true);
var N = 1000; // 1000 per round
var round = 0;
var time = 0;
for (;;) {
var t = now();
for (var i = 0; i < N; i++) {
gameMod.updateGameFrame();
}
t = now() - t;
round++;
time += t;
if (time > 1000) {
break;
}
}
var frames = round * N;
var sec = time / 1000;
alert('Benchmark Result: ' + Math.round(frames / sec) + 'FPS');
audioSetMute(false);
};
})();
// Page Visibility
(function() {
var key, evt;
if ('hidden' in document) {
key = 'hidden';
evt = 'visibilitychange';
} else if ('msHidden' in document) {
key = 'msHidden';
evt = 'msvisibilitychange';
} else if ('webkitHidden' in document) {
key = 'webkitHidden';
evt = 'webkitvisibilitychange';
}
if (!key) {
return;
}
document.addEventListener(evt, function() {
var isHidden = document[key];
audioSetMute(isHidden);
});
})();
function main() {
render();
}
main();
下面是汇编语言反编译的JS部分。 到github上面下载吧:
https://github.com/Michael18811380328/game