標籤:

從零開始手敲次世代遊戲引擎(十)

上一篇文章我們分別使用GDI(Windows)和XCB(Linux)在窗體當中繪製了一個矩形。

然而,這些矩形其實是由CPU繪製的,而不是GPU。因此這種繪製方式是很慢的。

本篇開始我們使用GPU完成圖形的繪製。首先讓我們看一下Windows平台特有的Direct X。

Direct X在早期其實分為幾個模塊:專門繪製2D圖形的DirectDraw(現在改名為Direct2D),專門繪製3D圖形的Direct3D,專門用於多媒體播放的DirectShow,等等。

我們首先來看看使用Direct2D來進行繪製的代碼是什麼樣子的。

因為D2D只是提供一種用GPU繪製圖形的方式,創建窗口等操作還是和以前一樣的,也就是說我們可以重用之前寫的helloengine_win.c的大部分代碼。另外,為了將來可以很方便的在CPU繪製和GPU繪製之間切換比較,我們應該保留helloengine_win.c。

所以,首先將helloengine_win.c複製一份,命名為helloengine_d2d.cpp。(改為.cpp後綴的原因是我們將要使用的d2d的頭文件需要按照C++方式編譯)

然後如下修改這個文件(左側有」+「號的行為新增加的行,」-「的行為刪除的行):

(本文代碼參考MSDN Direct 2D教程編寫)

@@ -3,6 +3,63 @@n #include <windowsx.h>n #include <tchar.h>nn+#include <d2d1.h>n+n+ID2D1Factory *pFactory = nullptr;n+ID2D1HwndRenderTarget *pRenderTarget = nullptr;n+ID2D1SolidColorBrush *pLightSlateGrayBrush = nullptr;n+ID2D1SolidColorBrush *pCornflowerBlueBrush = nullptr;n+n+template<class T>n+inline void SafeRelease(T **ppInterfaceToRelease)n+{n+ if (*ppInterfaceToRelease != nullptr)n+ {n+ (*ppInterfaceToRelease)->Release();n+n+ (*ppInterfaceToRelease) = nullptr;n+ }n+}n+n+HRESULT CreateGraphicsResources(HWND hWnd)n+{n+ HRESULT hr = S_OK;n+ if (pRenderTarget == nullptr)n+ {n+ RECT rc;n+ GetClientRect(hWnd, &rc);n+n+ D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left,n+ rc.bottom - rc.top);n+n+ hr = pFactory->CreateHwndRenderTarget(n+ D2D1::RenderTargetProperties(),n+ D2D1::HwndRenderTargetProperties(hWnd, size),n+ &pRenderTarget);n+n+ if (SUCCEEDED(hr))n+ {n+ hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LightSlateGray), &pLightSlateGrayBrush);n+n+ }n+n+ if (SUCCEEDED(hr))n+ {n+ hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::CornflowerBlue), &pCornflowerBlueBrush);n+n+ }n+ }n+ return hr;n+}n+n+void DiscardGraphicsResources()n+{n+ SafeRelease(&pRenderTarget);n+ SafeRelease(&pLightSlateGrayBrush);n+ SafeRelease(&pCornflowerBlueBrush);n+}n+n+n // the WindowProc function prototypen LRESULT CALLBACK WindowProc(HWND hWnd,n UINT message,n@@ -20,6 +77,9 @@ int WINAPI WinMain(HINSTANCE hInstance,n // this struct holds information for the window classn WNDCLASSEX wc;nn+ // initialize COMn+ if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE))) return -1;n+n // clear out the window class for usen ZeroMemory(&wc, sizeof(WNDCLASSEX));nn@@ -28,7 +88,7 @@ int WINAPI WinMain(HINSTANCE hInstance,n wc.style = CS_HREDRAW | CS_VREDRAW;n wc.lpfnWndProc = WindowProc;n wc.hInstance = hInstance;n- wc.hCursor = LoadCursor(NULL, IDC_ARROW);n+ wc.hCursor = LoadCursor(nullptr, IDC_ARROW);n wc.hbrBackground = (HBRUSH)COLOR_WINDOW;n wc.lpszClassName = _T("WindowClass1");nn@@ -38,12 +98,12 @@ int WINAPI WinMain(HINSTANCE hInstance,n // create the window and use the result as the handlen hWnd = CreateWindowEx(0,n _T("WindowClass1"), // name of the window classn- _T("Hello, Engine!"), // title of the windown+ _T("Hello, Engine![Direct 2D]"), // title of the windown WS_OVERLAPPEDWINDOW, // window stylen- 300, // x-position of the windown- 300, // y-position of the windown- 500, // width of the windown- 400, // height of the windown+ 100, // x-position of the windown+ 100, // y-position of the windown+ 960, // width of the windown+ 540, // height of the windown NULL, // we have no parent window, NULLn NULL, // we arent using menus, NULLn hInstance, // application handlen@@ -58,7 +118,7 @@ int WINAPI WinMain(HINSTANCE hInstance,n MSG msg;nn // wait for the next message in the queue, store the result in msgn- while(GetMessage(&msg, NULL, 0, 0))n+ while(GetMessage(&msg, nullptr, 0, 0))n {n // translate keystroke messages into the right formatn TranslateMessage(&msg);n@@ -67,6 +127,9 @@ int WINAPI WinMain(HINSTANCE hInstance,n DispatchMessage(&msg);n }nn+ // uninitialize COMn+ CoUninitialize();n+n // return this part of the WM_QUIT message to Windowsn return msg.wParam;n }n@@ -74,30 +137,126 @@ int WINAPI WinMain(HINSTANCE hInstance,n // this is the main message handler for the programn LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)n {n+ LRESULT result = 0;n+ bool wasHandled = false;n+n // sort through and find what code to run for the message givenn switch(message)n {n+ case WM_CREATE:n+ if (FAILED(D2D1CreateFactory(n+ D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory)))n+ {n+ result = -1; // Fail CreateWindowEx.n

