Lập trình Windows ***************** Khi viết bài này mình dùng Visual Studio 2022 Community. Chọn ``System`` là ``Windows`` thay vì ``Console``. Lúc này, hàm chính của chương trình không phải là ``main`` nữa mà là ``wWinMain`` có prototype như sau: .. code-block:: cpp INT WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow); Ở đây có hai tham số cần lưu ý là ``hInstance`` và ``nCmdShow``. Trong đó ``hInstance`` chỉ bản thân process, tức "thể hiện" của nó, còn ``nCmdShow`` chỉ việc window khi được tạo sẽ hiện hay không. Trong nhiều trường hợp, để xem trước đó chương trình đã được chạy hay chưa chúng ta dùng ``hPrevInstance``. Ví dụ, chúng ta sẽ không muốn file game bị chạy nhiều lần, nên cần kiểm tra xem trước đó file exe đã được load lên chưa thông qua ``hPrevInstance``. Ở đây chúng ta chưa xem xét tới ``hPrevInstance``. Lập trình Windows tức là tạo cửa sổ. Ở đây, để điều khiển cửa sổ mình thực hiện ba bước: 1. Tạo ``WNDCLASSEX`` - mình phải set trường ``cbSize`` của thành ``sizeof(WNDCLASSEX)``; - mình cần chú ý trường ``lpfnWndProc``. Trường này là con trỏ tới hàm ``WndProc`` (windows process) sẽ xử lý các sự kiện của cửa sổ window. 2. Sử dụng kiểu dữ liệu ``HWND`` để handle tới cửa sổ (HWND - handle window). Trong lập trình windows, mỗi handle sẽ xử lý tín hiệu từ một phần nào đó. 3. Xử lý các message (sự kiện) được truyền vào cửa sổ (nhấn nút tắt, gõ phím, click chuột, ...) trong ``WndProc``. Templete sau định nghĩa hàm ``WndProc`` sẽ kết thúc chương trình khi click vào nút tắt (X). .. code-block:: cpp #include #define APPTITLE L"Sample Window" LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); INT WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; ZeroMemory(&wc, sizeof(WNDCLASSEX)); wc.cbSize = sizeof(WNDCLASSEX); wc.hInstance = hInstance; wc.lpszClassName = APPTITLE; wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.lpfnWndProc = WndProc; wc.style = CS_HREDRAW | CS_VREDRAW; RegisterClassEx(&wc); HWND hwnd = CreateWindowExW( 0, APPTITLE, APPTITLE, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, NULL, NULL, hInstance, NULL); if (hwnd == 0) { return FALSE; } UpdateWindow(hwnd); ShowWindow(hwnd, nCmdShow); MSG msg = { 0 }; while (GetMessage(&msg, NULL, NULL, NULL)) { TranslateMessage(&msg); DispatchMessageW(&msg); } return msg.wParam; } Dưới đây định nghĩa ``WndProc`` với một chức năng là đóng cửa sổ khi nhận được message ``WM_DESTROY`` (nhấn dấu X ở góc trên phải của cửa sổ). .. code-block:: cpp LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, msg, wParam, lParam); break; } return 0; } Mình cần một hàm xuất các thông tin debug. Trong ``Windows.h`` có hỗ trợ hàm ``OutputDebugString`` để xuất chuỗi debug. Tuy nhiên hàm này chỉ nhận vào chuỗi, chứ không có tham số (format string) như ``printf``. Do đó cần code thêm một hàm để nhận các tham số, ví dụ như ``GetLastError`` và ghi lên một mảng kí tự trước rồi mới xuất với ``OutputDebugString``. .. code-block:: cpp // debug.h #pragma once void DebugOut(const wchar_t* fmt, ...); .. code-block:: cpp // debug.cpp #include "debug.h" #include #include #include void DebugOut(const wchar_t* fmt, ...) { wchar_t debug[2048]; va_list args; va_start(args, fmt); vswprintf_s(debug, fmt, args); OutputDebugString(debug); va_end(args); } - Để định nghĩa các tham số cho format string cần các tham số: ``va_list``, ``va_start`` và ``va_end``. - Lúc này tập hợp các tham số cho format string sẽ là ``args``. - Mình dùng hàm ``vswprintf_s`` để chép format string đã có tham số vào mảng ``debug``. - Dùng ``OutputDebugString`` để xuất nó ra. Ví dụ, để xuất lỗi từ hàm ``GetLastError`` thì mình dùng như sau: .. code-block:: cpp DebugOut(L"An error has occurred with code: %l\n", GetLastError()); Tiếp theo mình định nghĩa delta time. Ý tưởng là trong một đơn vị thời gian (thường là một giây) thì mình sẽ render một lượng frame nhất định. Ở đây mình viết lại vòng lặp xử lý message. Trong game có hai hàm cực kì quan trọng là: - ``Update(DWORD dt)``: hàm này chịu trách nhiệm update các thông tin cần thiết để chuẩn bị render, ví dụ như vị trí, vận tốc, xử lý va chạm, chuyển vị trí camera (đối với camera), nhận input từ bàn phím và chuột, ... - ``Render()``: render hình ảnh lên vị trí đã update trước đó, chọn các dạng subset sẽ render, .... Hàm này xử lý các vấn đề đồ họa. Các hàm cần định nghĩa gồm: .. code-block:: cpp void Update(DWORD dt); void Render(); Mình định nghĩa hằng số ``FRAME_PER_SECOND`` và dùng nó để tính toán delta time giữa hai lần ``Update`` cũng như ``Render``. Khi delta time giữa ``start`` và ``now`` đủ lớn (vượt không nhiều ``tickPerFrame``) thì tiến hành ``Update`` và ``Render``. .. code-block:: cpp const int FRAME_PER_SECOND = 60 MSG msg = { 0 }; DWORD start = GetTickCount(); DWORD tickPerFrame = 1000 / FRAME_PER_SECOND; while (msg.message != WM_QUIT) { if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } else { DWORD now = GetTickCount(); DWORD dt = now - start; if (dt >= tickPerFrame) { start = now; Update(dt); Render(); } else Sleep(tickPerFrame - dt); } } Tận dụng khả năng của lập trình hướng đối tượng, chúng ta dùng một base class cho mọi đối tượng trong game là ``GameObject``. .. code-block:: cpp // GameObject.h #pragma once class GameObject { protected: float m_x, m_y, m_z; public: GameObject(float x = 0, float y = 0, float z = 0) : m_x(x), m_y(y), m_z(z) { } virtual void Update(DWORD dt) = 0; virtual void Render() = 0; };