手把手教你编写游戏模拟器 - Chip8篇(2)

 

手把手教你编写游戏模拟器 - Chip8篇(2)

 

翻译整理分析:by Yiran Xie

 

*如要转载请附上本文链接

 

书接上文(手把手教你编写一个游戏模拟器 - CHIP8),下载下来的源代码包中包含了可执行文件(含glut32.dll)和源代码。源代码主要是三个文件chip8.h, chip8.cpp, main.cpp。其中chip8.h和chip8.cpp主要是对于chip8的内存、CPU指令、寄存器等等的模拟,main.cpp是用openGL接口实现主函数,包含输入系统与图像系统。

 

限于篇幅,这篇中主要讨论下chip8.h和chip8.cpp。

 

如果完整看完上一篇文章的话,这些代码应该都是小case了。很简单的类,成员变量和成员函数在文章(一)中几乎都有提及,除了一个debugRender(),是作者debug时的辅助函数,用于控制台方式输出显存中的值。

 

cpp文件中最长的部分就是opcode的实现,由于几乎都是参照着http://en.wikipedia.org/wiki/CHIP-8所写,所以乏善可陈。对于每条opcode,其实不需要去理解它为何要实现这个功能,只要能做到准确地实现即可。大部分opcode的难点个例在文章(一)中也都有涉及。

这里附上详细的注释

chip8.h

///////////////////////////////////////////////////////////////////////////////

// Project description

// Name: myChip8

//

// Author: Laurence Muller

// Contact: [email protected]

//

// License: GNU General Public License (GPL) v2 

// ( http://www.gnu.org/licenses/old-licenses/gpl-2.0.html )

//

// Copyright (C) 2011 Laurence Muller / www.multigesture.net

///////////////////////////////////////////////////////////////////////////////



class chip8 

{

    public:

        chip8()//构造函数是空的,初始化主要是由init()完成的

        ~chip8();

        

        bool drawFlag;//用于记录是否需要绘画的标志位



        void emulateCycle();//模拟周期

        void debugRender();//用于debug,用控制台方式输出显存中的值

        bool loadApplication(const char * filename);//把游戏rom读入内存(注意,其中包含init())        



        unsigned char  gfx[64 * 32];//Chip8的显示缓存,总共2048个像素

        unsigned char  key[16];//16个按键输入            



    private:    

        unsigned short pc;//程序计数器

        unsigned short opcode;//当前opcode

        unsigned short I;//当前索引寄存器

        unsigned short sp;//当前栈顶指针

        

        unsigned char  V[16];//寄存器(V0-VF),前15个是通用寄存器,第16个是进位标志

        unsigned short stack[16];//栈(16级)

        unsigned char  memory[4096];//主内存(4k大小)        

                

        unsigned char  delay_timer;        

        unsigned char  sound_timer;//两个计时器        



        void init();//主要的初始化工作

};

 

chip8.cpp

///////////////////////////////////////////////////////////////////////////////

// Project description

// Name: myChip8

//

// Author: Laurence Muller

// Contact: [email protected]

//

// License: GNU General Public License (GPL) v2 

// ( http://www.gnu.org/licenses/old-licenses/gpl-2.0.html )

//

// Copyright (C) 2011 Laurence Muller / www.multigesture.net

///////////////////////////////////////////////////////////////////////////////



#include "chip8.h"

#include <stdio.h>

#include <stdlib.h>

#include <time.h>



//用于显示的字体集

unsigned char chip8_fontset[80] =

{ 

    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

};



chip8::chip8()

{

    // empty

}



chip8::~chip8()

{

    // empty

}



void chip8::init()

