Monday, 14 March 2011

Game Style Object control in Qt

It is quite typical in a game to need to control an object by keeping a key pressed and for it to respond to these pressed key continuously. The following example demonstrates this in action as shown in the following video

video
Space Ship class
The following class diagram shows the basic space ship class we are going to move
The constructor of the class is very simple, it loads in the mesh and sets the member variables to default values as shown

SpaceShip::SpaceShip(
                     ngl::Vector _pos,
                     std::string _fname
                     )
 {
   m_pos=_pos;
   m_mesh = new ngl::Obj(_fname);
   m_mesh->createVBO(GL_STATIC_DRAW);
   m_rotation=0;
 }

To draw the ship we move our transform stack position to the correct location and rotation and draw the VBO from the mesh
void SpaceShip::draw()
{
  ngl::ShaderManager *shader=ngl::ShaderManager::instance();
  (*shader)["Blinn"]->use();
  m_transform.getCurrentTransform().setPosition(m_pos);
  m_transform.setRotation(0,m_rotation,0);
  m_transform.loadGlobalAndCurrentMatrixToShader("Blinn","ModelMatrix");
  m_mesh->draw();
}
To update the ships position m_pos and rotation we use the following methods
const static float s_xExtents=40;
const static float s_yExtents=30;

void SpaceShip::move(
                      float _x,
                      float _y
                    )
{
 float currentX=m_pos.m_x;
 float currentY=m_pos.m_y;
 m_pos.m_x+=_x;
 m_pos.m_y+=_y;
 if(m_pos.m_x<=-s_xExtents || m_pos.m_x>=s_xExtents)
 {
  m_pos.m_x=currentX;
 }


 if(m_pos.m_y<=-s_yExtents || m_pos.m_y>=s_yExtents)
 {
  m_pos.m_y=currentY;
 }
}

void SpaceShip::rotate(
                        float _rot
                      )
{
 m_rotation+=_rot;
}

The move method, first stores the current x and y values of m_pos, we then increment the values based on the parameter passed in and check agains the extents of the screen. If we are not at the edge of the screen as set in these extents we add the values passed in to the value of the m_pos attribute.
For the rotation the value is just added.

Processing Key presses

Each subclass of the QWidget class has access to the following virtual protected methods


void QWidget::keyPressEvent ( QKeyEvent * event ) ;
void QWidget::keyReleaseEvent ( QKeyEvent * event ); 

Which ever QtWindow has focus will receive these events and the current key can be queried via the event parameter passed in. In this example the MainWindow class processes the key events as we wish to allow the window to become fullscreen and this method can only be called from a class that inherits from QMainWindow The following code shows how the key press and release methods are implemented

void MainWindow::keyPressEvent(
                               QKeyEvent *_event
                              )
{
  // this method is called every time the main window recives a key event.
  switch (_event->key())
  {
  case Qt::Key_Escape : QApplication::exit(EXIT_SUCCESS); break;
  case Qt::Key_W : glPolygonMode(GL_FRONT_AND_BACK,GL_LINE); break;
  case Qt::Key_S : glPolygonMode(GL_FRONT_AND_BACK,GL_FILL); break;
  case Qt::Key_F : showFullScreen(); break;
  case Qt::Key_N : showNormal(); break;
  default : break;
  }
 // once we have processed any key here pass the event
 // onto the GLWindow class to do more processing
  m_gl->processKeyDown(_event);
}

void MainWindow::keyReleaseEvent(
                 QKeyEvent *_event
                 )
{
 // once we have processed any key here pass the event
 // onto the GLWindow class to do more processing
  m_gl->processKeyUp(_event);

}
Once any keys required in the MainWindow class are processed the key events are then passed onto the GLWindow class for more processing.
GLWindow key processing
To process the keys in the GLWindow class we are going to create a QSet to contain the active key strokes, for each keyPress event (key down) we will add the key to the QSet using the += operator. Every time is released the key will be removed from the QSet using the -= operator. This is shown in the following code

// in GLWindow.h
/// @brief the keys being pressed
QSet< Qt::Key> m_keysPressed;

// in GLWindow.cpp
void GLWindow::processKeyDown(
                QKeyEvent *_event
               )
{
 // add to our keypress set the values of any keys pressed
 m_keysPressed += (Qt::Key)_event->key();

}


void GLWindow::processKeyUp(
                QKeyEvent *_event
               )
{
 // remove from our key set any keys that have been released
 m_keysPressed -= (Qt::Key)_event->key();
}



Now we will create an method to process the key values stored in the set and update the ship.
void GLWindow::moveShip()
{
 /// first we reset the movement values
 float xDirection=0.0;
 float yDirection=0.0;
 float rotation=0.0;
 // now we loop for each of the pressed keys in the the set
 // and see which ones have been pressed. If they have been pressed
 // we set the movement value to be an incremental value
 foreach(Qt::Key key, m_keysPressed)
 {
  switch (key)
  {
   case Qt::Key_Left :  { xDirection=s_shipUpdate; break;}
   case Qt::Key_Right : { xDirection=-s_shipUpdate; break;}
   case Qt::Key_Up :   { yDirection=s_shipUpdate; break;}
   case Qt::Key_Down :  { yDirection=-s_shipUpdate; break;}
   case Qt::Key_R :     { rotation=1.0; break;}
   default : break;
  }
 }
 // if the set is non zero size we can update the ship movement
 // then tell openGL to re-draw
 if(m_keysPressed.size() !=0)
 {
  m_ship->move(xDirection,yDirection);
  m_ship->rotate(rotation);
 }
}

First we set the x,y and rotation values to 0 to indicate there is no movement to the ship, next we loop through all the keys in the QSet using the foreach iterator provided by Qt. We then check to see if a key is active and update the direction variable based on the static variable s_shipUpdate. Depending upon the direction we set this to +/-s_shipUpdate.
Finally we check to see if there are any keys active and if so update the ship direction by calling the move function.

Tying it all together
Finally we want to separate the movement and draw commands so we can get a fixed update for drawing. To do this we create two timers one which will trigger the ship move update and one for the re-draw. This is done with the following code, first in the constructor we start the timer.
m_updateShipTimer=startTimer(2);
m_redrawTimer=startTimer(10);


Next in the timerEvent in GLWindow we process and dispatch if we get the correct timer event as follows
void GLWindow::timerEvent(
                          QTimerEvent *_event
                         )
{
 // the usual process is to check the event timerID and compare it to
 // any timers we have started with startTimer
 if (_event->timerId() == m_updateShipTimer)
 {
  moveShip();
 }
 if (_event->timerId() == m_redrawTimer)
 {
  updateGL();
 }

}


The full code for this demo can be downloaded from here using the command bzr branch http://nccastaff.bmth.ac.uk/jmacey/Code/GameKeyControl 

No comments:

Post a Comment