从零开始游戏开发——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 }
本文来自博客园,作者:毅安,转载请注明原文链接:https://www.cnblogs.com/primarycode/p/16327970.html,文章内容同步更新微信公众号:“游戏开发实战”或“GamePrimaryCode”