基于glut的OpenGL框架(二)
——加入键盘控制
前面一篇文章我向大家介绍了我自己制作的基于glut的OpenGL框架。接下来我们要加入交互的内容,说到交互,其实就是键盘的控制了。我们会在这一次体会到面向对象机制给我们带来的巨大便利以及glut给我们带来的诸多方便。
组件化glut按键处理功能,这是一个好主意。在这个想法下,我将glut按键处理的功能写到一个类,叫作KeyEvent。下面是我们这个类的定义:
#ifndef KEYEVENT_H #define KEYEVENT_H #define _MAX_KEY_NUM_ 256 class KeyEvent { public: KeyEvent( void ); void KeyDown( int key ); void KeyUp( int key ); protected: char m_KeyState[_MAX_KEY_NUM_]; }; #endif // KEYEVENT_H
这个类非常简单,包含了一个构造函数、按键按下时的函数以及按键弹上时的函数。私有成员为256个按键。定义_MAX_KEY_NUM_为256是因为考虑键盘一般有108个按键,而一个字节最多可以保存28即256中状态。所以就定义了256。以后为了适应不同的情况,可以将这个宏的值进行改变。
接下来我要向大家展示一个程序,并且希望大家能够编译运行再测试一下这个程序,看它的按键响应效果。(来自《OpenGL超级宝典》)
// Points.c // OpenGL SuperBible // Demonstrates OpenGL Primative GL_POINTS // Program by Richard S. Wright Jr. #include <GL/glut.h> #include <math.h> // Define a constant for the value of PI #define GL_PI 3.1415f // Rotation amounts static GLfloat xRot = 0.0f; static GLfloat yRot = 0.0f; // Called to draw scene void RenderScene(void) { GLfloat x,y,z,angle; // Storeage for coordinates and angles // Clear the window with current clearing color glClear(GL_COLOR_BUFFER_BIT); // Save matrix state and do the rotation glPushMatrix(); glRotatef(xRot, 1.0f, 0.0f, 0.0f); glRotatef(yRot, 0.0f, 1.0f, 0.0f); // Call only once for all remaining points glBegin(GL_POINTS); z = -50.0f; for(angle = 0.0f; angle <= (2.0f*GL_PI)*3.0f; angle += 0.1f) { x = 50.0f*sin(angle); y = 50.0f*cos(angle); // Specify the point and move the Z value up a little glVertex3f(x, y, z); z += 0.5f; } // Done drawing points glEnd(); // Restore transformations glPopMatrix(); // Flush drawing commands glutSwapBuffers(); } // This function does any needed initialization on the rendering // context. void SetupRC() { // Black background glClearColor(0.0f, 0.0f, 0.0f, 1.0f ); // Set drawing color to green glColor3f(0.0f, 1.0f, 0.0f); } void SpecialKeys(int key, int x, int y) { if(key == GLUT_KEY_UP) xRot-= 5.0f; if(key == GLUT_KEY_DOWN) xRot += 5.0f; if(key == GLUT_KEY_LEFT) yRot -= 5.0f; if(key == GLUT_KEY_RIGHT) yRot += 5.0f; if(key > 356.0f) xRot = 0.0f; if(key < -1.0f) xRot = 355.0f; if(key > 356.0f) yRot = 0.0f; if(key < -1.0f) yRot = 355.0f; // Refresh the Window glutPostRedisplay(); } void ChangeSize(int w, int h) { GLfloat nRange = 100.0f; // Prevent a divide by zero if(h == 0) h = 1; // Set Viewport to window dimensions glViewport(0, 0, w, h); // Reset projection matrix stack glMatrixMode(GL_PROJECTION); glLoadIdentity(); // Establish clipping volume (left, right, bottom, top, near, far) if (w <= h) glOrtho (-nRange, nRange, -nRange*h/w, nRange*h/w, -nRange, nRange); else glOrtho (-nRange*w/h, nRange*w/h, -nRange, nRange, -nRange, nRange); // Reset Model view matrix stack glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } int main(int argc, char* argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); glutCreateWindow("Points Example"); glutReshapeFunc(ChangeSize); glutSpecialFunc(SpecialKeys); glutDisplayFunc(RenderScene); SetupRC(); glutMainLoop(); return 0; }
运行一下,你会看到是一个螺线形(圆形),截图如下:
按上下左右可以移动螺线形,但是的细心的同学可能发现了,如果你按紧左键,它会连续地运动,但是其中有一个小小的停顿。这是由于这种机制下的按键是按照按下按键的ASCII码来响应的。不信大家可以打开gedit或者notepad,按紧a,你会发现第一个a和第二个a出现中间有一段停顿,而后面的a则较连贯地显示出来。这样的效果是不符合大多数游戏的交互体验的,而正是这样,我才开始了新的摸索,好在glut还提供了这样一个函数:glutKeyboardUpFunc,它可以对按键弹上进行回调。glutKeyboardUpFunc()函数的声明是这样的:
FGAPI void FGAPIENTRY glutKeyboardUpFunc( void (* callback)( unsigned char, int, int ) );
有了这个函数,我们就可以知道玩家按下一个按键有多久,可以实现一些游戏当中才能出现的按键效果,比如组合键、蓄气、二倍速跑、二段跳等。了解了这么多,我开始介绍KeyEvent类的实现了。
// KeyEvent.cpp 键盘事件的实现 // 19时35分01秒 最后编辑 #include <cstring> #include "KeyEvent.h" KeyEvent::KeyEvent( void ) { // 初始化这些按键 using namespace std; memset( m_KeyState, 0, _MAX_KEY_NUM_ ); } void KeyEvent::KeyDown( int key ) { m_KeyState[key] = 1; } void KeyEvent::KeyUp( int key ) { m_KeyState[key] = 0; }
在实现中,我们对108个按键的状态进行标识,以便使用的时候读取。KeyDown()函数对按下的按键置1,KeyUp()函数对弹出的按键置0。
现在将我们的按键功能集成到GLWidget类中吧。当然是使用继承啦。
#ifndef GLWIDGET_H #define GLWIDGET_H #include <assert.h> #include <GL/glut.h> #include "KeyEvent.h" class GLWidget: public KeyEvent { public: GLWidget( void ); ~GLWidget( void ); void Init( int width, int height ); void Release( void ); void Render( void ); void ProcessKey( void ); // 按键处理 void Reshape( int width, int height ); // 重新改变窗口大小 private: float m_X2; GLdouble m_Width, m_Height; GLdouble m_AspectRatio; }; #endif // GLWIDGET_H
以上是GLWidget类的定义,我们看到,该类继承自KeyEvent类,表示我们108个按键的状态都可以在GLWidget类的内部使用。此外定义了一个成员函数ProcessKey(),它专门用来处理按键的响应。
以下是GLWidget.cpp的实现:
// GLWidget.cpp 包含了控件的使用 // 11:14:27 最后编辑 #include "GLWidget.h" GLWidget::GLWidget( void ) { // 构造函数的代码在这里 m_Width = 0.0; m_Height = 0.0; m_X2 = 160.0f; } GLWidget::~GLWidget( void ) { Release( ); } void GLWidget::Init( int width, int height ) { // 保存初始化时窗口的宽和高 m_Width = GLdouble( width ); m_Height = GLdouble( height ); m_AspectRatio = m_Width / m_Height; // 初始化代码 glClearColor( 0.0, 0.0, 0.0, 1.0 ); } void GLWidget::Render( void ) { // 渲染代码 glColor3ub( 255, 0, 0 ); glRectf( 0.0f, 0.0f, m_X2, 180.0f ); glRectf( 160.0f, 260.0f, 180.0f, 280.0f ); } void GLWidget::Release( void ) { // 释放空间代码 } void GLWidget::Reshape( int width, int height ) { // 改变大小时程序如何应对? GLdouble aspectRatio = GLdouble( width ) / GLdouble( height ); // 设置视口 if ( aspectRatio < m_AspectRatio ) { GLint smallHeight = GLint( GLdouble( width ) / m_AspectRatio ); GLint heightBlank = ( GLint( height ) - smallHeight ) / 2; glViewport( 0, heightBlank, GLint( width ), smallHeight ); } else { GLint smallWidth = GLint( GLdouble( height ) * m_AspectRatio ); GLint widthBlank = ( GLint( width ) - smallWidth ) / 2; glViewport( widthBlank, 0, smallWidth, GLint( height ) ); } glMatrixMode( GL_PROJECTION ); glLoadIdentity( ); // 设置裁剪区域(左右下上近远) glOrtho( 0.0, m_Width, 0.0, m_Height, -10.0, 10.0 ); // 为模型视图载入标准矩阵 glMatrixMode( GL_MODELVIEW ); glLoadIdentity( ); } void GLWidget::ProcessKey( void ) { // 注意:这里大写的B和小写的b是不一样的。如果你开启了caps lock键,那么按下b键就会有效,否则无效。 if ( m_KeyState['b'] ) { m_X2 += 0.5f; } if ( m_KeyState['x'] ) { delete this; exit( 0 ); } }
前面的函数我在第一节和大家都介绍了,下面我介绍一下ProcessKey()函数。ProcessKey()函数体内包含了两条if语句,表示对按键b和x的处理。这里注意,B和b的意义不一样,要响应B键,要在CapsLock键按下才有效。此外,当我们按下x键后,将会退出程序。不必担心deletethis语句,因为在这条语句后,我们不会访问this下的任何数据成员,我们会直接调用exit(0 )退出。
最后,让我们再看看main.cpp是什么样子吧。
// main.cpp // 11时08分52秒 最后编辑 #include "GLWidget.h" // 宽屏的程序要求纵横比16:9,我们指定高,宽就出来了。 #define _WINDOW_HEIGHT_ 360 #define _WINDOW_WIDTH_ _WINDOW_HEIGHT_ * 16 / 9 static GLWidget* pWidget = 0; void Reshape( int x, int y ) { assert( pWidget != 0 ); pWidget->Reshape( x, y ); } void Render( void ) { glClear( GL_COLOR_BUFFER_BIT ); // 用黑色清屏 glColor3ub( 255, 255, 255 ); glRecti( 0, 0, _WINDOW_WIDTH_, _WINDOW_HEIGHT_ );// 绘制白色的矩形背景 // 执行widget里的绘图函数 assert( pWidget != 0 ); pWidget->Render( ); // 交换缓存 glutSwapBuffers( ); } void Idle( void ) // 空转时候运行的函数 { assert( pWidget != 0 ); // 处理按键 pWidget->ProcessKey( ); // 如果有必要的话,让其更新 glutPostRedisplay( ); } void KeyDown( unsigned char key, int, int ) { assert( pWidget != 0 ); pWidget->KeyDown( key ); } void KeyUp( unsigned char key, int, int ) { assert( pWidget != 0 ); pWidget->KeyUp( key ); } int main( int argc, char** argv ) { // 初始化控件类 pWidget = new GLWidget; glutInit( &argc, argv ); glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH ); glutInitWindowSize( _WINDOW_WIDTH_, _WINDOW_HEIGHT_ ); glutCreateWindow( "Simple Object" ); glutDisplayFunc( Render ); // 渲染函数 glutReshapeFunc( Reshape ); // 重改变形状函数 //glutSpecialFunc( Special ); // 特殊函数 glutKeyboardFunc( KeyDown ); // 键盘按下函数 glutKeyboardUpFunc( KeyUp ); // 键盘按上函数 glutIdleFunc( Idle ); // 空转时运行的函数 pWidget->Init( _WINDOW_WIDTH_, _WINDOW_HEIGHT_ ); glutMainLoop( ); return 0; }
实现连续响应按键的关键一步,就是将响应按键的内容放入Idle()函数中,而Idle()函数在程序空转的时候执行按键的响应和画面的渲染。此外,在按键按下的时候,我们通过调用GLWidget::KeyDown()和GLWidget::KeyUp()函数记录下按键的状态,从而保证按键状态是最新的。
好了,激动人心的一刻到来了,我们要运行我们的源程序,看看它对于响应按键的强大吧。
我们按下b键,可以看到下面红色矩形逐渐变宽,这种变化是实时的。而按下x键则退出程序。
<style type="text/css"> <!-- @page {margin:2cm} p {margin-bottom:0.21cm} --> </style>(程序源代码下载地址在这里)