视频地址https://www.youtube.com/watch?v=PH5kH8h82L8&list=PLv8DnRaQOs5-MR-zbP1QUdq5FL0FWqVzg&index=3
接上一篇内容,main.cpp的内容增加了一些代码,显得严谨一些:
#include
#include
int main()
{
try {
OGame game;
game.Run();
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
return 1;
}
return 0;
}
然后是Game类的添加了很多内容:
Game.h文件:
#pragma once
#include
class OGraphicsEngine;
class OWindow;
class OGame
{
public:
OGame();
~OGame();
virtual void onCreate();
virtual void onUpdate();
virtual void onQuit();
void Run();
protected:
bool m_isRunning = true;
std::unique_ptr m_graphicsEngine;
std::unique_ptr m_display;
};
Game.cpp文件:
#include
#include
#include
#include
OGame::OGame()
{
m_graphicsEngine = std::make_unique();
m_display = std::make_unique();
m_display->makeCurrentContext();
}
OGame::~OGame()
{
}
void OGame::onCreate()
{
m_graphicsEngine->clear(OVec4(1,0,0,1));
m_display->present(false);
}
void OGame::onUpdate()
{
}
void OGame::onQuit()
{
}
void OGame::Run()
{
onCreate();
MSG msg;
while (m_isRunning)
{
msg = {};
if (PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE))
{
if (msg.message == WM_QUIT)
{
m_isRunning = false;
continue;
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
onUpdate();
}
onQuit();
}
和上一篇相比,在构造函数里面创建了OGraphicEngine对象,同时执行了OWindow对象m_display的方法makeCurrentContext,这个方法也是OWindow类在本次视频里面后加的。需要注意的是,这两行代码的顺序不能错:
m_graphicsEngine = std::make_unique();
m_display = std::make_unique();
因为OWindow构造期间需要获取的HDC里面的内容,而这个HDC内容要在OGraphicsEngine构造期间设置,所以必定要先构造OGraphicsEngine对象,顺序颠倒了肯定不行。
同时OGame类里面还添加onCreate、onUpdate、onQuit方法,注意一下这三个方法的调用时机就好了。
Owindow.h文件
#pragma once
#include
class OWindow
{
public:
OWindow();
~OWindow();
void makeCurrentContext();
void present(bool vsync);
private:
HWND m_handle = nullptr;
HGLRC m_context = nullptr;
};
OWindow.cpp文件
#include
#include
#include
#include
#include
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_DESTROY:
{
OWindow* window = (OWindow*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
break;
}
case WM_CLOSE:
{
PostQuitMessage(0);
break;
}
default:
return DefWindowProc(hwnd, msg, wParam, lParam);
}
return NULL;
}
OWindow::OWindow()
{
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.lpszClassName = L"OGL3DWindow";
wc.lpfnWndProc = WndProc;
auto classId = RegisterClassEx(&wc);
assert(classId);
RECT rc = { 0,0,1024,768 };
AdjustWindowRect(&rc, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU, false);
m_handle = CreateWindowEx(NULL,
MAKEINTATOM(classId),
L"Parcode | OpenGL 3D Game",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
rc.right - rc.left, rc.bottom - rc.top,
NULL, NULL, NULL, NULL);
assert(m_handle);
SetWindowLongPtr(m_handle, GWLP_USERDATA, (LONG_PTR)this);
ShowWindow(m_handle, SW_SHOW);
UpdateWindow(m_handle);
HDC hDC = GetDC(m_handle);
int pixelFormatAttributes[] = {
WGL_DRAW_TO_WINDOW_ARB,GL_TRUE,
WGL_SUPPORT_OPENGL_ARB,GL_TRUE,
WGL_DOUBLE_BUFFER_ARB,GL_TRUE,
WGL_ACCELERATION_ARB,WGL_FULL_ACCELERATION_ARB,
WGL_PIXEL_TYPE_ARB,WGL_TYPE_RGBA_ARB,
WGL_COLOR_BITS_ARB,24,
WGL_DEPTH_BITS_ARB,24,
WGL_STENCIL_BITS_ARB,8,
0
};
int iPixelFormat = 0;
UINT numFormats = 0;
wglChoosePixelFormatARB(hDC, pixelFormatAttributes, nullptr, 1, &iPixelFormat, &numFormats);
assert(numFormats);
PIXELFORMATDESCRIPTOR pixelFormatDesc = {};
DescribePixelFormat(hDC, iPixelFormat, sizeof(PIXELFORMATDESCRIPTOR), &pixelFormatDesc);
SetPixelFormat(hDC, iPixelFormat, &pixelFormatDesc);
int openAttributes[] = {
WGL_CONTEXT_MAJOR_VERSION_ARB,4,
WGL_CONTEXT_MINOR_VERSION_ARB,6,
WGL_CONTEXT_PROFILE_MASK_ARB,WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
0
};
m_context = wglCreateContextAttribsARB(hDC, 0, openAttributes);
assert(m_context);
}
OWindow::~OWindow()
{
wglDeleteContext(m_context);
DestroyWindow(m_handle);
}
void OWindow::makeCurrentContext()
{
wglMakeCurrent(GetDC(m_handle), m_context);
}
void OWindow::present(bool vsync)
{
wglSwapIntervalEXT(vsync);
wglSwapLayerBuffers(GetDC(m_handle), WGL_SWAP_MAIN_PLANE);
}
这里需要解释一些创建窗口的问题,其实在上一篇文件里就应该解释,但是当时理解的也不到位,这里算是补充一下。
WNDCLASSEX创建一个名称为wc的结构体实例,这个实例相当于我们事先写一份要创建窗口的类型申请表,RegisterClass这个函数就是把wc这个申请表提交(注册)给Window系统,然后当需要创建窗口时,Window系统就会按这个申请表来创建。
RegisterClass的形参是指向WNDCLASSEX结构体的指针,这个指针会加入到system atom table即SAT中,这样系统就可以通过查找这张表来找到用户自定义的窗口类,window预定义的窗口类指针也在SAT中。
SAT实际上实现了一种用于查询的映射,ATOM(翻译过来叫“原子”)实际类型是short,即16整数。ATOM表(原子表)是一个系统定义的用于存放字符串和相应的标识符的表。程序把一个字符串放入ATOM表,获得一个相应的16位整数,这个整数就叫原子,可以用来访问该字符串。一个被放进原子表的字符串叫做原子名称。
只有系统才可直接访问这张表,但在调用某些api函数时,如Registerclass,可以告知系统来存取这张表。当然,还有本地原子表和全局原子表,这些表应用程序是可以直接访问的。
MAKEINTATOM 宏 (位于winbase.h)将指定的原子转换为对应的字符串地址,以便可以将其传递给接受原子或字符串的函数。调用CreateWindowEx就用到了这个宏。
后面补充的代码是从
HDC hDC = GetDC(m_handle);
这行代码开始,一直到
assert(m_context);
这行代码结束,其核心的目的是获取绘制上下文,就是 m_context 。而获取上下文的代码时使用的代码是:
m_context = wglCreateContextAttribsARB(hDC, 0, openAttributes);
总体来说对我是个晕头转向的过程,不过大概就是先要获取形参,第一个是hDC,也是唯一需要说明一下的,这个内容有点儿多,查了老半天也是很糊涂的,先能明白多少算多少。
HDC——Handle of the Device Context,中文意思是“设备上下文句柄”,这个东西会在后面多次用到。HDC是一种包含有关某个设备(如显示器或打印机)的绘制属性信息的 Windows 数据结构。所有绘制调用都通过设备上下文对象进行,这些对象封装了用于绘制线条、形状和文本的 Windows API。
虽然说GetDC函数很容易就获取了这个hDC,但是对其像素格式进行设置。就是下面的代码:
SetPixelFormat(hDC, iPixelFormat, &pixelFormatDesc);
这个过程设计好几个函数,基本没看明白,估计照着写就行。
就说一个wglChoosePixelFormatARB函数吧,它能够找到合适的像素格式。
Windows下要通过程序设置多重采样,必须使用wglChoosePixelFormatARB这个函数。正确使用这个函数的关键,就是需要创建一个临时窗体,通过这个窗体,我们可以获取必须的基础像素格式,然后再使用wglChoosePixelFormatARB这个函数,得到可用的多重采样像素格式,最后,对渲染窗口设置这个像素格式即可。
新增加了OGraphicsEngine类,先贴出代码:
OGraphicsEngine.h文件:
#pragma once
#include
class OGraphicsEngine
{
public:
OGraphicsEngine();
~OGraphicsEngine();
public:
void clear(const OVec4& color);
};
OGraphicsEngine.cpp文件:
#include
#include
#include
#include
#include
OGraphicsEngine::OGraphicsEngine()
{
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.lpszClassName = L"OGL3DDummyWindow";
wc.lpfnWndProc = DefWindowProc;
wc.style = CS_OWNDC;
auto classId = RegisterClassEx(&wc);
assert(classId);
auto dummyWindow = CreateWindowEx(NULL, MAKEINTATOM(classId), L"", WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, NULL, NULL);
assert(dummyWindow);
HDC dummyDC = GetDC(dummyWindow);
PIXELFORMATDESCRIPTOR pixelFormatDesc = {};
pixelFormatDesc.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pixelFormatDesc.nVersion = 1;
pixelFormatDesc.iPixelType = PFD_TYPE_RGBA;
pixelFormatDesc.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
pixelFormatDesc.cColorBits = 32;
pixelFormatDesc.cAlphaBits = 8;
pixelFormatDesc.cDepthBits = 24;
pixelFormatDesc.cStencilBits = 8;
pixelFormatDesc.iLayerType = PFD_MAIN_PLANE;
auto iPixelFormat = ChoosePixelFormat(dummyDC, &pixelFormatDesc);
SetPixelFormat(dummyDC, iPixelFormat, &pixelFormatDesc);
auto dummyContext = wglCreateContext(dummyDC);
assert(dummyContext);
wglMakeCurrent(dummyDC, dummyContext);
if (!gladLoadWGL(dummyDC))throw std::runtime_error("OGraphic Engine Error:gladLoadWGL failed.");
if (!gladLoadGL())throw std::runtime_error("OGraphic Engine Error:gladLoadGL failed.");
wglMakeCurrent(dummyDC, 0);
wglDeleteContext(dummyContext);
ReleaseDC(dummyWindow, dummyDC);
DestroyWindow(dummyWindow);
}
OGraphicsEngine::~OGraphicsEngine()
{
}
void OGraphicsEngine::clear(const OVec4& color)
{
glClearColor(color.x,color.y,color.z,color.w);
glClear(GL_COLOR_BUFFER_BIT);
}
choosePixelFormat从HDC中选择最匹配的内容,返回一个索引(iPixelFormat,开头的i应该就是index)。
SetPixelformat函数将指定设备上下文(HDC)的像素格式设置为索引(iPixelFormat)指定的格式。
这一通操作意思就是你开始定义的PIXELFORMATDESCRIPTOR只是你自己一厢情愿的想法,设备(显示器或者打印机)有可能支持(这当然最好),也有可能不支持,一旦不支持,就给你找个最接近你要求的参数凑合用用。
wglCreateContext(为什么是wgl开头呢?应该是来源于Wingdi.h文件的GL函数,wingdi应该就是Window Graphic Deviec Interface)函数创建一个呈现上下文,该上下文适用于在 hdc 引用的设备上绘图。
其返回值是HGLRC类型(the Handle of GL Rendering Context,GL渲染上下文句柄)
wglMakeCurrent就是指示后续在hdc上的绘制工作都是基于dummyContext的。
绕了一大圈都是为了这两句:
if (!gladLoadWGL(dummyDC))throw std::runtime_error("OGraphic Engine Error:gladLoadWGL failed.");
if (!gladLoadGL())throw std::runtime_error("OGraphic Engine Error:gladLoadGL failed.");
先说一下,通过throw抛出异常会终止线程。
这两句也都是对hDC状态进行设置。
补充两个文件,一个是OVec4.h文件,另一个是OPerequisites.h文件。
OVec4.h文件:
#pragma once
#include
class OVec4
{
public:
OVec4() {}
OVec4(f32 x, f32 y, f32 z, f32 w) :x(x), y(y), z(z), w(w) {}
~OVec4() {}
public:
f32 x = 0, y = 0, z = 0, w = 0;
};
OPerequisites.h文件:
#pragma once
typedef float f32;
说实话,Window编程真的很绕,很晦涩(其实本来想说很**,但我要保持优雅的底线),也许这就是垄断的结果吧。其实以前开发苹果App的时候,感觉苹果对开发者就很不友好(对玩家用户倒是友好得很!),禁不住回想在Unity环境下用C#编程,似乎真的很美好(Long Live Unity!)。不过话说回来,开发游戏引擎的通常比使用引擎做游戏的工资更高,算是对被这种代码折磨的补偿吧 :D