+ return result;n+ }n+ wasHandled = true;n+ result = 0;n+ break;n+n case WM_PAINT:n {n- PAINTSTRUCT ps;n- HDC hdc = BeginPaint(hWnd, &ps);n- RECT rec = { 20, 20, 60, 80 };n- HBRUSH brush = (HBRUSH) GetStockObject(BLACK_BRUSH);n-n- FillRect(hdc, &rec, brush);n-n- EndPaint(hWnd, &ps);n- } break;n- // this message is read when the window is closedn- case WM_DESTROY:n- {n- // close the application entirelyn- PostQuitMessage(0);n- return 0;n- } break;n+ HRESULT hr = CreateGraphicsResources(hWnd);n+ if (SUCCEEDED(hr))n+ {n+ PAINTSTRUCT ps;n+ BeginPaint(hWnd, &ps);n+n+ // start build GPU draw commandn+ pRenderTarget->BeginDraw();n+n+ // clear the background with white colorn+ pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));n+n+ // retrieve the size of drawing arean+ D2D1_SIZE_F rtSize = pRenderTarget->GetSize();n+n+ // draw a grid background.n+ int width = static_cast<int>(rtSize.width);n+ int height = static_cast<int>(rtSize.height);n+n+ for (int x = 0; x < width; x += 10)n+ {n+ pRenderTarget->DrawLine(n+ D2D1::Point2F(static_cast<FLOAT>(x), 0.0f),n+ D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height),n+ pLightSlateGrayBrush,n+ 0.5fn+ );n+ }n+n+ for (int y = 0; y < height; y += 10)n+ {n+ pRenderTarget->DrawLine(n+ D2D1::Point2F(0.0f, static_cast<FLOAT>(y)),n+ D2D1::Point2F(rtSize.width, static_cast<FLOAT>(y)),n+ pLightSlateGrayBrush,n+ 0.5fn+ );n+ }n+n+ // draw two rectanglesn+ D2D1_RECT_F rectangle1 = D2D1::RectF(n+ rtSize.width/2 - 50.0f,n+ rtSize.height/2 - 50.0f,n+ rtSize.width/2 + 50.0f,n+ rtSize.height/2 + 50.0fn+ );n+n+ D2D1_RECT_F rectangle2 = D2D1::RectF(n+ rtSize.width/2 - 100.0f,n+ rtSize.height/2 - 100.0f,n+ rtSize.width/2 + 100.0f,n+ rtSize.height/2 + 100.0fn+ );n+n+ // draw a filled rectanglen+ pRenderTarget->FillRectangle(&rectangle1, pLightSlateGrayBrush);n+n+ // draw a outline only rectanglen+ pRenderTarget->DrawRectangle(&rectangle2, pCornflowerBlueBrush);n+n+ // end GPU draw command buildingn+ hr = pRenderTarget->EndDraw();n+ if (FAILED(hr) || hr == D2DERR_RECREATE_TARGET)n+ {n+ DiscardGraphicsResources();n+ }n+n+ EndPaint(hWnd, &ps);n+ }n+ }n+ wasHandled = true;n+ break;n+n+ case WM_SIZE:n+ if (pRenderTarget != nullptr)n+ {n+ RECT rc;n+ GetClientRect(hWnd, &rc);n+n+ D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top);n+n+ pRenderTarget->Resize(size);n+ }n+ wasHandled = true;n+ break;n+n+ case WM_DESTROY:n+ DiscardGraphicsResources();n+ if (pFactory) {pFactory->Release(); pFactory=nullptr; }n+ PostQuitMessage(0);n+ result = 0;n+ wasHandled = true;n+ break;n+n+ case WM_DISPLAYCHANGE:n+ InvalidateRect(hWnd, nullptr, false);n+ wasHandled = true;n+ break;n }nn // Handle any messages the switch statement didntn- return DefWindowProc (hWnd, message, wParam, lParam);n+ if (!wasHandled) { result = DefWindowProc (hWnd, message, wParam, lParam); }n+ return result;n }n

