從零開始遊戲開發——1.1 第一個三角形
1.11 環境搭建
本系列主要在Windows平臺下進行開發,後續核心程式碼與可以移植到其它平臺上。首先我們利用Windows API 顯示最基本的視窗而不借助於任何視窗庫。下面是顯示一個視窗的基本程式碼。
1 #include <Windows.h> 2 3 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); 4 5 int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, intnCmdShow) 6 { 7 // Register the window class. 8 const wchar_t CLASS_NAME[] = L"Window"; 9 10 WNDCLASS wc = { }; 11 12 wc.lpfnWndProc = WindowProc; 13 wc.hInstance = hInstance; 14 wc.lpszClassName = CLASS_NAME; 15 16 RegisterClass(&wc); 17 18 // Create the window.19 20 HWND hwnd = CreateWindowEx( 21 0, // Optional window styles. 22 CLASS_NAME, // Window class 23 L"第一個視窗顯示", // Window text 24 WS_OVERLAPPEDWINDOW, // Window style 25 26 // Size and position 27 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,28 29 NULL, // Parent window 30 NULL, // Menu 31 hInstance, // Instance handle 32 NULL // Additional application data 33 ); 34 35 if (hwnd == NULL) 36 { 37 return 0; 38 } 39 40 ShowWindow(hwnd, nCmdShow); 41 42 // Run the message loop. 43 MSG msg = { }; 44 while (GetMessage(&msg, NULL, 0, 0)) 45 { 46 TranslateMessage(&msg); 47 DispatchMessage(&msg); 48 } 49 50 return 0; 51 } 52 53 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 54 { 55 switch (uMsg) 56 { 57 case WM_DESTROY: 58 PostQuitMessage(0); 59 return 0; 60 } 61 return DefWindowProc(hwnd, uMsg, wParam, lParam); 62 }
然後在遊戲開發中,我們通常會對上面程式碼做些修改,為了輸出除錯,我們使用main()函式做為程式入口, 同時在訊息迴圈部分,替換GetMessage為PeekMessage以增加更多遊戲主迴圈的控制。修改後的main函式程式碼如下:
1 void Update(int delta) 2 { 3 } 4 5 void Render() 6 { 7 8 } 9 10 int main(int argc, char *argv[]) 11 { 12 // Register the window class. 13 const wchar_t CLASS_NAME[] = L"Window"; 14 15 HINSTANCE hInstance = GetModuleHandle(NULL); 16 WNDCLASSEX wcex; 17 wcex.cbSize = sizeof(WNDCLASSEX); 18 wcex.style = CS_HREDRAW | CS_VREDRAW; 19 wcex.lpfnWndProc = WindowProc; 20 wcex.cbClsExtra = 0; 21 wcex.cbWndExtra = 0; 22 wcex.hInstance = hInstance; 23 wcex.hIcon = LoadCursor(NULL, IDC_ICON); 24 wcex.hCursor = LoadCursor(NULL, IDI_APPLICATION); 25 wcex.hbrBackground = NULL; 26 wcex.lpszMenuName = 0; 27 wcex.lpszClassName = CLASS_NAME; 28 wcex.hIconSm = 0; 29 30 RegisterClassEx(&wcex); 31 32 DWORD style = WS_SYSMENU | WS_BORDER | WS_CAPTION | WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 33 34 35 RECT clientSize; 36 clientSize.top = 0; 37 clientSize.left = 0; 38 clientSize.right = WIDTH; 39 clientSize.bottom = HEIGHT; 40 41 AdjustWindowRect(&clientSize, style, FALSE); 42 43 int realWidth = clientSize.right - clientSize.left; 44 int realHeight = clientSize.bottom - clientSize.top; 45 46 int windowLeft = (GetSystemMetrics(SM_CXSCREEN) - realWidth) / 2; 47 int windowTop = (GetSystemMetrics(SM_CYSCREEN) - realHeight) / 2; 48 49 // Create the window. 50 hWnd = CreateWindowEx( 51 0, // Optional window styles. 52 CLASS_NAME, // Window class 53 L"第一個視窗顯示", // Window text 54 style, // Window style 55 56 // Size and position 57 windowLeft, windowTop, realWidth, realHeight, 58 59 NULL, // Parent window 60 NULL, // Menu 61 hInstance, // Instance handle 62 NULL // Additional application data 63 ); 64 65 if (hWnd == NULL) 66 { 67 return 0; 68 } 69 ShowWindow(hWnd, SW_SHOWNORMAL); 70 UpdateWindow(hWnd); 71 MoveWindow(hWnd, windowLeft, windowTop, realWidth, realHeight, TRUE); 72 73 ShowCursor(TRUE); 74 75 76 // Run the message loop. 77 bool isRunning = true; 78 MSG msg = { }; 79 while (isRunning) 80 { 81 if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) 82 { 83 TranslateMessage(&msg); 84 DispatchMessage(&msg); 85 86 if (msg.message == WM_QUIT) 87 isRunning = false; 88 } 89 90 unsigned long long curTick = GetTickCount64(); 91 92 long long sleepTime = nextGameTick - curTick; 93 if (sleepTime <= 0) 94 { 95 nextGameTick = curTick + SKIP_TICKS; 96 Update(SKIP_TICKS - (int)sleepTime); 97 Render(); 98 } 99 else 100 { 101 Sleep(sleepTime); 102 } 103 } 104 105 return 0; 106 }
這個遊戲主迴圈還有一些問題在後面的章節會有更完善的實現,但在這裡已經足夠了。Update()函式主要用於遊戲邏輯的更新,Render()函式則做為繪製圖形顯示。
1.12 第一個三角形
基於第一節的遊戲主迴圈,這裡主要填充Update()和Render()函式。這裡還需要用到一個Windows資料結構BITMAPINFO和API StretchDIBits,宣告BITMAPINFO和buffer變數,並增加一個將位元組資料提交給硬體的輔助函式DrawBuffer:
1 BITMAPINFO bmpInfo; 2 unsigned int* buffer; 3 4 ... 5 6 void DrawBuffer(unsigned char * buffer) 7 { 8 HDC hdc = GetDC(hWnd); 9 10 auto err = StretchDIBits(hdc, 0, 0, WIDTH, HEIGHT, 0, HEIGHT, WIDTH, -HEIGHT, buffer, &bmpInfo, DIB_RGB_COLORS, SRCCOPY); 11 12 ReleaseDC(hWnd, hdc); 13 }
現在,我們要做的就是向這個buffer裡填充三角形的RGB資料了,通常我們在Update裡設定三角形資料,Render函式進行繪製操作:
1 void Update(int delta) 2 { 3 static bool isDataInited= false; 4 if (!isDataInited) 5 { 6 triangle.position[0] = {400, 100}; 7 triangle.position[1] = {100, 500}; 8 triangle.position[2] = {700, 500}; 9 isDataInited= true; 10 } 11 } 12 13 void Render() 14 { 15 for (int i = 0; i < WIDTH; ++i) 16 { 17 for (int j = 0; j < HEIGHT; ++j) 18 { 19 Vector2 triEdge0 = { triangle.position[1].x - triangle.position[0].x, triangle.position[1].y - triangle.position[0].y }; 20 Vector2 triEdge1 = { triangle.position[2].x - triangle.position[1].x, triangle.position[2].y - triangle.position[1].y }; 21 Vector2 triEdge2 = { triangle.position[0].x - triangle.position[2].x, triangle.position[0].y - triangle.position[2].y }; 22 Vector2 pTo0 = { i - triangle.position[0].x, j - triangle.position[0].y }; 23 Vector2 pTo1 = { i - triangle.position[1].x, j - triangle.position[1].y }; 24 Vector2 pTo2 = { i - triangle.position[2].x, j - triangle.position[2].y }; 25 26 if (pTo0.x * triEdge0.y - triEdge0.x * pTo0.y > 0 27 && pTo1.x * triEdge1.y - triEdge1.x * pTo1.y > 0 28 && pTo2.x * triEdge2.y - triEdge2.x * pTo2.y > 0) 29 buffer[j * WIDTH + i] = 0xffff0000; 30 else 31 buffer[j * WIDTH + i] = 0x00000000; 32 } 33 } 34 35 DrawBuffer((unsigned char *)buffer); 36 }
通過上面的程式碼,我們就可以看到一個三角形被畫出來了,
這裡Render()做的事情很簡單,判斷畫素點在三角形內,則將buffer中的值設為0xffff0000及紅色,否則將顏色值設定為黑色,這其實是最簡單的光柵化過程,在渲染器章節我們將更詳細的進行介紹。
1.13 讓三角形動起來
我們已經擁顯示一個三角形的能力了,現在我們要做的是讓三角形動起來,我們利用三角函式實現的三角形的旋轉。
如上圖,單位向量a可以表示為基向量的和即x + y,當x和y逆時針轉旋轉到x'和y'位置時,向量a相當於順時針旋轉了θ度,即此時
a = x‘ + y',
而根據三角函式,x'和y'的向量值分別為
x' = (cosθ * x + sinθ * y),
y' = (-sinθ * x + cosθ * y),
通過簡單的數學計算後
a = (cosθ * x - sinθ * y, sinθ * x + cosθ * y),
由此來計算三角形的三個頂點的旋轉,注意window視窗的y軸向下,最終Update程式碼如下:
1 void Update(int delta) 2 { 3 Vector2 initValue[3] = { {400, 100}, {100, 500}, {700, 500} }; 4 static float rot = 0.f; 5 rot += 0.05f; 6 if (rot > 3.14 * 2) 7 rot = 0; 8 float c = cos(rot); 9 float s = sin(rot); 10 for (int i = 0; i < 3; ++i) 11 { 12 initValue[i].x -= WIDTH * 0.5f; 13 initValue[i].y -= HEIGHT * 0.5f; 14 triangle.position[i].x = initValue[i].x * c - initValue[i].y * s; 15 triangle.position[i].y = initValue[i].x * s + initValue[i].y * c; 16 triangle.position[i].x += WIDTH * 0.5f; 17 triangle.position[i].y += HEIGHT * 0.5f; 18 initValue[i].x += WIDTH * 0.5f; 19 initValue[i].y += HEIGHT * 0.5f; 20 } 21 }
Update程式碼中的旋轉即是後面使用矩陣旋轉的基礎,會在數學庫部分進行詳細解釋。
到此,第一個會動的三角形到此全部完成,我們能畫三角形,我們就能畫任何內容,當然效率也是關鍵。
完整程式碼如下:
1 #include <Windows.h> 2 #include <stdio.h> 3 #include <math.h> 4 5 const int FRAMES_PER_SECOND = 60; 6 const int SKIP_TICKS = 1000 / FRAMES_PER_SECOND; 7 const int WIDTH = 800; 8 const int HEIGHT = 600; 9 10 unsigned long long nextGameTick = GetTickCount(); 11 12 HWND hWnd; 13 BITMAPINFO bmpInfo; 14 unsigned int* buffer; 15 16 typedef struct 17 { 18 int x; 19 int y; 20 }Vector2; 21 22 typedef struct 23 { 24 Vector2 position[3]; 25 }Triangle; 26 27 Triangle triangle; 28 29 void Update(int delta); 30 void Render(); 31 void DrawBuffer(unsigned char *buffer); 32 33 34 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 35 { 36 switch (uMsg) 37 { 38 case WM_DESTROY: 39 PostQuitMessage(0); 40 return 0; 41 42 default: 43 break; 44 } 45 return DefWindowProc(hwnd, uMsg, wParam, lParam); 46 } 47 48 int main(int argc, char *argv[]) 49 { 50 // Register the window class. 51 const wchar_t CLASS_NAME[] = L"Window"; 52 53 HINSTANCE hInstance = GetModuleHandle(NULL); 54 WNDCLASSEX wcex; 55 wcex.cbSize = sizeof(WNDCLASSEX); 56 wcex.style = CS_HREDRAW | CS_VREDRAW; 57 wcex.lpfnWndProc = WindowProc; 58 wcex.cbClsExtra = 0; 59 wcex.cbWndExtra = 0; 60 wcex.hInstance = hInstance; 61 wcex.hIcon = LoadCursor(NULL, IDC_ICON); 62 wcex.hCursor = LoadCursor(NULL, IDI_APPLICATION); 63 wcex.hbrBackground = NULL; 64 wcex.lpszMenuName = 0; 65 wcex.lpszClassName = CLASS_NAME; 66 wcex.hIconSm = 0; 67 68 RegisterClassEx(&wcex); 69 70 DWORD style = WS_SYSMENU | WS_BORDER | WS_CAPTION | WS_CLIPCHILDREN | WS_CLIPSIBLINGS; 71 72 73 RECT clientSize; 74 clientSize.top = 0; 75 clientSize.left = 0; 76 clientSize.right = WIDTH; 77 clientSize.bottom = HEIGHT; 78 79 AdjustWindowRect(&clientSize, style, FALSE); 80 81 int realWidth = clientSize.right - clientSize.left; 82 int realHeight = clientSize.bottom - clientSize.top; 83 84 int windowLeft = (GetSystemMetrics(SM_CXSCREEN) - realWidth) / 2; 85 int windowTop = (GetSystemMetrics(SM_CYSCREEN) - realHeight) / 2; 86 87 // Create the window. 88 hWnd = CreateWindowEx( 89 0, // Optional window styles. 90 CLASS_NAME, // Window class 91 L"第一個視窗顯示", // Window text 92 style, // Window style 93 94 // Size and position 95 windowLeft, windowTop, realWidth, realHeight, 96 97 NULL, // Parent window 98 NULL, // Menu 99 hInstance, // Instance handle 100 NULL // Additional application data 101 ); 102 103 if (hWnd == NULL) 104 { 105 return 0; 106 } 107 ShowWindow(hWnd, SW_SHOWNORMAL); 108 UpdateWindow(hWnd); 109 MoveWindow(hWnd, windowLeft, windowTop, realWidth, realHeight, TRUE); 110 111 ShowCursor(TRUE); 112 113 114 //初始化繪製需要的變數 115 memset(&bmpInfo, 0, sizeof(BITMAPINFO)); 116 bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); 117 bmpInfo.bmiHeader.biWidth = WIDTH;//寬度 118 bmpInfo.bmiHeader.biHeight = HEIGHT;//高度 119 bmpInfo.bmiHeader.biPlanes = 1; 120 bmpInfo.bmiHeader.biBitCount = 32; 121 bmpInfo.bmiHeader.biCompression = BI_RGB; 122 123 buffer = (unsigned int*)malloc(WIDTH * HEIGHT* sizeof(unsigned int)); 124 125 // Run the message loop. 126 bool isRunning = true; 127 MSG msg = { }; 128 while (isRunning) 129 { 130 if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) 131 { 132 TranslateMessage(&msg); 133 DispatchMessage(&msg); 134 135 if (msg.message == WM_QUIT) 136 isRunning = false; 137 } 138 139 unsigned long long curTick = GetTickCount64(); 140 141 long long sleepTime = nextGameTick - curTick; 142 if (sleepTime <= 0) 143 { 144 nextGameTick = curTick + SKIP_TICKS; 145 Update(SKIP_TICKS - (int)sleepTime); 146 Render(); 147 } 148 else 149 { 150 Sleep(sleepTime); 151 } 152 } 153 154 return 0; 155 } 156 157 void Update(int delta) 158 { 159 Vector2 initValue[3] = { {400, 100}, {100, 500}, {700, 500} }; 160 static float rot = 0.f; 161 rot += 0.05f; 162 if (rot > 3.14 * 2) 163 rot = 0; 164 float c = cos(rot); 165 float s = sin(rot); 166 for (int i = 0; i < 3; ++i) 167 { 168 initValue[i].x -= WIDTH * 0.5f; 169 initValue[i].y -= HEIGHT * 0.5f; 170 triangle.position[i].x = initValue[i].x * c - initValue[i].y * s; 171 triangle.position[i].y = initValue[i].x * s + initValue[i].y * c; 172 triangle.position[i].x += WIDTH * 0.5f; 173 triangle.position[i].y += HEIGHT * 0.5f; 174 initValue[i].x += WIDTH * 0.5f; 175 initValue[i].y += HEIGHT * 0.5f; 176 } 177 } 178 179 void Render() 180 { 181 for (int i = 0; i < WIDTH; ++i) 182 { 183 for (int j = 0; j < HEIGHT; ++j) 184 { 185 Vector2 triEdge0 = { triangle.position[1].x - triangle.position[0].x, triangle.position[1].y - triangle.position[0].y }; 186 Vector2 triEdge1 = { triangle.position[2].x - triangle.position[1].x, triangle.position[2].y - triangle.position[1].y }; 187 Vector2 triEdge2 = { triangle.position[0].x - triangle.position[2].x, triangle.position[0].y - triangle.position[2].y }; 188 Vector2 pTo0 = { i - triangle.position[0].x, j - triangle.position[0].y }; 189 Vector2 pTo1 = { i - triangle.position[1].x, j - triangle.position[1].y }; 190 Vector2 pTo2 = { i - triangle.position[2].x, j - triangle.position[2].y }; 191 192 if (pTo0.x * triEdge0.y - triEdge0.x * pTo0.y > 0 193 && pTo1.x * triEdge1.y - triEdge1.x * pTo1.y > 0 194 && pTo2.x * triEdge2.y - triEdge2.x * pTo2.y > 0) 195 buffer[j * WIDTH + i] = 0xffff0000; 196 else 197 buffer[j * WIDTH + i] = 0x00000000; 198 } 199 } 200 201 DrawBuffer((unsigned char *)buffer); 202 } 203 204 void DrawBuffer(unsigned char * buffer) 205 { 206 HDC hdc = GetDC(hWnd); 207 208 auto err = StretchDIBits(hdc, 0, 0, WIDTH, HEIGHT, 0, HEIGHT, WIDTH, -HEIGHT, buffer, &bmpInfo, DIB_RGB_COLORS, SRCCOPY); 209 210 ReleaseDC(hWnd, hdc); 211 }