Click here to Skip to main content
Click here to Skip to main content

TetroGL: An OpenGL Game Tutorial in C++ for Win32 Platforms - Part 1

, 26 Aug 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
Learn to create a Win32 message loop and game window and how to set-up OpenGL properly for 2D games

Foreword

This series of articles focuses on 2D game development with C++ and OpenGL for Windows platform. The target is to provide a game like the classic block puzzle game by the end of the series. We will not only focus on OpenGL but also talk about the designs that are commonly used in game programming with a full object oriented approach. You should already be familiar with the C++ language in order to get the maximum out of this series. There is a message board at the bottom of the article that you can use if you have questions, remarks or suggestions.

The series is divided into three articles:

  • Part 1: covers the win32 message loop, the window creation and the setting-up of OpenGL. You will also learn how to draw some simple shapes.
  • Part 2 : Covers resources handling and displaying simple animations.
  • Part 3: groups everything together and talk about the game logic.

Contents

Introduction

This part of the article focuses on setting up an OpenGL window in a Windows environment. We will learn how to create a message loop to receive notifications and how to create the main window that will be used for drawing. Then, we will see how to configure OpenGL properly for a 2 dimensions game. Finally, when everything is ready to start, we will learn how to display some basic shapes in the newly created OpenGL window.

Project Settings