簡單說明一下:

首先,包含了d2d1.h。這是一個Direct2D的Wrapper,也就是一個對Direct2D進行了簡單的包裝的頭文件。

然後,定義了如下4個全局變數:

ID2D1Factory *pFactory = nullptr;nID2D1HwndRenderTarget *pRenderTarget = nullptr;nID2D1SolidColorBrush *pLightSlateGrayBrush = nullptr;nID2D1SolidColorBrush *pCornflowerBlueBrush = nullptr;n

第一個是程序設計模式(Design Pattern)當中的所謂建造工廠的一個介面。第二個到第四個與我們應該已經比較眼熟了,一個是渲染對象,就是畫布,後面是兩支畫筆。不過這裡的都是COM的介面。也就是說,這些對象實際上是存在於COM當中的,我們的程序只是擁有一個指向它們的介面。

D2D的庫是以一種被稱為」COM組件「的方式提供的。粗略地來說類似於Java當中的Reflection,不僅僅是一個動態庫,而且這個庫所提供的API也是可以動態查詢的,而不是事先通過頭文件進行申明。

"COM"自身是一個很複雜的概念,是微軟當時為了讓Office能夠在辦公軟體當中勝出,所開發出來的一種技術。在Office當中使用的基於"COM"的技術主要有"OLE"和」DDE「。前者是嵌入式對象,就是我們可以把一個視頻、或者一個別的什麼原本Office當中不支持的文件,放到Office文檔當中。然後這個東西就會顯示為一個圖標,或者是一個靜態的圖片(snapshot),雙擊這個圖標或者這個靜態的圖片就會啟動能夠支持這個格式的軟體對其進行播放或者編輯。這種播放或者編輯有兩種形式:一種是in place,就是直接在Office文檔當中進行,一直是standalone,就是在Office之外進行。in place的時候,其實是在後台啟動能夠支持這種格式的一個程序,但是隱藏其窗口。然後把Office當中的一小塊客戶區域(就是之前我們用過的Rect所定義的一個矩形區域)傳遞給這個後台程序,讓其負責處理這塊區域的繪製和用戶輸入。也就是說,在Office程序的WM_PAINT事件的處理當中,將Office窗口的整個客戶區域分割為由自己繪製的部分和由OLE繪製的部分,由OLE繪製的部分通過COM技術傳遞給後台應用進行繪製。比如我們嵌入的OLE對象是一個視頻,那麼當你在Office文檔內播放這個視頻的時候,實際上後台會啟動Windows Media Player,只不過它的界面是隱藏的。Windows Media Player對視頻進行解碼播放,只不過和平常不一樣的是,最後畫面不是畫在Windows Media Player自己的窗體上,而是畫在Office文檔當中的一塊矩形區域當中。

最常見的應用就是在PPT裡面放一個視頻,或者放一個Excel表格,Word文檔什麼的。這個其實就是用的"OLE"技術。

而」DDE「大部分和"OLE"類似,所不同的是這個對象是單獨存放在磁碟上,而不是嵌入到Office文檔當中進行保存的。我們將一個Excel拖入到PPT的時候,Office會問我們是作為嵌入式對象,還是鏈接。嵌入式對象就是"OLE",而鏈接就是"DDE"。「DDE」 的特點是你可以隨時在外部編輯那個文件,而改變會自動反映到使用「DDE」鏈接進的那個文檔當中。也就是說,如果你用「鏈接」的方式把一個Excel放入PPT,那麼後面如果你修改了那個Excel,PPT裡面的那個Excel對象的數據也會跟著變。

