博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
用JavaScript编写Chip-8模拟器
阅读量:5745 次
发布时间:2019-06-18

本文共 16085 字,大约阅读时间需要 53 分钟。

img_2945367bf4f3559bafae50b55bc2e5e7.jpe

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

文章完整代码放在:
关于Chip-8参考资料(可能有墙):

0x00 CHIP8简介

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

0x01 创建CHIP8对象

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

     创建Chip8对象     

0x02 编写简单的显示屏

根据可以了解到,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 编写扬声器

这里需要参考 :

  • API
  • API
  • 示例
  • 示例

扬声器也十分简单:

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

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

     编写扬声器    频率:                

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核心部分

如果你已经成功编写完成了键盘扬声器显示器

那么接下去的就是重点了

同样是在 上可以了解到:

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.其他详细信息查看
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中的位运算有所了解

你可以查看我编写的 简单了解下

在 上有对操作码的说明:

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

代码:

            

img_cc44abd1b252e227e72101282945ee6e.gif

0x08 显示优化

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

            

img_3b65ffea6af0a706566ade0098391491.gif

0x09 最终效果

img_e267d071f9ce3da897712927b3e05423.gif

转载地址:http://jlazx.baihongyu.com/

你可能感兴趣的文章
ext2磁盘布局
查看>>
Ubuntu 12.04 root用户登录设置
查看>>
存储过程点滴
查看>>
[LeetCode]22.Generate Parentheses
查看>>
计算A/B Test需要的样本量
查看>>
二叉树前序中序后序遍历的非递归方法
查看>>
[Unity3d]Shader 着色器 学习前了解知识
查看>>
维辰超市:借助云商城成功转型新零售
查看>>
web.xml中<load-on-start>n</load-on-satrt>作用
查看>>
【算法】CRF
查看>>
Windows UI风格的设计(7)
查看>>
SQL中使用WITH AS提高性能 使用公用表表达式(CTE)简化嵌套SQL
查看>>
oracle 强行杀掉一个用户连接
查看>>
Git提交本地库代码到远程服务器的操作
查看>>
让你快速上手的Glide4.x教程
查看>>
浮动和清除(闭合)浮动
查看>>
LR录制脚本时IE打不开的原因
查看>>
Sublime Text 2.0.2,Build 2221注册码
查看>>
最长递增子序列 动态规划
查看>>
原生CSS设置网站主题色—CSS变量赋值
查看>>