We will start by creating a new project and configuring the different options. The tutorial project has been created with Visual Studio 2005 but it can be easily applied for another compiler. Start by creating a new project of type "Win32 Console Application" and giving it an appropriate name, then click Ok. In the creation wizard, select the type "Windows application" (not console) and check the "Empty project" option (we don't really need code that is generated for us).

When this is done, add a new source file Main.cpp to the project (if there are no source files in the project, some options are not accessible). Now open the project options and go to the "Linker" category -> "Input". In the "Addition Dependencies" option, add opengl32.lib. This tells the linker that it has to use the OpenGL library when linking the project.

New project

Next, we will disable UNICODE because we don't need it and it makes things a bit more complicated. Go into "C/C++" -> "Preprocessor" and click on "Preprocessor Definitions". A button will appear on the right, click on it and in the dialog that pops up, uncheck the "Inherit from parent or project defaults". This will disable UNICODE which is inherited from the project default.

New project

Now that the project settings are properly configured, we are ready to look at some code. Let's first examine how a Win32 application receives and processes events (keyboard, mouse, ...).

The Message Loop

The system (Windows) creates a message queue for each application and pushes messages in this queue whenever an event occurs on a window of that specific application. Your application should then retrieve and process those messages in order to react upon them. This is what is called the message loop and it is the heart of all Win32 applications.

A typical message loop looks like this:

    MSG Message;
    Message.message = (~WM_QUIT);
    // Loop until a WM_QUIT message is received
    while (Message.message != WM_QUIT)
    {
        if (PeekMessage(&Message, NULL, 0, 0, PM_REMOVE))
        {
            // If a message was waiting in the message queue, process it
            TranslateMessage(&Message);
            DispatchMessage(&Message);
        }
        else
        {
            // Do processing stuff here...
        }
    }

PeekMessage retrieves a message from the queue if any; the PM_REMOVE tells PeekMessage that messages should be removed from the queue. The message will be stored in the first argument and the function returns nonzero if a message was retrieved. The second argument of the function lets you specify a window handle for which the messages have to be retrieved. If NULL is supplied, messages for all windows of the application will be retrieved. The third and fourth parameters let you specify a range for the messages that should be retrieved. If 0 is supplied for both, all messages will be retrieved.
The purpose of the TranslateMessage function is to translate virtual-keys messages (WM_KEYDOWN and WM_KEYUP) into character messages (WM_CHAR). A WM_CHAR message will be generated by a combination of WM_KEYDOWN and WM_KEYUP messages.
Finally the DispatchMessage will redirect the message to the correct window procedure. As we will see later, each window in your application has a specific function (called a window procedure) that processes those messages.
So, this snippet of code tries to extract a message from the queue. If a message was available, it will be dispatched to the correct window procedure. If no message was available, we do some processing specific to the application. Once a WM_QUIT message is retrieved, the loop is exited, which terminates the application.

If we look at the code of this first tutorial, we can see that the message loop is wrapped into a class called CApplication. Let's take a closer look at this class. First the class declaration:

// The application class, which simply wraps the message queue and process
// the command line.
class CApplication
{
public:
  CApplication(HINSTANCE hInstance);
  ~CApplication();

  // Parses the command line to see if the application
  // should be in fullscreen mode.
  void ParseCmdLine(LPSTR lpCmdLine);
  // Creates the main window and starts the message loop.
  void Run();

private:
  HINSTANCE m_hInstance;
  // Specifies if the application has to be started in fullscreen
  // mode. This option is supplied through the command line
  // ("-fullscreen" option).
  bool m_bFullScreen;
};

The ParseCmdLine function is quite straightforward: it simply checks if an argument "-fullscreen" is present in the command line. In that case, the flag m_bFullScreen is set to true.

Let's look at the Run function:

void CApplication::Run()
{
  // Create the main window first
  CMainWindow mainWindow(800,600,m_bFullScreen);

    MSG Message;
    Message.message = ~WM_QUIT;
  DWORD dwNextDeadLine = GetTickCount() + FRAME_TIME;
  DWORD dwSleep = FRAME_TIME;
  bool bUpdate = false;

  // Loop until a WM_QUIT message is received
    while (Message.message != WM_QUIT)
    {
    // Wait until a message comes in or until the timeout expires. The
    // timeout is recalculated so that this function will return at
    // least every FRAME_TIME msec.
    DWORD dwResult = MsgWaitForMultipleObjectsEx(0,NULL,dwSleep,QS_ALLEVENTS,0);
    if (dwResult != WAIT_TIMEOUT)
    {
      // If the function returned with no timeout, it means that at 
      // least one message has been received, so process all of them.
      while (PeekMessage(&Message, NULL, 0, 0, PM_REMOVE))
      {
        // If a message was waiting in the message queue, process it
        TranslateMessage(&Message);
        DispatchMessage(&Message);
      }

      // If the current time is close (or past) to the 
      // deadline, the application should be processed.
      if (GetTickCount() >= dwNextDeadLine)
        bUpdate = true;
      else
        bUpdate = false;
    }
    else
      // On a timeout, the application should be processed.
      bUpdate = true;

    // Check if the application should be processed
    if (bUpdate)
    {
      DWORD dwCurrentTime = GetTickCount();
      // Update the main window
      mainWindow.Update(dwCurrentTime);
      // Draw the main window
      mainWindow.Draw();

      dwNextDeadLine = dwNextDeadLine + FRAME_TIME;
    }

    // Process the sleep time, which is the difference
    // between the current time and the next deadline.
    dwSleep =  dwNextDeadLine - GetCurrentTime();
    // If the sleep time is larger than the frame time,
    // it probably means that the processing was stopped 
    // (e.g. the window was being moved,...), so recalculate
    // the next deadline.
    if (dwSleep>FRAME_TIME)
    {
      dwSleep = FRAME_TIME;
      dwNextDeadLine = GetCurrentTime() + FRAME_TIME;
    }
  }
}

The first line of the function simply creates the main window. We will see in the next chapter what it does exactly. For now, just imagine that this creates and displays the main window with a specific width and height and in fullscreen or not. As you might see, the loop itself is a bit different than what we saw before. The reason is simple: in general for a 2D game, you don't need to refresh the screen as fast as you can. Refreshing it at a constant rate, is sufficient to display animation and do the processing stuff. In our case, we defined a constant (FRAME_TIME) that specifies the time in msec between two frames.
We could do something simpler: in the first message loop example we saw, we could replace the "// Do processing stuff here..." by a check to see if 30 msec elapsed since the last update:

        else
        {
            // Do processing stuff here...
            if(GetCurrentTime() >= dwLastUpdate+30)
            {
              dwLastUpdate = GetCurrentTime();
              // Update the main window
              mainWindow.Update(dwCurrentTime);
              // Draw the main window
              mainWindow.Draw();
            }
        }

That will work fine except for the fact that it is busy waiting: if no messages are received, we will loop continuously and eat all available CPU time. This is not really nice because the CPU is used for doing nothing.
A best approach would be to wait until a message arrives or until we reached the next refresh deadline. That's what the MsgWaitForMultipleObjectsEx function does. In brief, we can specify multiple objects on which we would like to wait, but we are only interested in messages (so, that's why we specify 0 objects in the first argument and a NULL for the second argument). This function will wait without consuming CPU cycles until either the timeout period expires (specified in the 3rd argument) or when a message has been received. You can specify a filter for messages to be received in the 4th parameter, but we are interested in all messages. When the function times out, it returns WM_TIMEOUT, which is used in the code to detect when it is time to refresh the screen and update the game logic. If the function didn't time out, it means that one or more messages are waiting in the queue, so we extract all of them using PeekMessage (the function returns FALSE when no messages are in the queue anymore). Whe then determine if the application should be processed or not. At the end of the function, we recalculate the sleep time depending on the next deadline. If this sleep time is bigger then the frame time, it means that the current time was bigger than the next deadline (negative overflow). This typically happens when the window is moved or resized: during this time, the application is not processed anymore. In that case, we simply recalculate a new deadline and sleep time based on the current time.

Great, so now we have a message loop to dispatch the messages to the correct window. But there's something missing: the window itself. So let's look at how this window is created and how the messages sent to it are processed.

The Main Window

Creating the Window