除了這種應用,Windows服務,DirectX 3D當中所用的filter,.NET技術,IE Browser所用的插件,Office所用的插件,等等,都是基於"COM"技術。」COM「技術還有後繼的"COM+"技術以及在多個電腦上分散式處理的」DCOM「(在Windows Server當中我們可以由一台伺服器部署管理其它伺服器,就是靠著「DCOM」) 技術。

--(題外話開始) --

筆者剛剛參加工作的時候,所在的項目組是負責一台名為」Morpheus「的台式機的開發(正式商品名」VAIO Type X")

vaio.sony.co.jp/Product

這台機器可以支持7個電視頻道同時24小時x7天無縫錄像。當時一套的售價(含顯示器)是大約100萬日元,按那個時候的匯率大概是7-8萬RMB。(東京只有7個免費電視頻道)

我當時進入公司的時候這個項目的開發已經接近一半。也就是硬體基本設計定型了而軟體才剛剛開始。這個時候公司突然決定要去參加一個VAIO的市場活動,需要展示這台巨無霸機器。然而如果只是展示硬體頗為無趣,所以想要展示錄像功能,雖然錄像功能並沒有做好。

所以,需要快速地開發一種替代模式來進行展示。當時的別的型號的VAIO也是可以錄像的,只不過每台只能錄製一個頻道。所以為了實現7個頻道的同時錄製,就需要7台電腦同時工作一個禮拜。但是如果只是將7台電腦打開放在那裡,錄製的節目是連續的,並不會按照電子節目單(EPG)進行分割。而如果找7個人去手動按開始結束,在國內可能可行,在日本這個開銷就大了。因為要3班倒,需要21個人。

筆者採用DCOM解決了這個問題。就是寫個程序去按照點開始和結束,然後導入第8台機器,下載分析EPG並通過DCOM去控制那7台電腦上面的程序。

--(題外話結束) --

不過這個「COM」 技術雖然很NB,但並不是微軟原創的技術。這種技術實際上是一種名為"CORBA(Welcome To CORBA Web Site!)"的技術的微軟版本而已。

+n+template<class T>n+inline void SafeRelease(T **ppInterfaceToRelease)n+{n+ if (*ppInterfaceToRelease != nullptr)n+ {n+ (*ppInterfaceToRelease)->Release();n+n+ (*ppInterfaceToRelease) = nullptr;n+ }n+}n+n

這是我們寫的第一個使用了C++模板機制的函數。模板也稱為泛型,具體就不展開了,有興趣的可以去看C++的書。

+HRESULT CreateGraphicsResources(HWND hWnd)n+{n+ HRESULT hr = S_OK;n+ if (pRenderTarget == nullptr)n+ {n+ RECT rc;n+ GetClientRect(hWnd, &rc);n+n+ D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left,n+ rc.bottom - rc.top);n+n+ hr = pFactory->CreateHwndRenderTarget(n+ D2D1::RenderTargetProperties(),n+ D2D1::HwndRenderTargetProperties(hWnd, size),n+ &pRenderTarget);n+n+ if (SUCCEEDED(hr))n+ {n+ hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LightSlateGray), &pLightSlateGrayBrush);n+n+ }n+n+ if (SUCCEEDED(hr))n+ {n+ hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::CornflowerBlue), &pCornflowerBlueBrush);n+n+ }n+ }n+ return hr;n+}n

這個函數的作用是創建繪圖所需要的畫布、畫筆。使用GPU繪圖的時候,這個部分其實是有很多工作需要做的。然而這些D2D都為我們封裝好了,所以我們可以簡簡單單地,以一種非常接近於GDI的方式去調用。(但同時是使我們少了很多控制力,這個就是之前所說的新的DX12所要解決的問題)

+void DiscardGraphicsResources()n+{n+ SafeRelease(&pRenderTarget);n+ SafeRelease(&pLightSlateGrayBrush);n+ SafeRelease(&pCornflowerBlueBrush);n+}n+n

這個是用來釋放畫布、畫筆所對應的GPU資源的。使用了我們上面定義的泛型函數。在我們這個例子裡面,需要釋放這些資源的主要有兩種情況:

  1. 窗口的大小發生了改變
  2. 窗口被銷毀(程序結束)

+ // initialize COMn+ if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE))) return -1;n+n

