|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
ForewordThis 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:
Contents
IntroductionThis 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 SettingsWe 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.
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.
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 LoopThe 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...
}
}
If we look at the code of this first tutorial, we can see that the message loop is wrapped into a class called // 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 Let's look at the 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 ( 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. 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 WindowCreating the WindowAs we saw before, we only had to create an instance of the 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 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 Now, back to the 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 The Window ProcedureWe just created a new window by calling 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
Depending on the type of message, some additional information will be stored in the As this function is Let's look at the 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 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 HandlingError 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 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 Setting up OpenGLIf you remember, in our 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 The 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 Let's now take a look at the 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 Now we'll
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 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.
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 Drawing Simple ShapesNow 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 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 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 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 Modeling TransformationsI 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 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: 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). ConclusionIn 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
AcknowledgementsI would like to thanks Andrew Vos for the nice projection images. History
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||