{

    pc        = 0x200;        //程序计数器指向0x200(ROM将被加载到的位置)

    opcode    = 0;            //初始化当前opcode

    I        = 0;            //初始化索引寄存器

    sp        = 0;            //初始化栈顶指针



    //清理显存

    for(int i = 0; i < 2048; ++i)

        gfx[i] = 0;



    //清理栈

    for(int i = 0; i < 16; ++i)

        stack[i] = 0;



    //清理从V0到VF的寄存器

    for(int i = 0; i < 16; ++i)

        key[i] = V[i] = 0;



    //清理内存

    for(int i = 0; i < 4096; ++i)

        memory[i] = 0;

                    

    //读取字体集到内存的0x00-0x50处

    for(int i = 0; i < 80; ++i)

        memory[i] = chip8_fontset[i];        



    //初始化计时器

    delay_timer = 0;

    sound_timer = 0;



    //初始化绘画标志

    drawFlag = true;



    srand((unsigned int)time(NULL));//产生随机数种子

}



void chip8::emulateCycle()//模拟周期

{

    //获取opcode(把两个字节合并在一起)

    opcode = memory[pc] << 8 | memory[pc + 1];

    

    //处理opcode(可以参考http://en.wikipedia.org/wiki/CHIP-8)

    switch(opcode & 0xF000)

    {        

        case 0x0000:

            switch(opcode & 0x000F)

            {

                case 0x0000: //0x00E0: 清理显存(全部置为0)

                    for(int i = 0; i < 2048; ++i)//一共2048个像素,每个像素一个byte

                        gfx[i] = 0x0;

                    drawFlag = true;

                    pc += 2;

                break;



                case 0x000E: //0x00EE: 从子函数返回

                    -- sp;            //栈指针往回走一格(注意这个栈是往上生长的)

                    pc = stack[sp];    //把PC(程序计数器)恢复成原来的值                    

                    pc += 2;        //别忘了PC还要往前跳一格

                break;



                default:

                    printf ("Unknown opcode [0x0000]: 0x%X\n", opcode);                    

            }

        break;



        case 0x1000: //0x1NNN: 跳到NNN这个地址(不用返回),相当于jmp/goto

            pc = opcode & 0x0FFF;//NNN这个地址存储在低12位,通过&取出

        break;



        case 0x2000: //0x2NNN: 调用NNN这个地址的子函数(将来需要返回)

            stack[sp] = pc;//把当前PC压栈

            ++sp;//步进栈顶指针

            pc = opcode & 0x0FFF;//跳转

        break;

        

        case 0x3000: //0x3XNN: 如果VX == NN,跳过接下去的指令

            if(V[(opcode & 0x0F00) >> 8] == (opcode & 0x00FF))

                pc += 4;

            else

                pc += 2;

        break;

        

        case 0x4000: //0x4XNN: 如果VX != NN,跳过接下去的指令

            if(V[(opcode & 0x0F00) >> 8] != (opcode & 0x00FF))

                pc += 4;

            else

                pc += 2;

        break;

        

        case 0x5000: //0x5XY0: 如果VX == VY,跳过接下去的指令

            if(V[(opcode & 0x0F00) >> 8] == V[(opcode & 0x00F0) >> 4])

                pc += 4;

            else

                pc += 2;

        break;

        

        case 0x6000: //0x6XNN: VX = NN

            V[(opcode & 0x0F00) >> 8] = opcode & 0x00FF;

            pc += 2;

        break;

        

        case 0x7000: // 0x7XNN: VX += NN

            V[(opcode & 0x0F00) >> 8] += opcode & 0x00FF;

            pc += 2;

        break;

        

        case 0x8000:

            switch(opcode & 0x000F)

            {

                case 0x0000: // 0x8XY0: VX = VY

                    V[(opcode & 0x0F00) >> 8] = V[(opcode & 0x00F0) >> 4];

                    pc += 2;

                break;



                case 0x0001: // 0x8XY1: VX = VX | VY

                    V[(opcode & 0x0F00) >> 8] |= V[(opcode & 0x00F0) >> 4];

                    pc += 2;

                break;



                case 0x0002: // 0x8XY2: VX = VX & VY

                    V[(opcode & 0x0F00) >> 8] &= V[(opcode & 0x00F0) >> 4];

                    pc += 2;

                break;



                case 0x0003: // 0x8XY3: VX = VX ^ VY

                    V[(opcode & 0x0F00) >> 8] ^= V[(opcode & 0x00F0) >> 4];

                    pc += 2;

                break;



                case 0x0004: // 0x8XY4: VX +=VY. 如果有溢出,则把VF(进位标志)设为1,否则为0                    

                    if(V[(opcode & 0x00F0) >> 4] > (0xFF - V[(opcode & 0x0F00) >> 8])) //即VY > 255 - VX

                        V[0xF] = 1; //出现了溢出,则把VF置为1

                    else 

                        V[0xF] = 0;                    

                    V[(opcode & 0x0F00) >> 8] += V[(opcode & 0x00F0) >> 4];//VX += VY

                    pc += 2;                    

                break;



                case 0x0005: // 0x8XY5: VX -= VY. 如果有借位发生(差小于0),VF设为0,否则设为1

                    if(V[(opcode & 0x00F0) >> 4] > V[(opcode & 0x0F00) >> 8]) //即VY > VX

                        V[0xF] = 0; //有借位情况,置为0

                    else 

                        V[0xF] = 1;                    

                    V[(opcode & 0x0F00) >> 8] -= V[(opcode & 0x00F0) >> 4];//VX -= VY

                    pc += 2;

                break;



                case 0x0006: // 0x8XY6: VX右移一位. VF设为VX右移前的最低位

                    V[0xF] = V[(opcode & 0x0F00) >> 8] & 0x1;//取出最低位,给VF

                    V[(opcode & 0x0F00) >> 8] >>= 1;

                    pc += 2;

                break;



                case 0x0007: // 0x8XY7: VX = VY - VX. 如果有借位发生(差小于0),VF设为0,否则设为1

                    if(V[(opcode & 0x0F00) >> 8] > V[(opcode & 0x00F0) >> 4])//即VX > VY

                        V[0xF] = 0; //有借位发生,置为0

                    else

                        V[0xF] = 1;

                    V[(opcode & 0x0F00) >> 8] = V[(opcode & 0x00F0) >> 4] - V[(opcode & 0x0F00) >> 8];//VX = VY - VX            

                    pc += 2;

                break;



                case 0x000E: // 0x8XYE: VX左移一位. VF设为VX左移前的最高位

                    V[0xF] = V[(opcode & 0x0F00) >> 8] >> 7;//取出最高位

                    V[(opcode & 0x0F00) >> 8] <<= 1;//左移一位

                    pc += 2;

                break;



                default:

                    printf ("Unknown opcode [0x8000]: 0x%X\n", opcode);

            }

        break;

        

        case 0x9000: //0x9XY0: 跳过接下去的指令,如果VX != VY

            if(V[(opcode & 0x0F00) >> 8] != V[(opcode & 0x00F0) >> 4])

                pc += 4;

            else

                pc += 2;

        break;



        case 0xA000: // ANNN: I = NNN

            I = opcode & 0x0FFF;

            pc += 2;

        break;

        

        case 0xB000: // BNNN: 跳转到 NNN + V0

            pc = (opcode & 0x0FFF) + V[0];

        break;

        

        case 0xC000: // CXNN: VX = random number & NN

            V[(opcode & 0x0F00) >> 8] = (rand() % 0xFF) & (opcode & 0x00FF);

            pc += 2;

        break;

    

        case 0xD000: // 在(VX, VY)坐标处画一个像素宽度固定为8,像素高度为N的小图案。这个图案在内存中保存于I(索引寄存器中保存的值),

                     //每个字节的8位刚好表示8个像素(1个像素对应1位),比如I中保存着图案第一行的8个像素,I+1保存着图案第二行的8个像素,以此类推。

                     //执行此opcode,并不会改变I的值。如果屏幕上任何点的状态由于此绘画从1变为了0,则VF要设为1,否则VF为0.

                     //它会比较当前希望设置像素的值(I中的值)与当前在屏幕上显示的值(显示缓存gfx[]中的值)是否一致,如果两者不同,则置为1,否则为0。 

        {

            unsigned short x = V[(opcode & 0x0F00) >> 8];

            unsigned short y = V[(opcode & 0x00F0) >> 4];//取得x,y(横纵坐标)

            unsigned short height = opcode & 0x000F;//取得图案的高度

            unsigned short pixel;



            V[0xF] = 0;//初始化VF为0

            for (int yline = 0; yline < height; yline++)//对于每一行

            {

                pixel = memory[I + yline];//取得内存I处的值,pixel中包含了一行的8个像素

                for(int xline = 0; xline < 8; xline++)//对于1行中的8个像素

                {

                    if((pixel & (0x80 >> xline)) != 0)//检查当前像素是否为1

                    {

                        if(gfx[(x + xline + ((y + yline) * 64))] == 1)//如果显示缓存gfx[]里该像素也为1,则发生了碰撞(64是CHIP8的显示宽度)

                        {

                            V[0xF] = 1;//设置VF为1                                

                        }

                        gfx[x + xline + ((y + yline) * 64)] ^= 1;//gfx中用1个byte来表示1个像素,其值为1或0。这里异或相当于取反

                    }

                }

            }

                        

            drawFlag = true;//绘画标志置为1        

            pc += 2;

        }

        break;

            

        case 0xE000:

            switch(opcode & 0x00FF)

            {

                case 0x009E: // EX9E: 如果VX中保存的按键被按下,则跳过下条指令

                    if(key[V[(opcode & 0x0F00) >> 8]] != 0)

                        pc += 4;

                    else

                        pc += 2;

                break;

                

                case 0x00A1: // EXA1: 如果VX中保存的按键没有被按下,则跳过下条指令

                    if(key[V[(opcode & 0x0F00) >> 8]] == 0)

                        pc += 4;

                    else

                        pc += 2;

                break;



                default:

                    printf ("Unknown opcode [0xE000]: 0x%X\n", opcode);

            }

        break;

        

        case 0xF000:

            switch(opcode & 0x00FF)

            {

                case 0x0007: // FX07: VX = delay计时器

                    V[(opcode & 0x0F00) >> 8] = delay_timer;

                    pc += 2;

                break;

                                

                case 0x000A: // FX0A: 如果有按键信息,存入VX        

                {

                    bool keyPress = false;



                    for(int i = 0; i < 16; ++i)

                    {

                        if(key[i] != 0)

                        {

                            V[(opcode & 0x0F00) >> 8] = i;

                            keyPress = true;//这里只存了最后一个按下的按键到VX

                        }

                    }



                    //如果没有按键按下,则返回

                    if(!keyPress)                        

                        return;



                    pc += 2;                    

                }

                break;

                

                case 0x0015: // FX15: delay_timer = VX

                    delay_timer = V[(opcode & 0x0F00) >> 8];

                    pc += 2;

                break;



                case 0x0018: // FX18: sound_timer = VX

                    sound_timer = V[(opcode & 0x0F00) >> 8];

                    pc += 2;

                break;



                case 0x001E: // FX1E: VX += I,如有溢出(I+VX>0xFFF)则VF置为1

                    if(I + V[(opcode & 0x0F00) >> 8] > 0xFFF)

                        V[0xF] = 1;

                    else

                        V[0xF] = 0;

                    I += V[(opcode & 0x0F00) >> 8];

                    pc += 2;

                break;



                case 0x0029: // FX29: 把I设为VX中字符对应的字体集的起始位置. 字符'0'-'F'每个是由4x5的像素矩阵组成

                    I = V[(opcode & 0x0F00) >> 8] * 0x5;

                    pc += 2;

                break;



                case 0x0033: // FX33: 把VX的十进制的表示存于I/I+1/I+2三个地址,其中I为百位,I+1为十位,I+2为个位。

                    memory[I]     = V[(opcode & 0x0F00) >> 8] / 100;//取得十进制百位

                    memory[I + 1] = (V[(opcode & 0x0F00) >> 8] / 10) % 10;//取得十进制十位

                    memory[I + 2] = (V[(opcode & 0x0F00) >> 8] % 100) % 10;//取得十进制个位            

                    pc += 2;

                break;



                case 0x0055: // FX55: 把V0-VX依次存入内存中I起始的地方                    

                    for (int i = 0; i <= ((opcode & 0x0F00) >> 8); ++i)

                        memory[I + i] = V[i];    



                    //在原解释器中,当这个操作完成的时候, I = I + X + 1.

                    I += ((opcode & 0x0F00) >> 8) + 1;

                    pc += 2;

                break;



                case 0x0065: // FX65: 把VX-VX依次设为内存中I起始的地方的值(与FX55操作相反)                    

                    for (int i = 0; i <= ((opcode & 0x0F00) >> 8); ++i)

                        V[i] = memory[I + i];            



                    //在原解释器中,当这个操作完成的时候, I = I + X + 1.

                    I += ((opcode & 0x0F00) >> 8) + 1;

                    pc += 2;

                break;



                default:

                    printf ("Unknown opcode [0xF000]: 0x%X\n", opcode);

            }

        break;



        default:

            printf ("Unknown opcode: 0x%X\n", opcode);

    }    



    //计时器--

    if(delay_timer > 0)

        -- delay_timer;



    //计时器--

    if(sound_timer > 0)

    {

        //if(sound_timer == 1) printf("beep\n");

        -- sound_timer;

    }    

}



