手把手教你编写游戏模拟器 - Chip8篇(3)
翻译整理分析:by Yiran Xie
*如要转载请附上本文链接
书接上文(第二篇),下面简单讨论下chip8模拟器剩余的main.cpp文件。main中包含了opengl的glut编程来实现图像与输入系统,我对这块一点经验也没有,所以也是摸着石头过河。
首先安装opengl的glut库,简单地总结一下吧。glut库可能已经过时,不过好在还能用。
下载地址http://www.opengl.org/resources/libraries/glut/glutdlls37beta.zip
Windows+ VC环境下安装的步骤:
1、将下载的压缩包解开,将得到5个文件
2、把gl.h放到visual studio的vc的include下,例如$\Microsoft Visual Studio 9.0\VC\include
3、将glut.lib和glut32.lib放到静态函数库所在文件夹,例如$\Microsoft Visual Studio 9.0\VC\lib
4、将glut.dll和glut32.dll放到windows\system32下
最后,当前project->linker->input->Additional Dependencies,加入glut32.lib glu32.lib。这样应该就能顺利编译了.
具体main.cpp代码如下,我做了下删减(去除了一些typedef和被作者抛弃的旧版的函数)这样看着更清楚一些。
/////////////////////////////////////////////////////////////////////////////// // 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 <stdio.h> #include <stdlib.h> #include <glut.h> #include "chip8.h" // Display size #define SCREEN_WIDTH 64 #define SCREEN_HEIGHT 32 chip8 myChip8;//作为全局变量 int ratio = 10;//window窗口大小和实际模拟器像素之间的比例 //windows窗口大小 int display_width = SCREEN_WIDTH * ratio; int display_height = SCREEN_HEIGHT * ratio; void display(); void reshape_window(GLsizei w, GLsizei h); void keyboardUp(unsigned char key, int x, int y); void keyboardDown(unsigned char key, int x, int y); unsigned char screenData[SCREEN_HEIGHT][SCREEN_WIDTH][3]; void setupTexture(); void DisplayAndSleep() { display(); _sleep(2); } int main(int argc, char **argv) { if(argc < 2) { printf("Usage: myChip8.exe chip8application\n\n"); return 1; } //读取rom if(!myChip8.loadApplication(argv[1])) return 1; // Setup OpenGL glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA); glutInitWindowSize(display_width, display_height); glutInitWindowPosition(320, 320); glutCreateWindow("myChip8 by Laurence Muller"); glutDisplayFunc(display); glutIdleFunc(DisplayAndSleep); glutReshapeFunc(reshape_window); glutKeyboardFunc(keyboardDown); glutKeyboardUpFunc(keyboardUp); setupTexture(); glutMainLoop(); return 0; } // Setup Texture void setupTexture() { // Clear screen for(int y = 0; y < SCREEN_HEIGHT; ++y) for(int x = 0; x < SCREEN_WIDTH; ++x) screenData[y][x][0] = screenData[y][x][1] = screenData[y][x][2] = 0; // Create a texture glTexImage2D(GL_TEXTURE_2D, 0, 3, SCREEN_WIDTH, SCREEN_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, (GLvoid*)screenData); // Set up the texture glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); // Enable textures glEnable(GL_TEXTURE_2D); } void updateTexture(const chip8& c8) { // Update pixels for(int y = 0; y < 32; ++y) for(int x = 0; x < 64; ++x) if(c8.gfx[(y * 64) + x] == 0) screenData[y][x][0] = screenData[y][x][1] = screenData[y][x][2] = 0; // Disabled else screenData[y][x][0] = screenData[y][x][1] = screenData[y][x][2] = 255; // Enabled // Update Texture glTexSubImage2D(GL_TEXTURE_2D, 0 ,0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, GL_RGB, GL_UNSIGNED_BYTE, (GLvoid*)screenData); glBegin( GL_QUADS ); glTexCoord2d(0.0, 0.0); glVertex2d(0.0, 0.0); glTexCoord2d(1.0, 0.0); glVertex2d(display_width, 0.0); glTexCoord2d(1.0, 1.0); glVertex2d(display_width, display_height); glTexCoord2d(0.0, 1.0); glVertex2d(0.0, display_height); glEnd(); } void display() { myChip8.emulateCycle(); if(myChip8.drawFlag) { // Clear framebuffer glClear(GL_COLOR_BUFFER_BIT); updateTexture(myChip8); // Swap buffers! glutSwapBuffers(); // Processed frame myChip8.drawFlag = false; } } void reshape_window(GLsizei w, GLsizei h) { glClearColor(0.0f, 0.0f, 0.5f, 0.0f); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0, w, h, 0); glMatrixMode(GL_MODELVIEW); glViewport(0, 0, w, h); // Resize quad display_width = w; display_height = h; } void keyboardDown(unsigned char key, int x, int y)//更新按键按下信息 { if(key == 27)//如果是esc按键,则退出。否则,更新相应chip8.key[]中的对应按键 exit(0); /*人为定义的键盘布局如下: 1234 qwer asdf zxcv*/ if(key == '1') myChip8.key[0x1] = 1; else if(key == '2') myChip8.key[0x2] = 1; else if(key == '3') myChip8.key[0x3] = 1; else if(key == '4') myChip8.key[0xC] = 1; else if(key == 'q') myChip8.key[0x4] = 1; else if(key == 'w') myChip8.key[0x5] = 1; else if(key == 'e') myChip8.key[0x6] = 1; else if(key == 'r') myChip8.key[0xD] = 1; else if(key == 'a') myChip8.key[0x7] = 1; else if(key == 's') myChip8.key[0x8] = 1; else if(key == 'd') myChip8.key[0x9] = 1; else if(key == 'f') myChip8.key[0xE] = 1; else if(key == 'z') myChip8.key[0xA] = 1; else if(key == 'x') myChip8.key[0x0] = 1; else if(key == 'c') myChip8.key[0xB] = 1; else if(key == 'v') myChip8.key[0xF] = 1; //printf("Press key %c\n", key); } void keyboardUp(unsigned char key, int x, int y)//更新按键释放信息 { if(key == '1') myChip8.key[0x1] = 0; else if(key == '2') myChip8.key[0x2] = 0; else if(key == '3') myChip8.key[0x3] = 0; else if(key == '4') myChip8.key[0xC] = 0; else if(key == 'q') myChip8.key[0x4] = 0; else if(key == 'w') myChip8.key[0x5] = 0; else if(key == 'e') myChip8.key[0x6] = 0; else if(key == 'r') myChip8.key[0xD] = 0; else if(key == 'a') myChip8.key[0x7] = 0; else if(key == 's') myChip8.key[0x8] = 0; else if(key == 'd') myChip8.key[0x9] = 0; else if(key == 'f') myChip8.key[0xE] = 0; else if(key == 'z') myChip8.key[0xA] = 0; else if(key == 'x') myChip8.key[0x0] = 0; else if(key == 'c') myChip8.key[0xB] = 0; else if(key == 'v') myChip8.key[0xF] = 0; }
现在原作者的代码中似乎缺少了对于帧数的控制,cpu几乎在全力运行,帧数太高。代码理解起来难度不大,主要的还是对于glut32相关库函数的熟悉,这块我目前了解的极少,打算有空时进一步研究一下(有待继续更新)。
不过到了这一步,openGL or glut32并不是唯一的选择,用SDL,DirectX或者其他库也能实现同样的功能。
举个例子,我前两天写了个c的版本,已经成功移植到了自己的x86 OS内核上(这个内核在编写时参考了Osask和OrangeOS,留待以后再细说)。主要是用到了文件读取\释放、窗口新建、绘画与刷新、定时器、按键读取这些API。换句话说,只要任何库能实现这些API,都可以用来作为glut32的替代实现。
具体来说,先对模拟器结构体进行初始化,接着读取rom,之后就是主loop,loop中更新按键状态、执行emulateCycle,最后根据drawflag去选择是否绘画(如要绘画,把gfx[]中的值映射到窗体上对应像素即可)。
其中有几点是我在编写过程中额外注意到的,一是要想办法去降低帧数(使之趋近于60hz),比如我是用了计时器;二是如果你用的是像我这样的山寨的显示API的话(即没有做过任何优化),那么有可能编写不当会出现大面积闪烁的情况。我的解决办法是尽量不要全局刷新,而仅作局部刷新。实践上,维持一个gfx_old[]数组,每次比对gfx[]与gfx_old[],仅刷新发生了变化的点(从亮变暗或从暗变亮的点)
最后补一张,chip8模拟器跑在我的OS内核上的图。图为同时开启三个进程,运行三款不同的ROM