As we saw before, we only had to create an instance of the CMainWindow class in the Run() method of our application class to create the main window. So let's take a look at the constructor, that's where all the stuff is handled.

CMainWindow::CMainWindow(int iWidth, int iHeight, bool bFullScreen)
  :  m_hWindow(NULL), m_hDeviceContext(NULL), m_hGLContext(NULL),
     m_bFullScreen(bFullScreen)
{
  RegisterWindowClass();

  RECT WindowRect;
  WindowRect.top = WindowRect.left = 0;
  WindowRect.right = iWidth;
  WindowRect.bottom = iHeight;

  // Window Extended Style
  DWORD dwExStyle = 0;
  // Windows Style
  DWORD dwStyle = 0;

  if (m_bFullScreen)
  {
    DEVMODE dmScreenSettings;
    memset(&dmScreenSettings,0,sizeof(dmScreenSettings));
    dmScreenSettings.dmSize = sizeof(dmScreenSettings);
    dmScreenSettings.dmPelsWidth  = iWidth;
    dmScreenSettings.dmPelsHeight = iHeight;
    dmScreenSettings.dmBitsPerPel = 32;
    dmScreenSettings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;

    // Change the display settings to fullscreen. On error, throw
    // an exception.
    if (ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)
        != DISP_CHANGE_SUCCESSFUL)
    {
      throw CException("Unable to switch to fullscreen mode");
    }

    dwExStyle = WS_EX_APPWINDOW;
    dwStyle = WS_POPUP;
    // In fullscreen mode, we hide the cursor.
    ShowCursor(FALSE);
  }
  else
  {
    dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
    dwStyle = WS_OVERLAPPEDWINDOW;
  }

  // Adjust the window to the true requested size
  AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);
  // Now create the main window
  m_hWindow = CreateWindowEx(dwExStyle,TEXT(WINDOW_CLASSNAME),
               TEXT("Tutorial1"),
               WS_CLIPSIBLINGS | WS_CLIPCHILDREN | dwStyle,
               0, 0, WindowRect.right-WindowRect.left,
               WindowRect.bottom-WindowRect.top,
               NULL, NULL,
               GetModuleHandle(NULL),
               this);
  if (m_hWindow==NULL)
    throw CException("Cannot create the main window");

  CreateContext();
  InitGL();
  ShowWindow(m_hWindow,SW_SHOW);
  // Call OnSize manually because in fullscreen mode it will be
  // called only when the window is created (which is too early
  // because OpenGL is not initialized yet).
  OnSize(iWidth,iHeight);
}

It looks like a lot of code but it is not that complicated. The first thing we do is call RegisterWindowClass which will, as its name states, registers the window class for our application. So what is a window class? Basically, it is a template that is used to define a window: you can specify an icon, a background brush, a cursor and other things. Every window is an instance of such a class. Let's take a look at the implementation of this function:

void CMainWindow::RegisterWindowClass()
{
    WNDCLASS WindowClass;
    WindowClass.style         = 0;
    WindowClass.lpfnWndProc   = &CMainWindow::OnEvent;
    WindowClass.cbClsExtra    = 0;
    WindowClass.cbWndExtra    = 0;
    WindowClass.hInstance     = GetModuleHandle(NULL);
    WindowClass.hIcon         = NULL;
    WindowClass.hCursor       = 0;
    WindowClass.hbrBackground = 0;
    WindowClass.lpszMenuName  = NULL;
    WindowClass.lpszClassName = WINDOW_CLASSNAME;

    RegisterClass(&WindowClass);
}

What it does is register a new class instance (which is called Tutorial1) and the only thing we specify is the window procedure that will be called when messages are retrieved for that window. This is the OnEvent function of the class. If you look closely at the function declaration, you will notice that it is a static function. The reason for that is very simple: non-static member functions don't have the same prototype as global functions even if they have the same argument list. It is because an implicit parameter is passed to the function: the this parameter which identifies the instance of the class on which the function is called. Static member functions do not follow the same rule, because they don't belong to a specific instance (they are shared among all instances of the class). The WNDCLASS structure accepts only global or static member functions for the lpfnWndProc parameter. We will see later the consequences of that.

Now, back to the CMainWindow constructor. The next thing we do there is check if the window should be in fullscreen. If that is the case, we switch to fullscreen mode (by calling ChangeDisplaySettings). If this function call fails, we throw an exception. We will talk more in detail about exceptions and exception handling in a following chapter.

We will now create the main window but first, we need to adjust the rectangle size because the window caption and borders are eating up a bit of the size. To correct that, we simply call AdjustWindowRectEx. This function doesn't have any effect if we are in fullscreen mode. We finally call CreateWindowEx which will create the window with the required style. The second parameter of the function specifies the window class to use (which will of course be the window class we registered earlier). In the last parameter of the function, we pass the this pointer (the pointer to this CMainWindow instance). We will see later why we do so. If the window creation fails, we also throw an exception. The CreateContext and InitGL functions will initialize OpenGL properly, but we will see that in a following chapter.

