用JavaScript编写Chip-8模拟器

简介: 我相信大多数人都用模拟器玩过游戏吧!比如GBA模拟器,PSP模拟器,NES模拟器等。所以应该也有人会跟我一样想自己写个游戏机模拟器。但这些模拟器对于一个新手来说难度太大了,就比如NES模拟器中CPU的指令就有100个以上了,更别说除了CPU还有显卡之类的东西需要模拟。
img_2945367bf4f3559bafae50b55bc2e5e7.jpe

我相信大多数人都用模拟器玩过游戏吧!比如GBA模拟器,PSP模拟器,NES模拟器等。所以应该也有人会跟我一样想自己写个游戏机模拟器。但这些模拟器对于一个新手来说难度太大了,就比如NES模拟器中CPU指令就有100个以上了,更别说除了CPU还有显卡之类的东西需要模拟。所以有没有一个比较简单适新手的模拟器项目呢?后来我就找到了 Chip-8
文章完整代码放在:
https://gitee.com/baojuhua/javascript_writing_Chip8/tree/master
关于Chip-8参考资料(可能有墙):
https://en.wikipedia.org/wiki/CHIP8
http://devernay.free.fr/hacks/chip8/C8TECH10.HTM
http://mattmik.com/files/chip8/mastering/chip8.html

0x00 CHIP8简介

我们根据CHIP8的Wiki可以了解到CHIP8是一种解释性的编程语言。最初被应用是在1970年代中期。CHIP8的程序运行在CHIP8虚拟机中,它的出现让电子游戏编程变得简单些了(相对于那个年代来说)。用CHIP8实现的电子游戏有,比如小蜜蜂,俄罗斯方块,吃豆人等。更多可以前往CHIP8的Wiki了解。

0x01 创建CHIP8对象

我们假设CHIP8是由处理器、键盘、显示屏与扬声器组成,其中CPU是CHIP8核心,那么代码应该像这样的:

<!DOCTYPE html>
<html>
<head> 
    <title>创建Chip8对象</title>
</head>
<body> 
    <script>
        (function () {
            function CPU() {/*...*/ };
            function Screen() {/*...*/ };
            function Keyboard() {/*...*/ };
            function Speaker(){/*...*/ };
            window.CHIP8 = function () {
                var c8 = new CPU();
                c8.screen = new Screen();
                c8.speaker = new Speaker();
                c8.input = new Keyboard();
                return c8;
            };
        })();
    </script>
</body>
</html>

0x02 编写简单的显示屏

根据CHIP8的Wiki可以了解到,CHIP8显示分辨率是64X32的像素,并且是单色的。某像素点为1则屏幕上显示相应像素点,为0则不显示。但某个像素点由有到无则进位标识被设置为1,可以用来进行冲撞检测。
那么代码应该像这样:

function Screen() {
    this.rows = 32;//32行
    this.columns = 64;//64列
    this.resolution = this.rows * this.columns;//分辨率
    this.bitMap = new Array(this.resolution);//像素点阵
    this.clear = function () {
        this.bitMap = new Array(this.resolution);
    }
    this.render = function () { };//显示渲染
    this.setPixel = function (x, y) {//在屏幕坐标(x,y)进行计算与显示
        // 显示溢出处理
        if (x > this.columns - 1) while (x > this.columns - 1) x -= this.columns;
        if (x < 0) while (x < 0) x += this.columns;

        if (y > this.rows - 1) while (y > this.rows - 1) y -= this.rows;
        if (y < 0) while (y < 0) y += this.rows;

        //获取点阵索引
        var location = x + (y * this.columns);
        //反向显示,假设二值颜色黑白分别用1、0代表,那么值为1那么就将值设置成0,同理0的话变成1
        this.bitMap[location] = this.bitMap[location] ^ 1;

        return !this.bitMap[location];
    }
};

编写好显示模块我们编写显示屏来测试显示模块(在线查看屏幕测试):

