1. 程式人生 > 其它 >從零開始遊戲開發——1.1 第一個三角形

從零開始遊戲開發——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, int
nCmdShow) 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 }