The Window Procedure

We just created a new window by calling CreateWindowEx and we specified that the window should use the window class we registered earlier. This window class uses the OnEvent function as a window procedure. Let's take a look at this function:

LRESULT CMainWindow::OnEvent(HWND Handle, UINT Message, WPARAM wParam, LPARAM lParam)
{
  if (Message == WM_NCCREATE)
  {
        // Get the creation parameters.
    CREATESTRUCT* pCreateStruct = reinterpret_cast<CREATESTRUCT*>(lParam);

    // Set as the "user data" parameter of the window
    SetWindowLongPtr(Handle, GWLP_USERDATA,
          reinterpret_cast<long>(pCreateStruct->lpCreateParams));
  }

  // Get the CMainWindow instance corresponding to the window handle
  CMainWindow* pWindow = reinterpret_cast<CMainWindow*>
    (GetWindowLongPtr(Handle, GWLP_USERDATA));
  if (pWindow)
    pWindow->ProcessEvent(Message,wParam,lParam);

  return DefWindowProc(Handle, Message, wParam, lParam);
}

As you remember, this function is a static function. The function will be called when a message is received and dispatched to our main window. It accepts four parameters:

  • Handle: The handle of the window to which the message is sent to
  • Message: The message Id
  • wParam: Optional message parameter
  • lParam: Optional message parameter

Depending on the type of message, some additional information will be stored in the wParam, lParam or both (e.g. a mouse move message contains the mouse coordinates, a key down event contains the key code...).

As this function is static, we don't have access to other non-static class member, which is of course not very useful in our situation. But, don't panic, there's an easy solution for that, and it's the reason why we passed the this pointer in the last argument of CreateWindowEx. One of the first message that will be sent to your window procedure is the WM_NCCREATE message. When this message is received, the lParam argument contains a pointer to a CREATESTRUCT structure, which contains information about the window creation, which are in fact the parameters that were passed in the CreateWindowEx call. The lpCreateParams field contains the additional data, which is in our case the pointer to the CMainWindow instance. Unfortunately, this additional data is not sent with every message, so we need a way to store this pointer for later use. That's what we are doing by calling SetWindowLongPtr: this function lets you save some user data (GWLP_USERDATA) for a specific window (identified by its handle). In this case, we save the pointer to the class instance. When other messages are received, we will simply retrieve this pointer by calling (GetWindowLongPtr), and then call a non-static function on the pointer: ProcessEvent, which is in charge of processing the message. The WM_NCCREATE message is not the first one that is sent, that's why we need to check if the call to GetWindowLongPtr did return something else than NULL.

Let's look at the ProcessEvent function:

void CMainWindow::ProcessEvent(UINT Message, WPARAM wParam, LPARAM lParam)
{
    switch (Message)
    {
    // Quit when we close the main window
    case WM_CLOSE :
      PostQuitMessage(0);
      break;
    case WM_SIZE:
      OnSize(LOWORD(lParam),HIWORD(lParam));
      break;
    case WM_KEYDOWN :
      break;
    case WM_KEYUP :
      break;
    }
}

Not too much code here, but this function will be filled in the next tutorials as we need to handle some events. The WM_CLOSE message is sent when the user clicks on the red cross of the window. At this time, we need to send a WM_QUIT message in order to exit the main loop and quit the program. A WM_SIZE message is sent whenever the window is resized, with the new size contained in the lParam (LOWORD and HIWORD are two macros that extract the first 2 bytes and the last 2 bytes from the parameter). When such message is received, we delegate the resizing handling to our OnSize member function. Some other messages will be handled later: WM_KEYDOWN when a key is pressed, WM_KEYUP when a key is released, ...

Up to now, the only thing our program does is create an empty window and display it on the screen (in fullscreen mode or not).

Exception Handling

Error management is an important point for all programs, and this is also true for games: you don't want your game to crash because a resource is missing. My preferred way to handle errors for games is to use exceptions. It is much more convenient than returning error codes from functions (and routing them where I want the error to be handled). The main reason is that I can delegate the error handling in one single place: in my main function, where all my exceptions will be caught. Let's first take a look at our exception class, which is quite basic:

class CException : public std::exception
{
public:
  const char* what() const  { return m_strMessage.c_str(); }

  CException(const std::string& strMessage="") : m_strMessage(strMessage)  { }
  virtual ~CException()  { }

  std::string m_strMessage;
};

So, nothing fancy here: our exception class inherits from std::exception (which is not mandatory but is considered good practice). We simply override the what() function which returns the error message. I kept the scenario quite simple here, but for a bigger game, you might want to specialize this exception into specific ones: out of memory, resource missing, file loading failed, ... This could prove handy because sometimes it is useful to filter the exceptions. A typical example is when the user of your game wants to load a file (containing a previous saved game) which is corrupted. In that case, the load file function will throw an exception but you don't want to exit the program because of that. Displaying a message to the user telling him that the file is corrupted is what you would like to do. You can then easily catch all 'file corrupted' exceptions at an early stage and let all the others be routed to your main exception handling function. After all, if some resources are missing when loading the file, this is probably a critical error and you might want to exit the program.