初始化COM。所有使用COM的程序,都需要在程序的開頭做這麼個調用,因為COM和普通動態庫不同,它其實是一個比較獨立的東西,有自己的一套機制。(其實動態庫在使用之前也是需要載入的。只不過很多只在Windows上面寫程序的程序員,因為微軟深度傻瓜封裝的關係,不太知道)

第一個參數是固定為nullptr(就是0。因為C++是強類型語言,0是一個整數,而空指針應該是指針類型,所以C++ 11裡面定義了這個nullptr,用來取代之前的0,來滿足類型匹配的要求)。

第二個參數由兩部分組成。

COINIT_APARTMENTTHREADED

這個是告訴COM以一種所謂STA的方式運行。很粗略的來說可以認為COM組件是以一種與我們程序同步的方式運行。如果想知道細節請參考COM相關資料,比如下面這篇官方文檔:

COINIT enumeration

簡單來說,就如我上面解釋「OLE」的時候介紹的,其實這個時候在我們的窗體之外,D2D COM會創建一個隱藏的窗體,然後監視著我們的窗體的消息隊列。同時,所有的繪製都重定向到我們的窗體,而不是它自己的窗體。

COINIT_DISABLE_OLE1DDE

這個是關閉一些已經過時的COM功能,減少不必要的開銷。

既然我們在應用程序初始化的時候初始化了COM組件,那麼我們就需要在應用程序結束的地方結束它:

+ // uninitialize COMn+ CoUninitialize();n+n

然後我們需要在窗口創建的過程當中創建Factory工廠。因為只有有了工廠我們才能創建畫布、畫筆。(在前面GDI或者XCB的代碼當中,因為這些對象都是我們程序內部創建的,所以我們並不需要工廠。但是,現在我們是使用D2D,對象是在遊離在我們程序本體之外的一個COM組件裡面創建的。對於這些對象我們所知甚少,所以就要通過工廠創建。打個比喻,「外包」)。WM_CREATE是在我們調用CreateWindowEx()這個系統API的時候,系統回調我們的消息處理函數所發送給我們的消息。

+ case WM_CREATE:n+ if (FAILED(D2D1CreateFactory(n+ D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory)))n+ {n+ result = -1; // Fail CreateWindowEx.n

+ return result;n+ }n+ wasHandled = true;n+ result = 0;n+ break;n+n

然後改動最大的部分,WM_PAINT消息處理部分:

+ HRESULT hr = CreateGraphicsResources(hWnd);n+ if (SUCCEEDED(hr))n+ {n+ PAINTSTRUCT ps;n+ BeginPaint(hWnd, &ps);n+n+ // start build GPU draw commandn+ pRenderTarget->BeginDraw();n+n+ // clear the background with white colorn+ pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));n+n+ // retrieve the size of drawing arean+ D2D1_SIZE_F rtSize = pRenderTarget->GetSize();n+n+ // draw a grid background.n+ int width = static_cast<int>(rtSize.width);n+ int height = static_cast<int>(rtSize.height);n+n+ for (int x = 0; x < width; x += 10)n+ {n+ pRenderTarget->DrawLine(n+ D2D1::Point2F(static_cast<FLOAT>(x), 0.0f),n+ D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height),n+ pLightSlateGrayBrush,n+ 0.5fn+ );n+ }n+n+ for (int y = 0; y < height; y += 10)n+ {n+ pRenderTarget->DrawLine(n+ D2D1::Point2F(0.0f, static_cast<FLOAT>(y)),n+ D2D1::Point2F(rtSize.width, static_cast<FLOAT>(y)),n+ pLightSlateGrayBrush,n+ 0.5fn+ );n+ }n+n+ // draw two rectanglesn+ D2D1_RECT_F rectangle1 = D2D1::RectF(n+ rtSize.width/2 - 50.0f,n+ rtSize.height/2 - 50.0f,n+ rtSize.width/2 + 50.0f,n+ rtSize.height/2 + 50.0fn+ );n+n+ D2D1_RECT_F rectangle2 = D2D1::RectF(n+ rtSize.width/2 - 100.0f,n+ rtSize.height/2 - 100.0f,n+ rtSize.width/2 + 100.0f,n+ rtSize.height/2 + 100.0fn+ );n+n+ // draw a filled rectanglen+ pRenderTarget->FillRectangle(&rectangle1, pLightSlateGrayBrush);n+n+ // draw a outline only rectanglen+ pRenderTarget->DrawRectangle(&rectangle2, pCornflowerBlueBrush);n+n+ // end GPU draw command buildingn+ hr = pRenderTarget->EndDraw();n+ if (FAILED(hr) || hr == D2DERR_RECREATE_TARGET)n+ {n+ DiscardGraphicsResources();n+ }n+n+ EndPaint(hWnd, &ps);n+ }n+ }n+ wasHandled = true;n+ break;n

這部分咋看改動很多,其實和GDI繪製是十分類似的。所不同的是所有繪製指令我們都通過pRenderTarget這個介面調用。pRenderTarget是D2D COM組件所提供給我們的一個介面,那麼也就是說實際的GPU繪圖指令是在D2D COM組件當中完成的,而我們只是將命令和參數傳(外包)給D2D。事實上,我們這些調用只是生成一些D2D消息放在我們窗體的消息隊列當中,然後D2D看到這些消息就會進行處理,命令GPU進行繪製。

+ case WM_SIZE:n+ if (pRenderTarget != nullptr)n+ {n+ RECT rc;n+ GetClientRect(hWnd, &rc);n+n+ D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top);n+n+ pRenderTarget->Resize(size);n+ }n+ wasHandled = true;n+ break;n

