TetroGL:适用于Win32平台的C ++ OpenGL游戏教程-第1部分(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/tetrogl-an-opengl-game-tutorial-in-c-for-win32-pla-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 35 分钟阅读 - 17315 个词 阅读量 0TetroGL:适用于Win32平台的C ++ OpenGL游戏教程-第1部分(译文)
原文地址:https://www.codeproject.com/Articles/27219/TetroGL-An-OpenGL-Game-Tutorial-in-C-for-Win32-Pla
原文作者:Cedric Moonen
译文由本站 robot-v1.0 翻译
前言
Learn to create a Win32 message loop and game window and how to set-up OpenGL properly for 2D games
学习创建Win32消息循环和游戏窗口,以及如何为2D游戏正确设置OpenGL
前言(Foreword)
本系列文章重点介绍使用C ++和OpenGL for Windows平台进行2D游戏开发.目标是在系列结束之前提供一款类似于经典积木益智游戏的游戏.我们将不仅关注OpenGL,还将讨论使用完整的面向对象方法在游戏编程中常用的设计.为了充分利用本系列,您应该已经熟悉C ++语言.如果您有任何问题,评论或建议,可以在文章底部使用留言板.(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:)
- 第1部分:涵盖win32消息循环,窗口创建和OpenGL的设置.您还将学习如何绘制一些简单的形状.(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.)
- 第2部分(Part 2) :涵盖资源处理和显示简单动画的内容.(: Covers resources handling and displaying simple animations.)
- 第三部分(Part 3) :将所有内容组合在一起,并讨论游戏逻辑.(: groups everything together and talk about the game logic.)
内容(Contents)
介绍(Introduction)
本文的这一部分着重于在Windows环境中设置OpenGL窗口.我们将学习如何创建一个消息循环来接收通知,以及如何创建用于绘图的主窗口.然后,我们将看到如何为2D游戏正确配置OpenGL.最后,当一切准备就绪时,我们将学习如何在新创建的OpenGL窗口中显示一些基本形状.(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)
我们将首先创建一个新项目并配置不同的选项.该教程项目是使用Visual Studio 2005创建的,但可以轻松地应用于其他编译器.首先创建一个类型为" Win32 Console Application"的新项目,并为其指定一个适当的名称,然后单击"确定".在创建向导中,选择类型" Windows应用程序"(不是控制台),然后选中"空项目"选项(我们实际上不需要为我们生成的代码).(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(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(opengl32.lib).这告诉链接器在链接项目时必须使用OpenGL库.(. This tells the linker that it has to use the OpenGL library when linking the project.) 接下来,我们将禁用UNICODE,因为我们不需要它,这会使事情变得更加复杂.进入" C/C ++"->“预处理器”,然后单击"预处理器定义".右侧将出现一个按钮,单击该按钮,然后在弹出的对话框中取消选中"从父项或项目默认值继承".这将禁用从项目默认值继承的UNICODE.(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.) 现在已经正确配置了项目设置,我们准备看一些代码.首先让我们检查一下Win32应用程序如何接收和处理事件(键盘,鼠标等).(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)
系统(Windows)为每个应用程序创建一个消息队列,并在该特定应用程序的窗口上发生事件时将消息推送到该队列中.然后,您的应用程序应检索并处理这些消息,以便对它们做出反应.这就是所谓的消息循环,它是所有Win32应用程序的核心.(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
该消息应从队列中删除.消息将存储在第一个参数中,如果检索到消息,该函数将返回非零值.该函数的第二个参数使您可以指定必须为其获取消息的窗口句柄.如果提供NULL,则将检索应用程序所有窗口的消息.第三个和第四个参数使您可以为应该检索的消息指定范围.如果两个都提供0,则将检索所有消息.(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
函数非常简单:它只是检查命令行中是否存在参数" -fullscreen".在这种情况下,标志(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;
}
}
}
函数的第一行只是创建主窗口.我们将在下一章中看到它的确切作用.现在,仅想象一下这会创建并显示具有特定宽度和高度的主窗口,并且是否以全屏显示.您可能会看到,循环本身与我们之前看到的有所不同.原因很简单:通常,对于2D游戏,您无需尽快刷新屏幕.以恒定速率刷新它,足以显示动画并进行处理.在我们的例子中,我们定义了一个常量((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.)
我们可以做些更简单的事情:在我们看到的第一个消息循环示例中,我们可以通过检查以查看自上次更新以来是否经过了30毫秒,来替换"//Do正在处理的东西…":(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();
}
}
除了忙于等待之外,其他方法都可以正常工作:如果没有收到消息,我们将连续循环并消耗所有可用的CPU时间.这不是真的很好,因为CPU用于执行任何操作.(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
第二个参数).该功能将等待,而不会消耗CPU周期,直到超时时间到期(在3中指定(for the second argument). This function will wait without consuming CPU cycles until either the timeout period expires (specified in the 3)rd(rd)参数)或收到消息时.您可以为4中要接收的邮件指定过滤器(argument) or when a message has been received. You can specify a filter for messages to be received in the 4)日(th)参数,但我们对所有消息都感兴趣.当函数超时时,它返回(parameter, but we are interested in all messages. When the function times out, it returns) WM_TIMEOUT
,用于代码中,以检测何时该刷新屏幕并更新游戏逻辑.如果函数没有超时,则意味着一个或多个消息正在队列中等待,因此我们使用PeekMessage提取所有消息(当队列中没有消息时,该函数将返回FALSE).然后由他决定是否应处理该申请.在函数结束时,我们根据下一个截止日期重新计算睡眠时间.如果此睡眠时间大于帧时间,则表示当前时间大于下一个截止时间(负溢出).通常在移动窗口或调整窗口大小时会发生这种情况:在此期间,不再处理该应用程序.在这种情况下,我们只需根据当前时间重新计算新的期限和睡眠时间.(, 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
函数将正确地初始化OpenGL,但我们将在下一章中看到.(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
:消息ID(: 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
确实返回了非NULL的值.(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
是两个从参数中提取前2个字节和后2个字节的宏.收到此类消息后,我们将调整大小处理委托给我们(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)
错误管理对于所有程序来说都是重要的一点,对于游戏也是如此:您不希望由于缺少资源而导致游戏崩溃.我处理游戏错误的首选方式是使用异常.这比从函数返回错误代码(然后将它们路由到我希望处理错误的地方)要方便得多.主要原因是我可以将错误处理委托给一个地方:在我的main函数中,将捕获所有异常.首先让我们看一下我们的异常类,它是非常基本的:(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.)
设置OpenGL(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
将初始化渲染上下文,以便可以在窗口上绘制OpenGL基元:(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
具有正确的信息:缓冲区用于绘制到窗口,必须支持OpenGL并使用双缓冲(以避免闪烁).然后我们打电话(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
.然后,我们通过调用来创建OpenGL渲染上下文(. We then create the OpenGL rendering context by calling) wglCreateContext
.最后,通过致电(. Finally, by calling) wglMakeCurrent
,我们指定在该设备上下文上绘制线程进行的所有后续OpenGL调用.您还可以看到,如果在创建上下文时遇到错误,则会引发异常,并将在我们的main函数中对其进行处理.(, 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);
}
我们首先启用2D纹理.没有此调用,我们将无法在屏幕上的形状上应用纹理.这些纹理将从文件中加载,并用于显示不同的游戏元素.然后,我们选择一个平滑的阴影模型.在我们的案例中,这并不是很重要,但是它只是告诉OpenGL如果基本体(基本形状,如三角形或矩形)的点具有不同的颜色,将对其进行插值.我们将在后面的具体示例中看到它的作用,然后指定清晰的颜色.此颜色用于在绘制任何内容之前清除颜色缓冲区.最后,我们启用Alpha测试.如果我们要使纹理的某些部分透明,则需要这样做.例如,假设您要在屏幕上绘制一艘船,并且该船是从文件中加载的.船的形状不是矩形,因此您希望使船周围的纹理透明,这样您的船中就不会有白色矩形.这可以通过使用Alpha通道来完成,该通道指定像素的不透明度(第二篇文章将对此进行详细介绍).启用Alpha测试后,我们还需要根据其Alpha通道选择使用哪个功能来丢弃像素.这是通过(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
:我们指定所有具有Alpha通道的像素都更大((: we specify that all pixels with an alpha channel greater () GL_GREATER
),则指定的阈值(0)将被丢弃(未绘制).还存在其他alpha函数(() 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
.此函数指定OpenGL将使用窗口的哪个部分进行绘制.例如,您可以将工程图限制为整个窗口的一部分.在本例中,我们将整个窗口用作(. 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
.默认情况下,OpenGL将使用整个窗口的大小,因此该调用不是必需的(仅出于教育目的).(. By default, OpenGL will use the full window size so this call is not necessary (only for educational purposes).)
现在,我们将(Now we’ll) glMatrixMode
.为了了解它的作用,让我首先解释一下OpenGL在过程的不同阶段使用三个矩阵堆栈.这些堆栈是:(. 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将始终使用当前位于每个堆栈顶部的矩阵,但是使用堆栈可能会很有用,因为您可以将当前矩阵向下推入堆栈以供以后使用.在本教程的最后,我们将看到一个更具体的示例.(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
这样做只是告诉OpenGL下一个操作将影响哪个矩阵堆栈.在您的代码中,我们选择投影堆栈.然后,我们将单位矩阵加载到堆栈中(这会将当前矩阵简单地重置为单位矩阵),然后指定我们希望对物体进行正交投影.(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.) 您可能想知道这个正投影是什么?让我们更深入地了解它的作用.在OpenGL中可以有两种不同的投影:透视投影或正投影.我将不进行详细解释,而将两张图片显示这两个投影.(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.)如您所见,透视投影是开发3D游戏的必经之路:它与您的眼睛所看到的相似,因为远离相机的物体看起来很小.另一方面,正交投影不会使对象变形:相距一定距离的多维数据集将与仅在相机前面的多维数据集具有相同的大小(假设它们具有相同的大小).对于2D游戏,我更喜欢使用正交投影,因为这样我就不必考虑z位置:我可以给出任何值,并且该值不会使对象变小或变大.(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
).实际上,您在此处选择的值将定义要使用的"单位":OpenGL不会自行定义任何单位.例如,我选择了窗口宽度作为查看体积的宽度.这意味着如果我将一个对象向左移动1个单位,它将移动1个像素.您还将经常看到从0.0到1.0的左/下和右/上的值.在这种情况下,一个单位是窗口在水平方向上的宽度,是窗口在垂直方向上的高度.在2D游戏中,我更喜欢使用第一个选项,因为如果我要彼此相邻绘制两个纹理,则我确切地知道我必须移动第二个纹理多少:这是第一个纹理的宽度(例如,如果我的纹理宽度为24像素,我的第二个纹理将向右移动24个单位).另一方面,如果我想在窗口中间放置一些东西,则必须考虑到窗口的宽度.对于其他选项,窗口的中间为0.5个单位.这只是选择的问题,但是正如我对MFC和GDI熟悉的那样,我倾向于使用第一种选择来产生相同的感觉.您可能还注意到了另一点:我给了(). 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
为顶部.这意味着我的顶部和底部是倒置的.在这里,这也只是一个选择问题:OpenGL中的Y轴从底部到顶部,这与我惯用的相反(窗口坐标从窗口的顶部开始到窗口的底部)窗户).(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允许您绘制一些简单的形状,称为(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),点本身的坐标,线段的端点或多边形的角.对于2D游戏,我们可能会将自己限制为矩形:纹理化时,它们允许您显示位图,这几乎是2D游戏所需的全部.对于更复杂的游戏(如3D游戏),可以通过将三角形组装在一起以形成网格来创建复杂的形状.让我们在屏幕上绘制一个矩形和一个三角形:我们会将这段代码放在绘图函数中的两个函数调用之间.(, 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
对,您只需要提供足够的顶点即可:如果要绘制两个矩形,则必须提供8个顶点.您提供给的参数(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
函数末尾的’指定函数参数的数量和类型.此函数有多种版本:从2到4个参数,可以是整数,浮点数,双精度数,有符号,无符号,数组等….只需选择最适合您需要的参数即可.(’ 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
,这里也存在该功能的多个版本.对于浮点版本,完整强度对应于1.0,没有强度对应于0.0.如果运行代码,您将看到每个顶点的颜色很好地融合在一起(这是本文顶部的图像).那是因为我们选择了(, 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.)
通过将特定的矩阵加载到模型视图矩阵堆栈中,可以将变换应用于场景中的对象.您可以手工合成该矩阵,但是我想这是您要避免的事情.这就是OpenGL提供三个可用于建模转换的例程的原因:(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
.您必须考虑的一件事是,对此类函数的每次调用等效于创建相应的平移,旋转或缩放矩阵,然后将当前模型视图矩阵与此矩阵相乘(并将结果存储在模型视图矩阵中).这意味着您可以"链接"这些调用以产生所需的转换.您可能还知道(或从数学课程中记住)矩阵乘法不是可交换的.这意味着调用函数的顺序很重要.实际上,程序中调用的最后一个转换命令是第一个应用的命令.您可以通过想象自己必须以想要应用转换的相反顺序调用转换来查看它.假设您想将一个对象放置在位置(100,100)(此处不考虑z),并使其绕z轴旋转180½½(但仍在同一位置居中),那么您需要首先应用平移,然后旋转对象.如果执行相反的操作,则将首先应用平移,然后应用旋转,这意味着您的对象将在位置(100,100)移动,然后绕(0,0)旋转180½½.这意味着它将最终到达位置(-100,-100).(. 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)
在本文中,我提供了一个基本框架,可以将其重用于编写2D游戏.它创建主窗口并相应地设置OpenGL.在下一篇文章中,我们将看到如何使用从文件加载的图像对形状进行纹理处理,以及如何有效地管理这些资源.(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)
- OpenGL编程指南(红皮书)(The OpenGL programming guide (The Redbook)) :如果您想更详细地了解OpenGL,可以作为参考(: Great reference if you want to go more into detail about OpenGL)
- Nehe教程(Nehe tutorials) :OpenGL教程,虽然有些过时,但仍然有用(: OpenGL tutorials, a bit out-dated but still useful)
- MSDN(MSDN) :有关Win32应用程序的消息循环的MSDN文档(: MSDN doc about the message loop for Win32 applications)
- 窗口程序作为类成员(Window procedure as class members) :讨论使用类成员函数处理Windows消息的方法的文章.(: article discussing ways of using a class member function to process Windows messages.)
致谢(Acknowledgements)
我要感谢Andrew Vos提供的出色的投影图像.(I would like to thanks Andrew Vos for the nice projection images.) 感谢审稿人:Vunic,Andrew.(Thanks to the reviewers: Vunic, Andrew.) 也感谢Jeremy Falcon,El Corazon和Nemanja Trifunovic的建议和帮助.(Thanks also to Jeremy Falcon, El Corazon and Nemanja Trifunovic for their advice and help.)
历史(History)
-
23(23)rd(rd)2008年6月:(June, 2008:)
- 初始版本(Initial version)
-
23(23)rd(rd)2008年8月:(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.)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C++ Windows Visual-Studio OpenGL Win32 Design 新闻 翻译