var chip8 = CHIP8();
chip8.screen.render = function () {//自定义实现显示渲染
    var boxs = document.getElementById("boxs");
    boxs.innerHTML = "";
    for (var i of this.bitMap) {
        var d = document.createElement("span");
        d.style = "width: 5px;height: 5px;float: left;";
        d.style.backgroundColor = i ? "#000" : "#fff";
        boxs.appendChild(d);
    }
};
/** 测试 **/
chip8.screen.setPixel(2, 2);//设置x,y坐标像素
chip8.screen.render();
chip8.screen.setPixel(2, 2);//设置x,y坐标像素

img_ee90b84f149e04bd89dcc1621e5a2bf3.gif

0x03 编写扬声器

这里需要参考 Web APIs:

扬声器也十分简单:

function Speaker() {
    var contextClass = (window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.oAudioContext || window.msAudioContext)
        , context
        , oscillator
        , gain; 
    if (contextClass) {
        context = new contextClass();
        gain = context.createGain();
        gain.connect(context.destination);
    } 
    //播放声音
    this.play = function (frequency) {
        //API https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode
        //示例 https://mdn.github.io/violent-theremin/
        if (context && !oscillator) {
            oscillator = context.createOscillator();
            oscillator.frequency.value = frequency || 440;//声音频率 
            oscillator.type = oscillator.TRIANGLE;//波形这里用的是三角波 查看示例:https://codepen.io/gregh/pen/LxJEaj
            oscillator.connect(gain);
            oscillator.start(0);
        }
    } 
    //停止播放
    this.clear = this.stop = function () {
        if (oscillator) {
            oscillator.stop(0);
            oscillator.disconnect(0);
            oscillator = null;
        }
    }
};

编写好扬声器我们可以对扬声器进行测试(在线查看扬声器测试):

<!DOCTYPE html>
<html>
<head> 
    <title>编写扬声器</title>
</head>
<body>
    频率:
    <input type="range" id="frequency" value="440" min="100" max="1000">
    <label id="showfv">(440)</label>
    <button id="play_btn">播放</button>
    <script>
        (function () {
            function CPU() {/*...*/ };
            function Screen() {/*...*/ };//略...
            function Keyboard() {/*...*/ };
            function Speaker() {/*...*/};//略...
            window.CHIP8 = function () {
                var c8 = new CPU();
                c8.screen = new Screen();
                c8.speaker = new Speaker();
                c8.input = new Keyboard();
                return c8;
            };
        })();

        var chip8 = CHIP8();
        //=======
        var f = document.getElementById("frequency");
        var isPlay = false;
        var play_btn = document.getElementById("play_btn");
        f.onchange = function () {
            var v = Number(this.value);
            document.getElementById("showfv").innerHTML = "(" + v + ")";
            if (isPlay) {
                chip8.speaker.stop();
                chip8.speaker.play(v);
            }
        };
        play_btn.onclick = function () {
            isPlay = !isPlay;
            this.innerHTML = isPlay ? '停止' : '播放';
            if (!isPlay) chip8.speaker.stop();
            else chip8.speaker.play(f.value);
        };
    </script>
</body>
</html>

0x04 编写键盘输入设备

CHIP8的输入设备是一个十六进制的键盘,其中有16个键值,0~F。“8”“6”“4”“2”一般用于方向输入。有三个操作码用来处理输入,其中一个是当键值按下则执行下一个指令,对应的是另外一个操作码处理指定键值没有按下则调到下一个指令。第三个操作码是等待一个按键按下,然后将其存放一个寄存器里。
CHIP8键盘布局:

       
1 2 3 C
4 5 6 D
7 8 9 E
A 0 B F
Chip8      我们键盘的映射
---------   ---------
 1 2 3 C     1 2 3 4
 4 5 6 D     q w e r
 7 8 9 E     a s d f
 A 0 B F     z x c v
