美文网首页
用JavaScript编写Chip-8模拟器

用JavaScript编写Chip-8模拟器

作者: 关爱单身狗成长协会 | 来源:发表于2018-02-28 11:29 被阅读7241次

    我相信大多数人都用模拟器玩过游戏吧!比如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坐标像素
    

    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 上有对操作码的说明:

    根据说明我们编写指令集就简单很多,我的做法是先获取到 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文件下载
    查看在线示例

    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文件下载
    查看在线示例

    0x09 最终效果

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

    相关文章

      网友评论

          本文标题:用JavaScript编写Chip-8模拟器

          本文链接:https://www.haomeiwen.com/subject/svlbxftx.html