So, how does my main function look like and how do I handle the exceptions ?

int WINAPI WinMain(HINSTANCE Instance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, INT)
{
  try
  {
    // Create the application class,
    // parse the command line and
    // start the app.
    CApplication theApp(Instance);
    theApp.ParseCmdLine(lpCmdLine);
    theApp.Run();
  }
  catch(CException& e)
  {
    MessageBox(NULL,e.what(),"Error",MB_OK|MB_ICONEXCLAMATION);
  }

  return 0;
}

Pretty easy to understand, isn't it ? We already saw what the CApplication class is doing and for the exception handling, we simply wrap everything inside a little try/catch block. When an exception is thrown somewhere in the program, we simply display an error message with the text of the exception and we nicely exit the program. Note that as theApp is local to the function, it will be destroyed at the end of the function and its destructor will be called.

Setting up OpenGL

If you remember, in our CMainWindow constructor, we were calling two functions: CreateContext and InitGL. I didn't explain yet what those functions do, so let's correct that now. CreateContext will initialize the rendering context so that OpenGL primitives can be drawn on the window:

void CMainWindow::CreateContext()
{
  // Describes the pixel format of the drawing surface
  PIXELFORMATDESCRIPTOR pfd;
  memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));
  pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
  pfd.nVersion = 1; // Version Number
  pfd.dwFlags = PFD_DRAW_TO_WINDOW |  // Draws to a window
                PFD_SUPPORT_OPENGL |  // The format must support OpenGL
                PFD_DOUBLEBUFFER;     // Support for double buffering
  pfd.iPixelType = PFD_TYPE_RGBA;     // Uses an RGBA pixel format
  pfd.cColorBits = 32;                // 32 bits colors

  if (!(m_hDeviceContext=GetDC(m_hWindow)))
    throw CException("Unable to create rendering context");

  int PixelFormat;
  // Do Windows find a matching pixel format ?
  if (!(PixelFormat=ChoosePixelFormat(m_hDeviceContext,&pfd)))
    throw CException("Unable to create rendering context");
  // Set the new pixel format
  if(!SetPixelFormat(m_hDeviceContext,PixelFormat,&pfd))
    throw CException("Unable to create rendering context");
  // Create the OpenGL rendering context
  if (!(m_hGLContext=wglCreateContext(m_hDeviceContext)))
    throw CException("Unable to create rendering context");
  // Activate the rendering context
  if(!wglMakeCurrent(m_hDeviceContext,m_hGLContext))
    throw CException("Unable to create rendering context");
}

The first part of the function fills a PIXELFORMATDESCRIPTOR with the correct information: the buffer is used to draw to a window, must support OpenGL and uses double buffering (to avoid flickering). We then call ChoosePixelFormat to see if this pixel format is supported. The function returns a pixel format index (or 0 if no matching pixel format was found). Once we have the index of the pixel format, we set the new format by calling SetPixelFormat. We then create the OpenGL rendering context by calling wglCreateContext. Finally, by calling wglMakeCurrent, we specify that all subsequent OpenGL calls made by the thread are drawn on this device context. You can also see that if an error is encountered while creating the context, an exception is thrown and will be handled in our main function.

The InitGL function is rather simple:

void CMainWindow::InitGL()
{
  // Enable 2D texturing
  glEnable(GL_TEXTURE_2D);
  // Choose a smooth shading model
    glShadeModel(GL_SMOOTH);
  // Set the clear color to black
  glClearColor(0.0, 0.0, 0.0, 0.0);

  // Enable the alpha test. This is needed 
  // to be able to have images with transparent 
  // parts.
  glEnable(GL_ALPHA_TEST);
  glAlphaFunc(GL_GREATER, 0.0f);
}

We first enable the 2D texturing. Without this call, we won't be able to apply textures to shapes on the screen. Those textures will be loaded from file and used to display the different game elements. We then choose a smooth shading model. This is not really important in our case, but it simply tells OpenGL if the points of a primitive (a basic shape, like a triangle or a rectangle) have different colors, they will be interpolated. We'll see later what it does on a concrete example.We then specify a clear color. This color is used to clear the color buffer before drawing anything to it. Finally, we enable the alpha testing. This is needed if we want to render some parts of a texture transparent. Suppose for example that you want to draw a ship on the screen and that this ship is loaded from a file. The ship doesn't have a rectangular shape so, you would like to make the texture around the ship transparent so that you don't have a white rectangle in which you have your ship. This is done by using an alpha channel that specifies the opacity of a pixel (this will be covered more in details in the second article). Once the alpha testing has been enabled, we need also to select which function will be used to discard pixels depending on their alpha channel. This is done through the glAlphaFunc: we specify that all pixels with an alpha channel greater (GL_GREATER) than the specified threshold (0) will be discarded (not drawn). Other alpha functions also exist (GL_LESS, GL_EQUAL, ...).