function Keyboard() {
    var keysPressed = [];//记录按下的按键

    //处理下一个按键
    this.onNextKeyPress = function () { }

    //清空
    this.clear = function () {
        keysPressed = [];
        this.onNextKeyPress = function () { }
    }

    //当前按键是否按下
    this.isKeyPressed = function (property) {
        var key = Keyboard.MAPPING[property];
        return !!keysPressed[key];
    }

    var self = this;
    this.keyDown = function (event) {
        var key = String.fromCharCode(event.which);
        keysPressed[key] = true;

        for (var property in Keyboard.MAPPING) {
            var keyCode = Keyboard.MAPPING[property];
            if (keyCode == key) {
                try {
                    self.onNextKeyPress(parseInt(property),keyCode);
                } finally {
                    self.onNextKeyPress = function () { }
                }
            }
        }
    }

    this.keyUp = function (event) {
        var key = String.fromCharCode(event.which);
        keysPressed[key] = false;
    }

    window.addEventListener("keydown", this.keyDown, false);//绑定键盘按下时间
    window.addEventListener("keyup", this.keyUp, false);//绑定键盘弹起时间
};
//自定义实际键盘按键对应Chip8输入值
Keyboard.MAPPING = {
    0x1:"1",
    0x2:"2",
    0x3:"3",
    0xC:"4",
    0x4:"Q",
    0x5:"W",
    0x6:"E",
    0xD:"R",
    0x7:"A",
    0x8:"S",
    0x9:"D",
    0xE:"F",
    0xA:"Z",
    0x0:"X",
    0xB:"C",
    0xF:"V"
}

编写好键盘输入,同样我们可以对演示器进行测试(在线查看键盘输入测试)

0x05 编写CHIP8核心部分

如果你已经成功编写完成了键盘扬声器显示器
那么接下去的就是重点了

同样是在 CHIP8的Wiki 上可以了解到:

1.内存

CHIP8基本是在一个有4K内存的系统上实现,也就是4096个字节。前512(也就是从0x000到0x1ff)字节由CHIP8的解释器占据。所以CHIP8的程序都是从0x200地址开始的。最顶上的256个字(0xF00-0xFFF) 用于显示刷新,在这下面的96个字节 (0xEA0-0xEFF) 用于栈, 内部使用或者其他变量。其中前512字节 (0x000-0x200) 存放字体数据

2.寄存器(先要了解寄存器是啥)

CHIP8有16个通用8位数据寄存器,V0~VF。VF寄存器存放进位标识。还有一个地址寄存器叫做I,2个字节的长度。
程序计数器(PC)应该是16位的,是用来存放当前正在执行的地址,堆栈是16个16位值的数组,用于存放函数返回的地址值和保存一些数据。

3.扬声器与定时器

CHIP8提供了2个定时器,延时定时器和一个声音定时器
延时定时器活跃时,延时定时器寄存器(Delay Timer 简称 DT)是非零的。这个定时器只是减去1,DT频率的在60Hz。当DT达0时无效。
声音定时器活跃时,声音定时器寄存器(Sound Timer 简称 ST)是非零的。这个定时器也递减率在60Hz,然而,只要ST的价值大于零,该CHIP8蜂鸣器发声。当ST达到零,定时器关闭声音。
由CHIP8翻译产生的声音只有一种声音。声音的音调或频率是由解释器开发者决定的。