這個是處理窗口尺寸變化的。當窗口尺寸變化的時候,我們需要通知GPU調整畫布的大小(實際上會導致拋棄所有之前的繪圖資源,重新建立一套新的畫布、畫筆)

+ case WM_DISPLAYCHANGE:n+ InvalidateRect(hWnd, nullptr, false);n+ wasHandled = true;n+ break;n

InvalidateRect是通知系統窗口的客戶區域(Client Rect)需要進行重新繪製。而WM_DISPLAYCHANGE是指顯示器解析度發生變化。

+ if (!wasHandled) { result = DefWindowProc (hWnd, message, wParam, lParam); }n+ return result;n

這部分是撿漏。Windows的消息隊列當中的消息很多,包括上面所說的COM相關消息。我們的代碼里之進行了一部分消息的定製化處理。對於我們沒有處理的消息,在這裡調用系統預設的處理方式進行處理。這個步驟很重要,否則窗口都不會創建成功。

好了。我們已經完成了整個代碼的更改。接下來是編譯它。因為我們用到了COM,所以我們追加需要鏈接old32.lib這個庫;我們用到了D2D1,所以我們需要追加鏈接d2d1.lib這個庫。我們現在沒有用到GDI,所以不需要gid32.lib這個庫了。

D:wenliSourceReposGameEngineFromScratchPlatformWindows>clang -l user32.lib -l ole32.lib -l d2d1.lib -o helloengine_d2d.exe helloengine_d2d.cppn

執行helloengine_d2d.exe,我們看到了下面這個結果:

好了,我們完成了人生中第一次與GPU的親密接觸。

保存我們的程序。

Windows下面的調試方法

現在我們的程序已經變得比較複雜了。因此很可能會有各種bug。解決bug的方式是調試。

雖然我們使用了Clang工具進行編譯,但是我們依舊可以使用Visual Studio進行調試。方法如下:

首先,我們需要將編譯分為兩個步驟,先使用clang進行obj的生成(也就是編譯)。

D:wenliSourceReposGameEngineFromScratchPlatformWindows>clang-cl -c -Z7 -o helloengine_d2d.obj helloengine_d2d.cppn

注意我們這裡實際上使用的是clang-cl這個工具。這個工具是clang的一個兼容性版本,可以識別Visual Studio提供的cl.exe編譯器的選項

llvm.org/devmtg/2014-04

然後我們使用Visual Studo的鏈接器進行鏈接

D:wenliSourceReposGameEngineFromScratchPlatformWindows>link -debug user32.lib ole32.lib d2d1.lib helloengine_d2d.objn

這樣我們就可以看到目錄當中生成了.pdb文件。這個文件就是Visual Studio的調試符號庫。

我們可以使用下面的命令啟動Visual Studio的調試窗口:

D:wenliSourceReposGameEngineFromScratchPlatformWindows>devenv /debug helloengine_d2d.exen

接下來就和常規的Visual Studio調試沒有任何區別了。

(-- EOF --)

參考引用:

  1. Direct2D (Windows)

本作品採用知識共享署名 4.0 國際許可協議進行許可。

推薦閱讀:

Old Man's Journey
《塞爾達傳說:荒野之息》美術風格剖析
在你個人的遊戲相關工作中,有過哪些特別成功或失敗的經歷?
TapTap致開發者的第一封信
《InsideUE4》UObject(二)類型系統概述

TAG:游戏开发 |