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:
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:
Tạo
WNDCLASSEXmình phải set trường
cbSizecủa thànhsizeof(WNDCLASSEX);mình cần chú ý trường
lpfnWndProc. Trường này là con trỏ tới hàmWndProc(windows process) sẽ xử lý các sự kiện của cửa sổ window.
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 đó.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).
#include <Windows.h>
#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ổ).
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.
// debug.h
#pragma once
void DebugOut(const wchar_t* fmt, ...);
// debug.cpp
#include "debug.h"
#include <Windows.h>
#include <stdio.h>
#include <stdarg.h>
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_startvà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ảngdebug.Dùng
OutputDebugStringđể xuất nó ra.
Ví dụ, để xuất lỗi từ hàm GetLastError thì mình dùng như sau:
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:
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.
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.
// 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;
};