4.其他详细信息查看 http://devernay.free.fr/hacks/chip8/C8TECH10.HTM
function CPU() {
    this.pc = 0x200;//CHIP8的程序都是从0x200地址开始的
    this.stack = new Array;//堆栈指针
    this.screen = { clear: function () { }, render: function () { }, setPixel: function () { } };//显示
    this.input = { isKeyPressed: function (key) { }, clear: function () { } };//输入
    this.speaker = { clear: function () { }, play: function () { }, stop: function () { } };//扬声器
    this.v = new Uint8Array(16);//16个数据寄存器 V0~VF
    this.i = 0;//地址寄存器
    this.memory = new Uint8Array(4096);//4K内存
    this.delayTimer = 0;//延时计时器
    this.soundTimer = 0;//声音计时器
    this.paused = false;//暂停
    this.speed = 10;//运行速度

    /**
        * 用默认值重置CPU的一些参数
        */
    this.reset = function () {
        this.pc = 0x200;
        this.stack = new Array;
        this.v = new Uint8Array(16);
        this.i = 0;
        this.memory = new Uint8Array(4096);
        this.delayTimer = 0;
        this.soundTimer = 0;
        this.screen.clear();
        this.input.clear();
        this.speaker.clear();
        this.loadFonts();
        this.paused = false;
    };

    /**
        * 显示渲染
        */
    this.render = function () { this.screen.render(); };

    /**
        * 播放扬声器直到声音计时器达到零 
        */
    this.playSound = function () {
        if (this.soundTimer > 0) {//只要soundTimer的值大于零,CHIP8蜂鸣器发声。
            this.speaker.play();
        } else {
            this.speaker.stop();
        }
    }

    /**
        * 更新CPU延迟和声音计时器 
        */
    this.updateTimers = function () {
        if (this.delayTimer > 0) this.delayTimer -= 1;//递减至0
        if (this.soundTimer > 0) this.soundTimer -= 1;//递减至0
    }

    /**
        * 加载字体到chip8内存
        */
    this.loadFonts = function () {
        var fonts = [
            0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
            0x20, 0x60, 0x20, 0x20, 0x70, // 1
            0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
            0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
            0x90, 0x90, 0xF0, 0x10, 0x10, // 4
            0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
            0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
            0xF0, 0x10, 0x20, 0x40, 0x40, // 7
            0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
            0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
            0xF0, 0x90, 0xF0, 0x90, 0x90, // A
            0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
            0xF0, 0x80, 0x80, 0x80, 0xF0, // C
            0xE0, 0x90, 0x90, 0x90, 0xE0, // D
            0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
            0xF0, 0x80, 0xF0, 0x80, 0x80  // F
        ];
        for (var i = 0, length = fonts.length; i < length; i++) {
            this.memory[i] = fonts[i];
        }
    };


    /**
    * 装程序到内存 
    * @param {Array} program  二进制程序
    */
    this.loadProgram = function (program) {
        for (var i = 0, length = program.length; i < length; i++) {
            this.memory[0x200 + i] = program[i];
        }
    }

        /**
        * CPU 开始执行
        */
    this.cycle = function () {
        for (var i = 0; i < this.speed; i++) {
            if (!this.paused) {
                var opcode = this.memory[this.pc] << 8 | this.memory[this.pc + 1]; //获取操作码,chip-8操作码是两个字节的长度,我们可以读到这两个字节或连接起来
                this.perform(opcode);
            }
        }
        if (!this.paused) {
            this.updateTimers();
        }
        this.playSound();
        this.render();
    };

    /**
        * 一个给定的操作码的进行解析执行
        * @param {Integer} opcode
        */
    this.perform = function (opcode) {/****/ }
};

0x06 CHIP8操作指令集

在编写指令集之前你要对JavaScript中的位运算有所了解
你可以查看我编写的 二进制运算 简单了解下

CHIP8的Wiki 上有对操作码的说明:

img_7e6c302e3ea1c6510e6dd7dbe7a6a524.png

根据说明我们编写指令集就简单很多,我的做法是先获取到 X Y NNN NN N 然后对操作码进行解析
代码:

//略.....
this.perform = function (opcode) {
    this.pc += 2;//每个指令都是两个字节长 
    var x = (opcode & 0x0F00) >> 8;//取得x
    var y = (opcode & 0x00F0) >> 4;//y 
    var NNN = opcode & 0x0FFF;
    var NN = opcode & 0x00FF;
    var N=opcode & 0x000F;
    ({
        0x0000() {
            let r = ({
                //00E0
                //执行“清理屏幕”
                0x00E0() {
                    self.screen.clear();
                },
                //00EE
                //执行“从子函数返回”
                0x00EE() {
                    self.pc = self.stack.pop();
                }
            })[opcode];
            if (r) r();
        },
        //1NNN
        //跳转到地址:NNN
        //例如:0x1222 则跳转到 0x0222
        0x1000() {
            self.pc = NNN;
        },
        //2NNN
        //解释器递增堆栈指针,然后跳转到地址:NNN
        0x2000() {
            self.stack.push(self.pc);
            self.pc = NNN;
        },
        //3XNN
        // if(Vx==NN) 将程序计数器递增2 跳过
        0x3000() {
            if (self.v[x] == NN) self.pc += 2;
        },
        //4XNN
        //if(Vx!=NN)  将程序计数器递增2 跳过
        0x4000() {
            if (self.v[x] != NN) self.pc += 2;
        },
        //5XY0
        //if(Vx==Vy) 将程序计数器递增2 跳过
        0x5000() {
            if (self.v[x] == self.v[y]) self.pc += 2;
        },
        //6XNN
        //设置 Vx=NN
        0x6000() {
            self.v[x] = NN;
        },
        //7XNN
        //设置 Vx+=NN
        0x7000() {
            self.v[x] += NN;
        },
        //8XY0
        0x8000() {
            ({
                //8XY0
                //Vx=Vy
                0x0000() {
                    self.v[x] = self.v[y];
                },
                //8XY1
                //设置 Vx=Vx|Vy
                0x0001() {
                    self.v[x] = self.v[x] | self.v[y];
                },
                //8XY2
                //Vx=Vx&Vy
                0x0002() {
                    self.v[x] = self.v[x] & self.v[y];
                },
                //8XY3
                //Vx=Vx^Vy
                0x0003() {
                    self.v[x] = self.v[x] ^ self.v[y];
                },
                //8XY4
                //Vx += Vy
                0x0004() {
                    var sum = self.v[x] + self.v[y];
                    if (sum > 0xFF) {//即VY+VX > 255 
                        self.v[0xF] = 1;//出现了溢出,则把VF置为1
                    } else {
                        self.v[0xF] = 0;//没有溢出VF置为0
                    }
                    self.v[x] = sum;
                },
                //8XY5
                //Vx -= Vy
                0x0005() {
                    if (self.v[x] > self.v[y]) {
                        self.v[0xF] = 1;
                    } else {
                        self.v[0xF] = 0;
                    }
                    self.v[x] = self.v[x] - self.v[y];
                },
                //8XY6
                //Vx=Vy=Vy>>1
                0x0006() {
                    self.v[0xF] = self.v[x] & 0x01;
                    self.v[x] = self.v[x] >> 1;
                },
                //8XY7
                //Vx=Vy-Vx
                0x0007() {
                    if (self.v[x] > self.v[y]) {
                        this.v[0xF] = 0;
                    } else {
                        self.v[0xF] = 1;
                    }
                    self.v[x] = self.v[y] - self.v[x];
                },
                //8XYE
                //Vx=Vy=Vy<<1
                0x000E() {
                    self.v[0xF] = self.v[x] & 0x80;
                    self.v[x] = self.v[x] << 1;
                }
            })[opcode & 0x000F]();
        },
        //if(Vx!=Vy)  将程序计数器递增2 跳过
        0x9000() {
            if (self.v[x] != self.v[y]) self.pc += 2;
        },
        //ANNN
        //设置 I = NNN
        0xA000() {
            self.i = NNN;
        },
        //BNNN
        //跳转到的位置NNN + V0
        0xB000() {
            self.pc = NNN + self.v[0];
        },
        //CXNN
        //Vx=(随机0至255)&NN
        0xC000() {
            self.v[x] = Math.floor(Math.random() * 0xFF) & NN;
        },
        //DXYN
        //绘画指令
        0xD000() {
            var row, col, sprite
                , width = 8
                , height = opcode & 0x000F;//取得N(图案的高度)

            self.v[0xF] = 0;//初始化VF为0

            for (row = 0; row < height; row++) {//对于每一行
                sprite = self.memory[self.i + row];//取得内存I处的值,pixel中包含了一行的8个像素

                for (col = 0; col < width; col++) {//对于一行的8个像素 
                    if ((sprite & 0x80) > 0) {//依次检查新值中每一位是否为1 
                        if (self.screen.setPixel(self.v[x] + col, self.v[y] + row)) {//如果显示缓存gfx[]里该像素也为1,则发生了碰撞
                            self.v[0xF] = 1;//设置VF为1  
                        }
                    }
                    sprite = sprite << 1;
                }
            } 
        },
        0xE000() {
            ({
                //EX9E
                //if(key()==Vx)  将程序计数器递增2 跳过
                0x009E() {
                    if (self.input.isKeyPressed(self.v[x])) self.pc += 2;
                },
                //EXA1
                //if(key()!=Vx)  将程序计数器递增2 跳过
                0x00A1() {
                    if (!self.input.isKeyPressed(self.v[x])) self.pc += 2;
                }
            })[NN]();
        },
        0xF000() {
            ({
                //FX07
                //Vx = delayTimer
                0x0007() {
                    self.v[x] = self.delayTimer;
                },
                //FX0A
                //Vx =input_key
                0x000A() {
                    self.paused = true;
                    self.input.onNextKeyPress = function (key) {
                        self.v[x] = key;
                        self.paused = false;
                    }.bind(self);
                },
                //FX15
                //delayTimer=Vx                            
                0x0015() {
                    self.delayTimer = self.v[x];
                },
                //FX18
                //soundTimer=Vx
                0x0018() {
                    self.soundTimer = self.v[x];
                },
                //FX1E
                //I +=Vx
                0x001E() {
                    self.i += self.v[x];
                },
                //FX29
                //I=sprite_addr[Vx],一般用4x5字体表示
                0x0029() {
                    self.i = self.v[x] * 5;
                },
                //FX33
                //reg_dump(Vx,&I)   
                0x0033() {
                    self.memory[self.i] = parseInt(self.v[x] / 100);//取得十进制百位
                    self.memory[self.i + 1] = parseInt(self.v[x] % 100 / 10);//取得十进制十位
                    self.memory[self.i + 2] = self.v[x] % 10;//取得十进制个位
                },
                //FX55
                //reg_load(Vx,&I)
                0x0055() {
                    for (var i = 0; i <= x; i++) {
                        self.memory[self.i + i] = self.v[i];
                    }
                },
                //FX65
                //I +=Vx
                0x0065() {
                    for (var i = 0; i <= x; i++) {
                        self.v[i] = self.memory[self.i + i];
                    }
                }
            })[NN]();
        }
    })[opcode & 0xF000]();
};

0x07 整合代码初步实现Chip-8

代码:

<!DOCTYPE html>
<html>

<head>
    <script>
        (function () {
        //略.....
       })();
    </script>
</head>

<body>
    <input type="file" id="f" />
    <div id="boxs" style="overflow: auto;width:320px;border: 1px solid #666;"></div> 
    <script> 

        var chip8 = CHIP8();
        chip8.screen.render = function () {//自定义实现显示渲染
            var boxs = document.getElementById("boxs");
            boxs.innerHTML = "";
            for (var i of this.bitMap) {
                var d = document.createElement("span");
                d.style = "width: 5px;height: 5px;float: left;";
                d.style.backgroundColor = i ? "#000" : "#fff";
                boxs.appendChild(d);
            }
        };
        f.onchange = function () {
            var fl = this.files[0]
            var reader = new FileReader();
            reader.onload = function (e) {
                chip8.reset();//重置
                chip8.rom = new Uint8Array(e.target.result);//读取到rom
                chip8.loadProgram(chip8.rom);//加载程序
                //启动
                if (chip8._loop) {
                    clearInterval(chip8._loop);
                    chip8._loop = null;
                }
                chip8._loop = setInterval(() => {
                    chip8.cycle();
                }, 1000 / 60);//模拟频率 60
            }
            reader.onerror = function (error) {
            }
            reader.readAsArrayBuffer(fl);
        };
    </script>
</body>

</html>

完整代码
测试ROM文件下载
查看在线示例

img_cc44abd1b252e227e72101282945ee6e.gif

0x08 显示优化

由于使用不断生成HTML代码作为显示会出现卡顿的现象,我们尝试着使用Canvas作为显示器

<!DOCTYPE html>
<html>

<head>
    <script>
        (function () {
        //略.....
       })();
    </script>
</head>

<body>
    <input type="file" id="f" />
    <br/>
    <canvas id="boxv"></canvas>
    <script>  
        var chip8 = CHIP8();
        // 使用Canvas作为显示
        var boxv = document.getElementById("boxv");
        var scale = 5;
        boxv.width = chip8.screen.columns * scale;
        boxv.height = chip8.screen.rows * scale;
        var ctx = boxv.getContext("2d");

        chip8.screen.render = function () {//自定义实现显示渲染
            var i, x, y;
            ctx.clearRect(0, 0, boxv.width, boxv.height);
            for (i = 0; i < chip8.screen.resolution; i++) {
                x = (i % chip8.screen.columns) * scale;
                y = Math.floor(i / chip8.screen.columns) * scale;

                if (chip8.screen.bitMap[i]) {
                    ctx.fillStyle = "#000";
                    ctx.fillRect(x, y, scale, scale);
                }
            }
        };
        f.onchange = function () {
            var fl = this.files[0];
            if(!fl)return;
            var reader = new FileReader();
            reader.onload = function (e) {
                chip8.reset();//重置
                chip8.rom = new Uint8Array(e.target.result);//读取到rom
                chip8.loadProgram(chip8.rom);//加载程序
                //启动
                if (chip8._loop) {
                    clearInterval(chip8._loop);
                    chip8._loop = null;
                }
                chip8._loop = setInterval(() => {
                    chip8.cycle();
                }, 1000 / 60);//模拟频率 60
            }
            reader.onerror = function (error) {
            }
            reader.readAsArrayBuffer(fl);
        };
    </script>
</body>

</html>

完整代码
测试ROM文件下载
查看在线示例

img_3b65ffea6af0a706566ade0098391491.gif

0x09 最终效果

完整代码
测试ROM文件下载
查看在线示例

img_e267d071f9ce3da897712927b3e05423.gif
相关文章
|
SQL JavaScript 前端开发
如何用 JavaScript 编写你的第一个单元测试
测试代码是确保代码稳定的第一步。能做到这一点的最佳方法之一就是使用单元测试....
如何用 JavaScript 编写你的第一个单元测试
|
前端开发 JavaScript
8 种技巧让你编写更简洁的 JavaScript 代码
8 种技巧让你编写更简洁的 JavaScript 代码
260 0
8 种技巧让你编写更简洁的 JavaScript 代码
|
JavaScript 前端开发
PubSubJS:用JavaScript编写的基于主题的发布/订阅库
PubSubJS:用JavaScript编写的基于主题的发布/订阅库
202 0
|
JavaScript 前端开发 测试技术
【Javascript】学习使用mocha(摩卡)编写测试用例-常用命令
【Javascript】学习使用mocha(摩卡)编写测试用例-常用命令
119 0
|
Web App开发 JavaScript 前端开发
|
JavaScript 前端开发
编写可维护的JavaScript
软件生命周期中80%的成本都消耗在了维护上;而且几乎所有的维护者都不是代码的直接开发人。如何让自己写的代码让别人阅读起来更高效?当然是写代码的时候注入一些规范。那么在Javascript中有哪些编程风格值得我们去注重呢?这篇文章将总结《编写可维护的JavaScript》里面的观点。
1404 0
|
SQL JavaScript 前端开发
javascript + sql编写SQL客户端工具tabris
祝大家2018新年快乐, 前不久发现了一个创意的脚本JtSQL(java编写) 开源地址为:https://github.com/noear/JtSQL JtSQL 特点:*.结合了JS、SQL、模板理念;*.
1108 0