Let's now take a look at the OnSize function. If you remember, this function is called whenever the window is resized (and at least once, at the window creation):

void CMainWindow::OnSize(GLsizei width, GLsizei height)
{
  // Sets the size of the OpenGL viewport
    glViewport(0,0,width,height);

  // Select the projection stack and apply
  // an orthographic projection
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(0.0,width,height,0.0,-1.0,1.0);
  glMatrixMode(GL_MODELVIEW);
}

It receives as parameter the new size of the window. The first thing we do here is call glViewport. This function specifies which section of the window will be used by OpenGL for drawing. You can for example limit the drawing to a portion of the full window. In our case, we will use the full window as the viewport. By default, OpenGL will use the full window size so this call is not necessary (only for educational purposes).

Now we'll glMatrixMode. In order to understand what it does, let me first explain that OpenGL uses three matrix stacks at different stages of the process. These stacks are:

  • GL_MODELVIEW: This matrix stack affects the objects in your scene. In case of our tutorial, these objects will simply be textured rectangles. By manipulating this matrix stack, you will be able to translate, rotate and scale the objects in your scene.
  • GL_PROJECTION: This matrix stack affects how the objects in your scene will be projected on the viewport. By manipulating this stack, you can specify which kind of projection should be applied to your objects.
  • GL_TEXTURE: This matrix stack defines how the textures will be manipulated before being applied to objects. We won't manipulate this stack in this tutorial.

OpenGL will always work with the matrix that is currently on top of each stack, but using a stack might be useful because you can then push the current matrix down the stack to be used later. We will see at the end of this tutorial a more concrete example of that.

After this little explanation, we are back to our code: what glMatrixMode does is that it simply tells OpenGL which matrix stack will be affected by the next operations. In your code, we select the projection stack. We then load the identity matrix in the stack (which simply resets the current matrix to the identity matrix) and then we specify that we would like an orthographic projection of the objects on the viewport. We finally switch back to the default matrix, which is the model view matrix.

You might wonder what is this orthographic projection? Let's take a deeper look at what it does. You can have two different projections in OpenGL: the perspective or the orthographic projection. Instead of going into a detailed explanation, I'll put two pictures showing the two projections.

Orthographic projection

Orthographic projection.

Perspective projection

Perspective projection.

As you can see, a perspective projection is the way to go if you develop a 3D game: it will be similar as what your eyes can see as objects that are far from the camera will look small. An orthographic projection on the other hand won't distort objects: a cube at a distance will look the same size as a cube just in front of the camera (given they are the same size). For a 2D game, I prefer to use an orthographic projection because then I don't have to take the z position into account: I can give whatever value and the object won't be smaller or bigger depending of this value.

The arguments you pass to glOrtho are the coordinates of the viewing volume (left, right, bottom, top, nearVal and farVal). The values you choose here will in fact define the 'units' you will be working with: OpenGL doesn't define any units on its own. For example, I've chosen the window width as the width of my viewing volume. It means that if I move an object 1 unit to the left, it will move 1 pixel. You will also often see values from 0.0 to 1.0 for left/bottom and right/top. In that case, one unit is the width of window in the horizontal direction and is the height of the window in the vertical direction. In 2D games, I prefer to use the first option because if I want to draw two textures next to each other, I know exactly how much I have to move my second texture: it is the width of the first texture (e.g. if my textures are 24 pixels width, my second texture will be moved 24 units to the right). On the other hand, if I want to position something in the middle of my window, I have to take into consideration the width of the window. For the other option, 0.5 units is the middle of the window. That's just a matter of choice but as I am familiar with MFC and GDI, I tend to use the first option to have the same feeling. You might also have noticed another point: I gave a value of height for the bottom and of 0 for the top. It means that my top and bottom are inverted. Here also, it is just a matter of choice: the Y axis in OpenGL goes from the bottom to the top, which is the opposite as what I'm used to do (window coordinates start at the top of the window to the bottom of the window).

Drawing Simple Shapes

Now that everything is set-up correctly, we will finally be able to draw some basic shapes on our window. We are using double buffering to avoid flickering, this means that everything will be written to an off-screen buffer and once the image is composed, the buffers will be swapped, bringing the off-screen buffer to the screen and vice-versa. This avoids having to draw directly on the buffer that is displayed on the screen. Let's look at our CMainWindow::Draw() function where the drawing code should be:

void CMainWindow::Draw()
{
  // Clear the buffer
  glClear(GL_COLOR_BUFFER_BIT);

  SwapBuffers(m_hDeviceContext);
}

