从零开始游戏开发——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 }

posted @ 2022-05-30 19:27  毅安  阅读(201)  评论(2编辑  收藏  举报