void chip8::debugRender()//用于debug,用控制台方式输出显存中的值

{

    for(int y = 0; y < 32; ++y)

    {

        for(int x = 0; x < 64; ++x)

        {

            if(gfx[(y*64) + x] == 0) 

                printf("O");

            else 

                printf(" ");

        }

        printf("\n");

    }

    printf("\n");

}



bool chip8::loadApplication(const char * filename)//加载ROM(相当于代码段)到0x200的地方(见memory map)

{

    init();

    printf("Loading: %s\n", filename);

        

    //打开rom文件(rb模式下读到什么返回什么,读到文件末尾才会返回EOF)

    FILE * pFile = fopen(filename, "rb");

    if (pFile == NULL)

    {

        fputs ("File error", stderr); 

        return false;

    }



    //获取文件大小

    fseek(pFile , 0 , SEEK_END);//把pFile的指向从文件首部移到文件尾部

    long lSize = ftell(pFile);//返回文件的大小

    rewind(pFile);//让pFile的重新指向头部

    printf("Filesize: %d\n", (int)lSize);

    

    //新建同样大小的缓存

    char * buffer = (char*)malloc(sizeof(char) * lSize);

    if (buffer == NULL) 

    {

        fputs ("Memory error", stderr); 

        return false;

    }



    //把ROM文件拷入缓存

    size_t result = fread (buffer, 1, lSize, pFile);

    if (result != lSize) 

    {

        fputs("Reading error",stderr); 

        return false;

    }



    //把缓存拷入Chip8的内存的指定位置(0x200 = 512)

    if((4096-512) > lSize)//判断是否有足够空间读取rom(4096字节是总内存,512是存放ROM的起始位置)

    {

        for(int i = 0; i < lSize; ++i)

            memory[i + 512] = buffer[i];

    }

    else

        printf("Error: ROM too big for memory");

    

    //关闭文件,释放缓存

    fclose(pFile);

    free(buffer);



    return true;

}

 

main.cpp的分析请见第三篇

你可能感兴趣的:(模拟器)