The first line of code simply clears the buffer using the clear color that was specified earlier in our InitGL function (black). At the end of the function, we swap the buffers by calling SwapBuffers. Our drawing code will be placed between these calls.

OpenGL allows you to draw some simple shapes, called primitives which can be points, lines and polygons (most of the times, triangles and rectangles). These primitives are described by their vertices, the coordinates of the points themselves, the endpoints of the line segments or the corners of the polygons. For 2D games, we will probably limit ourselves to rectangles: when textured, they allow you to display bitmaps which is almost all we need for a 2D game. For more complex games (like 3D games), complex shapes can be created by assembling triangles together to form a mesh. Let's draw a rectangle and a triangle on the screen: we will put this code between the two function calls in our drawing function.

  glBegin(GL_QUADS);
    glVertex3i(50,200,0);
    glVertex3i(250,200,0);
    glVertex3i(250,350,0);
    glVertex3i(50,350,0);
  glEnd();

  glBegin(GL_TRIANGLES);
    glVertex3i(400,350,0);
    glVertex3i(500,200,0);
    glVertex3i(600,350,0);
  glEnd();

Specifying vertices (calls to glVertex3i) should always be wrapped inside a glBegin/glEnd pair. The argument supplied to glBegin defines the type of shape we are drawing. You can draw multiple shapes within the same glBegin/glEnd pair, you simply have to provide enough vertices: e.g. if you want to draw two rectangles, you have to provide 8 vertices. The arguments you provide to glVertex3i are the coordinates of the vertex, which depend on how the projection was defined (remember what we did in the CMainWindow::OnSize() method). I've chosen to stick to window coordinates for this example. The '3i' at the end of the function specifies the number and type of arguments to the function. Several versions of this function exist: from two to four arguments which can be integers, floats, doubles, signed, unsigned, arrays, ... Simply select the one that is the most suited to your needs.

You can also specify a color for each of the vertices of your shape, so let's try some nice things here:

  glBegin(GL_QUADS);
    glColor3f(1.0,0.0,0.0);   glVertex3i(50,200,0);
    glColor3f(0.0,1.0,0.0);   glVertex3i(250,200,0);
    glColor3f(0.0,0.0,1.0);   glVertex3i(250,350,0);
    glColor3f(1.0,1.0,1.0);   glVertex3i(50,350,0);
  glEnd();

  glBegin(GL_TRIANGLES);
    glColor3f(1.0,0.0,0.0);  glVertex3i(400,350,0);
    glColor3f(0.0,1.0,0.0);  glVertex3i(500,200,0);
    glColor3f(0.0,0.0,1.0);  glVertex3i(600,350,0);
  glEnd();

Specifying the current color is done by calling glColor3f, here also, several versions of the function exist. For the floating point version, the full intensity corresponds to 1.0, and no intensity corresponds to 0.0. If you run the code, you will see that the colors of each vertex blend nicely together (it is the image that is on top of this article). That is because we've chosen the GL_SMOOTH shading model when calling glShadeModel in our CMainWindow::InitGL() function. If you change it into GL_FLAT, you'll see that the shapes have only one color, which is the last supplied one.

Modeling Transformations

I will finish this tutorial by showing you what can be done by manipulating the model view matrix stack. This won't be used in next tutorials (or even in the final game) but it is nice to understand these concepts. That's the reason why I'll be quite brief on this subject.

I already talked a bit about the model view matrix stack and said that you can apply transformations to this matrix which will affect the objects in your scene. I also explained that using a stack instead of a single matrix can be useful when you want to save the current matrix for later use. By calling glPushMatrix, you push the top matrix down the current selected stack (which is the model view stack by default) and create a duplicate of this matrix on the top of the stack. Once you have manipulated the model view matrix to affect certain objects in your scene, you can pop back to the previous pushed matrix by calling glPopMatrix. This is particularly useful when you have to draw elements that have children elements: the position and rotation of the children depends on the position and rotation of the parent (e.g. a finger on a robot hand depends on the position of the hand, which in turn depends on the position of the robot's arm). In that case you apply the transformation for the parent element, push the matrix down the stack, apply the transformations for the first child and draw it, then pop the first matrix to reset to the position and rotation of the parent element. You can then draw the second child by using the same method. Of course, those child elements can themselves have child elements in which case you apply the same technique.

Applying transformations to the objects in your scene is done by loading a specific matrix in the model view matrix stack. You can compound this matrix by hand but I guess that's something you would like to avoid. That's why OpenGL provides three routines that can be used for modeling transformations: glTranslate,glRotate and glScale. One thing you have to take into consideration is that each call to such functions is equivalent to creating the corresponding translation, rotation or scaling matrix and then multiply the current model view matrix with this matrix (and storing the result in the model view matrix). It means that you can 'chain' these calls to produce the transformation you like. You might also know (or remember from your math lessons) that matrix multiplication is not commutative. It means that the order in which you call your functions is important. In fact, the last transformation command called in your program is the first which is applied. You can look at it by imagining that you have to call the transformations in the reverse order in which you would like them to be applied. Suppose that you want to position an object at location (100,100) (we don't take z into account here) and have it rotated 180� around the z axis (but still centered at the same location), then you would need to apply the translation first and then rotate the object. If you do the opposite, the translation would be applied first and then the rotation would be applied, which means your object will be moved at location (100,100) and then rotated 180� around (0,0). Which means it will end up in position (-100,-100).

I don't want to go into too much detail here because matrix manipulation and modeling transformations are worth a full article on their own. I simply wanted to show you that manipulating the model view matrix could prove quite powerful, for example if you want to add some simple special effects (like rotation and scaling).

Conclusion

In this article, I've provided a basic framework that can be reused for writing 2D games. It creates the main window and set-up OpenGL accordingly. We will see in the next article how to texture the shapes with images that are loaded from files and how to efficiently manage those resources.

References

Acknowledgements

I would like to thanks Andrew Vos for the nice projection images.
Thanks to the reviewers: Vunic, Andrew.
Thanks also to Jeremy Falcon, El Corazon and Nemanja Trifunovic for their advice and help.

History

  • 23rd June, 2008:
    • Initial version

  • 23rd August, 2008:
    • Added link to the second article in the Foreword section.
    • The Run method of the CApplication class has been adapted.
    • The OnEvent method of the CMainWindow class has been adapted.
    • Added support for blending in the InitGL method of the CMainWindow class.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Cedric Moonen
Engineer
Belgium Belgium
I am a 29 years old guy and I live with my girlfriend in Hoegaarden, little city from Belgium well known for its white beer Smile | :) .
I studied as an industrial engineer in electronics but I oriented myself more towards software development when I started to work.
Currently I am working in a research centre in mechatronica. I mainly develop in C++ but I also do a bit of Java.
When I have so spare time, I like to read (mainly fantasy) and play electric guitar.

Comments and Discussions

 
GeneralMy vote of 5 PinmemberSachin Gorade29-Jul-12 8:59 
GeneralMy vote of 3 Pinmemberzuizuimei26-Dec-10 19:18 
GeneralRe: My vote of 3 PinmemberCedric Moonen12-Jan-11 4:38 
Generalblack window after ctrl + alt + del PinmemberKrasnapolsky5-Sep-10 13:21 
GeneralRe: black window after ctrl + alt + del PinmemberCedric Moonen5-Sep-10 20:27 
GeneralRe: black window after ctrl + alt + del [modified] PinmemberKrasnapolsky5-Sep-10 22:29 
Generalhi Pinmemberbensalah31-May-10 7:10 
Generalwait for Part 2!!! Pinmemberphinecos7-Aug-08 21:08 
GeneralRe: wait for Part 2!!! [modified] PinmvpCedric Moonen7-Aug-08 21:53 
GeneralRe: wait for Part 2!!! PinmvpCedric Moonen15-Aug-08 21:40 
GeneralExecutable not closing properly - still in memory PinmemberZatrael2-Aug-08 23:59 
GeneralRe: Executable not closing properly - still in memory PinmvpCedric Moonen3-Aug-08 20:43 
GeneralRe: Executable not closing properly - still in memory PinmemberZatrael6-Aug-08 2:08 
GeneralRe: Executable not closing properly - still in memory PinmvpCedric Moonen6-Aug-08 3:14 
GeneralRe: Executable not closing properly - still in memory PinmemberZatrael7-Aug-08 3:33 
GeneralRe: Executable not closing properly - still in memory PinmvpCedric Moonen7-Aug-08 3:38 
GeneralRe: Executable not closing properly - still in memory PinmemberZatrael7-Aug-08 21:15 
GeneralRe: Executable not closing properly - still in memory [modified] PinmemberZatrael9-Aug-08 16:30 
GeneralFIXED: Executable not closing properly - still in memory PinmemberZatrael12-Aug-08 12:23 
GeneralRe: FIXED: Executable not closing properly - still in memory PinmvpCedric Moonen12-Aug-08 21:02 
QuestionHow can I implementation shortest path problem in c++ using: Pinmemberengawsan20-Jul-08 0:11 
AnswerRe: How can I implementation shortest path problem in c++ using: PinmvpCedric Moonen20-Jul-08 3:17 
Generalshortest path problem Pinmemberengawsan20-Jul-08 0:05 
QuestionDev-C++ Pinmemberxskltn8-Jul-08 19:04 
AnswerRe: Dev-C++ [modified] PinmvpCedric Moonen8-Jul-08 20:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.141022.2 | Last Updated 26 Aug 2008
Article Copyright 2008 by Cedric Moonen
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid