第4章 基石——Windows编程核心概念与游戏开发实践

Windows操作系统作为全球PC游戏的主导平台,其底层编程机制是每位游戏开发者必须掌握的基石。本章将深入探索Windows编程的核心体系,从宏观架构到微观实现,结合经典游戏开发实例,为你揭开Windows游戏编程的神秘面纱。我们将从API与SDK的概念辨析开始,逐步深入到程序入口、窗口系统、消息机制等核心模块,并通过完整的代码实例演示如何构建一个可交互的游戏窗口。本章内容旨在构建坚实的理论基础,并立即将其应用于实践,使用符合现代规范的Allman风格代码和驼峰命名法,确保所有示例在Visual Studio 2022和VSCode中均可顺畅运行。

4.1 Windows编程体系与游戏开发的宏观视角

在深入代码细节之前,理解Windows编程的宏观体系及其与游戏开发的关联至关重要。Windows操作系统为应用程序,特别是游戏,提供了一个多层次、服务丰富的运行环境。这个环境不仅仅是一个“可以运行程序”的空间,更是一套包含了图形渲染、输入管理、音频输出、内存管理、设备抽象等服务的完整生态系统。

从游戏开发的理论角度看,Windows体系结构遵循的是“客户端-服务器”模型的核心变体。其中,系统内核与各种子系统(如Win32子系统)作为“服务器”,为运行在用户模式的应用程序(“客户端”)提供服务。对于游戏而言,最关键的服务器是图形服务器(通过DirectX或OpenGL驱动程序实现)和输入服务器。当你在《英雄联盟》中点击鼠标施放技能时,你的点击动作被输入服务器捕获,通过消息队列传递给你的游戏程序,游戏逻辑处理后,通过图形服务器调用GPU在屏幕上渲染出技能特效。整个流程高效且有序,这正是Windows编程体系设计的精妙之处。

商业游戏实例充分展示了利用这套体系的方式。以经典游戏《暗黑破坏神2》为例,在其发布年代,它深度依赖Win32 GDI进行2D图形渲染,并直接调用WinMM库播放MIDI音乐。其游戏循环紧密集成了PeekMessage消息泵,确保在流畅渲染动画的同时仍能即时响应玩家的键盘和鼠标输入。而现代游戏如《控制》(Control)则代表了另一种范式:它基于Windows平台,但主要通过高级图形API(如DirectX 12)与硬件对话,并利用XAudio2等现代音频API。然而,其窗口创建、消息处理、输入管理的基础,依然根植于本章所讲述的Win32核心原理。即便是在使用Unity或Unreal Engine等成熟引擎时,引擎底层也封装了这些Windows原生调用,为开发者提供了跨平台的便利,但理解其本质对于调试、性能优化和实现特定平台功能仍不可或缺。

4.2 基石概念辨析:API与SDK

在Windows编程领域,API(应用程序编程接口)和SDK(软件开发工具包)是两个最常被提及,也最易混淆的基础概念。清晰地区分它们,是有效学习和使用开发工具的前提。

4.2.1 应用程序编程接口(API)详解

API,即应用程序编程接口,是一组预定义的函数、数据结构、常量和协议的集合。它本质上是一个约定或契约,规定了软件组件之间如何相互通信。在Windows游戏编程语境下,API就是游戏程序与Windows操作系统进行对话的语言和规则。

我们可以将Windows API想象成一家大型游乐园(操作系统)为游客(应用程序)提供的服务目录和呼叫按钮。例如,你想创建一个窗口,就按下“创建窗口”按钮(调用CreateWindowEx函数);你想播放一段声音,就按下“播放声音”按钮(调用PlaySound或更现代的XAudio2接口)。游戏开发者无需知道游乐园后台的电力系统、机械结构如何运作(即操作系统内核和驱动的具体实现),只需按照规定的格式“按下按钮”,就能获得相应的服务。

Windows API主要分为几个层次:

  1. Win32 API:最核心、最底层的桌面应用程序接口,用于窗口管理、消息处理、文件操作等。它是所有Windows桌面程序的基石。
  2. DirectX API:微软为高性能多媒体应用,尤其是游戏和3D图形,设计的一系列API。包括Direct3D(图形)、DirectSound/DirectMusic/XAudio2(音频)、DirectInput(输入,已过时)等。这是游戏开发中最常打交道的API集之一。
  3. COM(组件对象模型):一种二进制接口标准,许多现代Windows API(如DirectX、WMI)都构建在COM之上。理解COM的基本概念(如接口、引用计数)对于深入使用这些API很有帮助。

理论意义在于,API提供了抽象。游戏开发者无需编写直接操作显卡寄存器或声卡DMA通道的汇编代码,只需调用ID3D12GraphicsCommandList::DrawInstancedIXAudio2SourceVoice::Start这样的高级函数。这种抽象极大地提高了开发效率和代码的可移植性(至少在Windows生态内)。

4.2.2 软件开发工具包(SDK)解析

如果说API是“语言规则”,那么SDK就是帮助你学习和使用这门语言的“全套学习资料和工具”。SDK,即软件开发工具包,是一个包含API文档、头文件、库文件、编译器、调试器、示例代码和各种实用工具的集合包。

对于Windows游戏开发,最重要的SDK包括:

  • Windows SDK:包含所有Win32 API、核心COM接口以及最新Windows系统功能的头文件和库文件。它还包含工具如MessageBox函数的原型声明就在其头文件中,链接时需要的User32.lib文件也在其库目录中。
  • DirectX SDK(现已集成到Windows SDK中):专门为DirectX开发提供的工具,如图形调试器、纹理转换工具、音频处理工具和大量的三维模型示例。

当你安装Visual Studio 2022并勾选“使用C++的桌面开发”时,安装程序会自动为你部署相应版本的Windows SDK。这意味着你立即获得了编写Windows程序所需的所有“原材料”:windows.h等头文件告诉你函数的模样,kernel32.lib等库文件帮助链接器将你的调用与系统内部的实现代码连接起来。

从实践流程看:你使用SDK中的头文件(.h)来编写代码,调用其中声明的API函数;编译后,链接器会使用SDK中的导入库(.lib)来解析这些函数调用;最终运行程序时,Windows系统中的动态链接库(.dll,如user32.dll)会提供函数的具体实现。SDK是这个流程得以启动和完成的保障。

4.3 程序的起点:WinMain函数及其伙伴

每一个C++控制台程序都以main函数为入口,而每一个标准的Windows桌面程序(包括游戏)则以WinMain函数为起点。这是程序被操作系统加载后,执行的第一段属于开发者的代码。

4.3.1 WinMain函数深度剖析

WinMain函数的签名具有特定的格式,它接收来自系统的四个参数:

int WINAPI WinMain(
    HINSTANCE hInstance,      // 当前程序实例的句柄
    HINSTANCE hPrevInstance,  // 在32位及以上Windows中总是NULL,仅为兼容性保留
    LPSTR     lpCmdLine,      // 命令行参数字符串
    int       nCmdShow        // 窗口初始显示方式(如最大化、最小化)
)

让我们逐一拆解:

  • HINSTANCE hInstance:这是操作系统赋予你的程序实例的一个唯一标识符(句柄)。它在程序生命周期内基本不变,在需要向系统标识“我是谁”时使用,例如在创建窗口、加载资源时经常需要传递这个句柄。你可以将其理解为你的程序在系统舞台上的“后台通行证”。
  • LPSTR lpCmdLine:允许玩家或启动器向你的游戏传递参数。例如,一些游戏支持“Game.exe -windowed”来启动窗口化模式,或“Game.exe +map level01”来直接加载特定关卡。游戏初始化时应解析此字符串。
  • int nCmdShow:系统建议的窗口显示方式。通常我们会在创建并显示窗口时,将此值传递给ShowWindow函数。但游戏全屏模式通常会覆盖此设置。

WinMain函数的核心职责是完成程序的初始化,并启动主消息循环。其返回的int值最终会作为进程的退出代码返回给系统。

4.3.2 快速交互工具:MessageBox函数

在游戏开发中,尤其是在调试初期,我们经常需要一个简单的方式来与用户通信或输出调试信息,而不必立即创建复杂的窗口。MessageBox函数就是这个理想的工具。

int MessageBox(
    HWND    hWnd,          // 父窗口句柄,可为NULL
    LPCSTR  lpText,        // 消息框正文
    LPCSTR  lpCaption,     // 消息框标题
    UINT    uType          // 对话框样式(按钮、图标)
);

一个简单的使用示例:

// 在WinMain开始时,弹出提示
int result = MessageBox(
    NULL,
    "欢迎进入游戏初始化阶段!是否继续?",
    "游戏启动确认",
    MB_YESNO | MB_ICONQUESTION
);

if (result == IDNO)
{
    // 玩家选择“否”,优雅退出
    return 0;
}

在商业游戏中,MessageBox可能用于显示非致命的错误信息(如“无法连接到服务器,是否重试?”),或在玩家尝试退出时弹出确认对话框。它是一种阻塞式调用,会暂停当前线程直到用户点击按钮,因此不适用于游戏主循环内的常规信息显示。

4.3.3 初试音效:PlaySound函数

为了让我们的第一个程序不那么沉默,我们引入PlaySound函数。这是一个相对简单、易于上手的音频播放API,适合播放短小的音效(如按钮点击、提示音)。

BOOL PlaySound(
    LPCSTR pszSound,  // 声音文件路径或系统声音标识
    HMODULE hmod,     // 包含资源的可执行文件句柄,通常为NULL
    DWORD   fdwSound  // 播放标志
);

示例:在程序启动时播放一个启动音。

// 播放位于项目目录下的wav文件
PlaySound(TEXT("startup.wav"), NULL, SND_FILENAME | SND_ASYNC);

参数SND_ASYNC表示异步播放,函数调用会立即返回,不会阻塞程序继续执行。这对于游戏体验至关重要,因为你不希望一个音效卡住整个游戏线程。

4.3.4 实践:Firstblood!第一个Windows程序

现在,让我们将上述知识整合,创建一个名为“Firstblood”的完整小程序。它将在启动时播放音效、弹出欢迎对话框,然后展示一个简单的消息。

#include <windows.h>
#include <string>

// 使用宽字符集兼容Unicode,这是现代Windows编程的推荐做法
int WINAPI wWinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR lpCmdLine,
    _In_ int nCmdShow)
{
    // 1. 解析命令行(此处简单示例)
    std::wstring cmdLine = lpCmdLine;
    bool isSilentMode = (cmdLine.find(L"-silent") != std::wstring::npos);

    // 2. 播放启动音效(如果不在静默模式)
    if (!isSilentMode)
    {
        PlaySound(TEXT("SystemStart"), NULL, SND_ALIAS_ID | SND_ASYNC);
    }

    // 3. 弹出欢迎消息框
    int userChoice = MessageBox(
        NULL,
        L"欢迎来到Windows游戏编程世界!\n这是你的Firstblood。\n点击‘确定’继续,或‘取消’退出。",
        L"游戏编程启程",
        MB_OKCANCEL | MB_ICONINFORMATION
    );

    // 4. 根据用户选择处理
    if (userChoice == IDOK)
    {
        // 这里可以开始正式的窗口创建和游戏初始化
        MessageBox(NULL, L"游戏逻辑即将开始...", L"准备就绪", MB_OK);
    }
    else
    {
        MessageBox(NULL, L"期待下次与你并肩作战!", L"退出", MB_OK);
    }

    // 5. 程序结束
    return 0;
}

这个程序虽然简单,但完整地演示了wWinMain(Unicode版本的WinMain)、命令行解析、音频播放和对话框交互,构成了一个Windows程序最基本的交互骨架。注意,我们使用了wWinMain和宽字符L"...",这是处理多语言文本的现代最佳实践。

4.4 程序的容颜:窗口(Window)

窗口是Windows图形用户界面的基本单元,对于游戏而言,它即是呈现游戏画面的画布,也是接收玩家输入的主要界面。一个窗口不仅仅是一个矩形区域,它还是一个拥有样式、菜单、边框、标题栏等属性的复杂对象,并且是系统进行消息投递的目标。

从游戏理论看,窗口对象封装了“呈现表面”和“输入焦点”两大核心功能。无论是《我的世界》的方块世界,还是《星际争霸2》的战场,最终都是绘制在一个或多个窗口客户区之内。同时,鼠标点击、键盘按键这些输入事件,也都是由操作系统以消息的形式发送到对应的窗口过程。游戏引擎的一项重要工作就是高效地管理这个“画布”并处理其上的所有事件。

4.5 资源的标识符:句柄(HANDLE)

句柄是Windows编程中无处不在的核心概念。它是一个抽象的值,通常是一个整数或指针,用于唯一标识一个内核对象或系统资源,如窗口、文件、设备上下文、画笔、位图等。

你可以把句柄想象成酒店前台的房卡。你并不需要知道房间具体的内部结构、电线布局(资源在系统内核中的实际地址),你只需要持有这张房卡(句柄),就可以通过酒店服务系统(操作系统API)来开关灯、调节空调(操作资源)。系统内部通过这张“房卡”找到对应的“房间”。

在游戏中:

  • HWND:标识一个窗口。
  • HDC:标识一个设备上下文,用于绘图。
  • HINSTANCE:标识一个程序实例。
  • HMENU:标识一个菜单。
  • HANDLE:更通用的句柄类型,可用于文件、线程、事件等。

使用句柄而非直接指针的好处在于封装安全性。系统可以动态管理资源(如移动内存中的对象),而无需通知应用程序,因为应用程序始终通过固定的句柄来访问。应用程序也无法通过句柄直接篡改内核对象的数据结构,从而提高了系统的稳定性。

4.6 程序的通信系统:消息与消息队列

Windows是一个事件驱动的操作系统。这意味着程序的执行流在很大程度上由外部发生的事件(如用户点击、定时器到期、窗口需要重绘)来推动。这些事件在系统中被抽象为“消息”。

4.6.1 消息的载体:MSG结构体

每个消息都被包装在一个MSG结构体中,它包含了事件的完整信息:

typedef struct tagMSG {
    HWND   hwnd;     // 接收此消息的窗口句柄
    UINT   message;  // 消息标识符(如WM_LBUTTONDOWN, WM_PAINT)
    WPARAM wParam;   // 附加信息,其含义依赖于message
    LPARAM lParam;   // 附加信息,其含义依赖于message
    DWORD  time;     // 消息被投递的时间
    POINT  pt;       // 消息产生时,光标在屏幕上的坐标
} MSG;

例如,当玩家在游戏窗口客户区按下鼠标左键时,系统会生成一个MSG对象:hwnd是你的游戏窗口句柄,messageWM_LBUTTONDOWNlParam的低字和高字分别包含了鼠标点击位置的X和Y坐标(相对于窗口客户区),wParam可能包含按键状态(如Ctrl键是否同时按下)。

4.6.2 消息队列的运作

为了防止事件丢失并有序处理,Windows为每个线程(特别是拥有窗口的线程)维护了一个或多个消息队列。当事件发生时,系统会将对应的消息投递(PostMessage)或发送(SendMessage)到相应线程的队列中。

  • 系统消息队列:接收所有原始的硬件输入(键盘敲击、鼠标移动)。
  • 线程消息队列:每个GUI线程都有一个。系统从系统队列中取出消息,经过翻译和分配,放入目标窗口所属线程的消息队列中。

游戏的主循环(消息泵)的核心任务就是从本线程的消息队列中不断地取出(GetMessagePeekMessage)这些MSG,并将其分派(DispatchMessage)给操作系统,操作系统则会调用对应的窗口过程函数来处理它。这个过程周而复始,构成了游戏运行时除了渲染和逻辑更新之外的另一条生命线。

4.7 窗口创建全流程解析

创建一个可用的窗口是一个系统性的过程,通常分为四个清晰的步骤,我们称之为“四步曲”。

4.7.1 设计窗口类(WNDCLASS/WNDCLASSEX)

在创建窗口前,你必须先设计一个“蓝图”,告诉系统你想要创建的窗口具有哪些共同的属性和行为。这个蓝图就是窗口类。注意,此“类”非C++中的类,而是一个描述窗口特征的结构体。

// 更现代的WNDCLASSEX结构体,包含大小信息
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);          // 结构体大小,必须设置
wc.style = CS_HREDRAW | CS_VREDRAW;      // 类样式:水平/垂直重绘时重绘整个窗口
wc.lpfnWndProc = WindowProc;             // **核心**:指向窗口过程函数的指针
wc.hInstance = hInstance;                 // 程序实例句柄
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); // 窗口图标
wc.hCursor = LoadCursor(NULL, IDC_ARROW);   // 窗口内光标形状
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // 背景色
wc.lpszMenuName = NULL;                    // 菜单资源名,无则为NULL
wc.lpszClassName = L"MyGameWindowClass";   // **唯一标识**:这个窗口类的名称
wc.hIconSm = NULL;                         // 小图标

关键字段:

  • lpfnWndProc:这是窗口的“大脑”,指定一个回调函数,所有发送给该窗口的消息都将由此函数处理。这是游戏交互逻辑的起点。
  • lpszClassName:为这个蓝图取一个唯一的名字。后续创建窗口时,就通过这个名字来指定使用哪个蓝图。
  • hbrBackground:指定默认的背景擦除方式。对于游戏,我们通常自己处理全部绘制,所以常设为NULL,并在WM_PAINT或游戏循环中自行绘制。
4.7.2 注册窗口类

设计好蓝图后,需要向系统“注册”它,使其可供使用。

if (!RegisterClassEx(&wc))
{
    // 注册失败,可能是类名重复等错误
    MessageBox(NULL, L"窗口类注册失败!", L"错误", MB_ICONERROR);
    return 0;
}

注册成功后,MyGameWindowClass这个名字就与你在wc中定义的一系列属性(特别是WindowProc函数)绑定在一起了。

4.7.3 创建窗口实例

使用注册好的类名,调用CreateWindowEx函数来创建具体的窗口实例。

HWND hWnd = CreateWindowEx(
    0,                              // 扩展窗口样式,如WS_EX_TOPMOST(置顶)
    L"MyGameWindowClass",           // 上一步注册的类名
    L"我的游戏窗口",                // 窗口标题
    WS_OVERLAPPEDWINDOW,           // 窗口样式:标准重叠窗口,含标题栏、边框等
    CW_USEDEFAULT, CW_USEDEFAULT,  // 窗口初始x, y位置(使用默认值)
    800, 600,                       // 窗口初始宽度、高度
    NULL,                           // 父窗口句柄,无则为NULL
    NULL,                           // 菜单句柄
    hInstance,                      // 程序实例句柄
    NULL                            // 创建参数,可传递自定义数据
);

if (hWnd == NULL)
{
    MessageBox(NULL, L"窗口创建失败!", L"错误", MB_ICONERROR);
    return 0;
}

CreateWindowEx函数执行了复杂的工作:在内存中分配窗口对象、建立内部数据结构、并将其与指定的窗口类关联。它返回一个HWND句柄,这是后续所有操作(移动、显示、绘制、接收消息)该窗口的唯一凭证。

4.7.4 显示与更新窗口

创建成功后,窗口在内存中已存在,但默认是不可见的。需要将其显示出来。

ShowWindow(hWnd, nCmdShow); // nCmdShow来自WinMain参数
UpdateWindow(hWnd);          // 立即发送WM_PAINT消息,触发首次绘制

ShowWindow控制窗口的可见性,UpdateWindow会强制窗口立即更新客户区,绕过消息队列直接向窗口过程发送一个WM_PAINT消息,确保窗口一出现就有内容(否则可能先显示一片空白,等待消息队列处理时才绘制)。

4.8 两种核心消息循环体系

消息循环是游戏程序的主脉搏。根据游戏类型和性能需求,主要存在两种实现模式。

4.8.1 以GetMessage为核心的传统循环

这是一种阻塞式的消息泵。GetMessage会从消息队列中取出一个消息。如果队列为空,它会等待(挂起线程),直到有新消息到来。

MSG msg = {};
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
    TranslateMessage(&msg); // 将按键消息转换为字符消息
    DispatchMessage(&msg);  // 将消息分派给窗口过程
}
// 当GetMessage检索到WM_QUIT消息时,返回0,循环结束

这种循环简单、节能(CPU在等待时休眠),适用于大多数普通的窗口应用程序。但在实时性要求极高的游戏中,如果主线程在GetMessage处休眠,意味着你的游戏逻辑和渲染也会停止,这显然是不可接受的。因此,纯GetMessage循环很少用于主游戏循环。

4.8.2 以PeekMessage为核心的实时循环

这是游戏开发中的标准模式。PeekMessage会“窥视”一下消息队列,有消息就取出,没有消息则立即返回FALSE,不会阻塞。

MSG msg = {};
while (true) // 游戏主循环
{
    // 1. 处理所有待处理的消息
    while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
    {
        if (msg.message == WM_QUIT)
        {
            // 收到退出消息,跳出游戏循环
            return (int)msg.wParam;
        }
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // 2. **关键**:在没有消息的空闲时间,执行游戏逻辑和渲染
    UpdateGameLogic(deltaTime); // 更新物理、AI、状态等
    RenderFrame();              // 渲染一帧图像

    // 3. 可以插入短暂的精确睡眠,以避免过度占用CPU
    // Sleep(1);
}

这种“Peek + 处理 + 更新/渲染”的模式,确保了游戏世界持续运转,同时又能及时响应用户输入。几乎所有商业PC游戏,从《反恐精英:全球攻势》到《艾尔登法环》,其底层都运行着这样一个或类似结构的循环。PM_REMOVE参数表示将消息从队列中移除。

4.9 窗口的中枢神经:窗口过程函数(Window Procedure)

窗口过程函数是窗口的“事件处理器”。它是一个由你编写的回调函数,系统在需要处理发送到该窗口的消息时自动调用它。

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    // 根据消息类型进行分支处理
    switch (uMsg)
    {
        case WM_KEYDOWN:
            // 处理键盘按下,wParam是虚拟键码
            if (wParam == VK_ESCAPE) // 按下ESC键
            {
                DestroyWindow(hwnd); // 销毁窗口,这会触发WM_DESTROY
            }
            return 0;

        case WM_LBUTTONDOWN:
            {
                // 处理鼠标左键按下
                int xPos = GET_X_LPARAM(lParam);
                int yPos = GET_Y_LPARAM(lParam);
                // ... 游戏逻辑,例如选中单位、发射子弹 ...
            }
            return 0;

        case WM_PAINT:
            {
                // 处理窗口绘制请求
                PAINTSTRUCT ps;
                HDC hdc = BeginPaint(hwnd, &ps);
                // ... 使用hdc进行绘图操作 ...
                EndPaint(hwnd, &ps);
            }
            return 0;

        case WM_DESTROY:
            // 窗口被销毁时,发出退出消息,结束消息循环
            PostQuitMessage(0);
            return 0;

        default:
            // 对于不处理的消息,必须交给默认处理函数
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
}

这个函数是游戏交互的灵魂。它将系统的消息(原始事件)转化为游戏内的具体动作。DefWindowProc是系统的默认处理器,负责处理那些最基础的窗口管理消息(如移动、缩放、关闭按钮点击),务必将不处理的消息传给它,否则窗口会出现怪异行为。

4.10 资源清理:窗口类的注销

当程序结束时,理论上应该注销已注册的窗口类。这通过UnregisterClass函数完成。

UnregisterClass(L"MyGameWindowClass", hInstance);

在现代Windows编程中,由于系统会在进程结束时自动清理其所有资源,此步骤常常被省略。但在一些动态加载/卸载DLL的场景中,显式注销是一个好习惯。

4.11 实战:一个完整游戏窗口程序的诞生

让我们将本章所有知识熔于一炉,创建一个结构完整、带简单动画和交互的“躲避方块”游戏雏形。此代码可在VS2022和VSCode(配置好MSVC或MinGW工具链)中编译运行。

#include <windows.h>
#include <string>
#include <ctime>

// 全局变量(为简化示例,实际项目应避免过多全局变量)
const int windowWidth = 800;
const int windowHeight = 600;
const int playerSize = 40;
const int obstacleSize = 30;
const int moveSpeed = 5;

int playerX = windowWidth / 2;
int playerY = windowHeight - 100;
int obstacleX = 0;
int obstacleY = 0;
int obstacleSpeedX = 3;
int obstacleSpeedY = 3;
int score = 0;
bool gameRunning = true;

// 前向声明窗口过程函数
LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
void UpdateGameLogic();
void RenderGame(HDC hdc);

// WinMain入口点
int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance,
                    _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
    // 初始化随机种子
    srand(static_cast<unsigned int>(time(nullptr)));
    obstacleX = rand() % (windowWidth - obstacleSize);
    obstacleY = rand() % (windowHeight / 2);

    // 1. 设计并注册窗口类
    const wchar_t className[] = L"GameWindowClass";

    WNDCLASSEX wc = {};
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = MainWindowProc;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor(nullptr, IDC_CROSS); // 使用十字准星光标
    wc.hbrBackground = nullptr; // 我们不依赖系统擦除背景
    wc.lpszClassName = className;
    wc.hIcon = LoadIcon(nullptr, IDI_APPLICATION);

    if (!RegisterClassEx(&wc))
    {
        MessageBox(nullptr, L"窗口类注册失败!", L"错误", MB_OK | MB_ICONERROR);
        return 0;
    }

    // 2. 创建窗口
    HWND hwnd = CreateWindowEx(
        0,
        className,
        L"躲避方块 - Windows游戏编程实战",
        WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX, // 无调整大小边框和最大化按钮
        CW_USEDEFAULT, CW_USEDEFAULT,
        windowWidth, windowHeight,
        nullptr,
        nullptr,
        hInstance,
        nullptr
    );

    if (!hwnd)
    {
        MessageBox(nullptr, L"窗口创建失败!", L"错误", MB_OK | MB_ICONERROR);
        UnregisterClass(className, hInstance);
        return 0;
    }

    // 3. 显示窗口
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    // 4. 游戏主循环(PeekMessage模式)
    MSG msg = {};
    auto lastTime = GetTickCount64();

    while (gameRunning)
    {
        // 处理窗口消息
        while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
        {
            if (msg.message == WM_QUIT)
            {
                gameRunning = false;
                break;
            }
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        // 计算时间增量(Delta Time),使游戏速度与帧率无关
        auto currentTime = GetTickCount64();
        float deltaTime = (currentTime - lastTime) / 1000.0f; // 转换为秒
        lastTime = currentTime;

        // 更新游戏逻辑
        UpdateGameLogic();

        // 渲染(触发WM_PAINT)
        InvalidateRect(hwnd, nullptr, FALSE); // 请求重绘,不擦除背景
        UpdateWindow(hwnd); // 立即更新,实现即时渲染

        // 短暂睡眠以让出CPU时间片
        Sleep(10);
    }

    // 5. 清理(此处可注销窗口类,现代Windows通常省略)
    // UnregisterClass(className, hInstance);
    return 0;
}

// 窗口过程函数
LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_KEYDOWN:
            switch (wParam)
            {
                case VK_LEFT:  playerX -= moveSpeed; break;
                case VK_RIGHT: playerX += moveSpeed; break;
                case VK_UP:    playerY -= moveSpeed; break;
                case VK_DOWN:  playerY += moveSpeed; break;
                case VK_ESCAPE: DestroyWindow(hwnd); break;
            }
            // 限制玩家在窗口内移动
            if (playerX < 0) playerX = 0;
            if (playerX > windowWidth - playerSize) playerX = windowWidth - playerSize;
            if (playerY < 0) playerY = 0;
            if (playerY > windowHeight - playerSize) playerY = windowHeight - playerSize;
            return 0;

        case WM_PAINT:
            {
                PAINTSTRUCT ps;
                HDC hdc = BeginPaint(hwnd, &ps);
                RenderGame(hdc);
                EndPaint(hwnd, &ps);
            }
            return 0;

        case WM_DESTROY:
            gameRunning = false;
            PostQuitMessage(0);
            return 0;

        default:
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
}

// 更新游戏逻辑
void UpdateGameLogic()
{
    // 移动障碍物
    obstacleX += obstacleSpeedX;
    obstacleY += obstacleSpeedY;

    // 边界碰撞检测与反弹
    if (obstacleX <= 0 || obstacleX >= windowWidth - obstacleSize)
    {
        obstacleSpeedX = -obstacleSpeedX;
        score++; // 碰到边界加分
    }
    if (obstacleY <= 0 || obstacleY >= windowHeight - obstacleSize)
    {
        obstacleSpeedY = -obstacleSpeedY;
        score++;
    }

    // 玩家与障碍物碰撞检测
    if (playerX < obstacleX + obstacleSize &&
        playerX + playerSize > obstacleX &&
        playerY < obstacleY + obstacleSize &&
        playerY + playerSize > obstacleY)
    {
        // 碰撞发生,游戏结束提示
        MessageBox(nullptr, L"你被撞到了!游戏结束。", L"Game Over", MB_OK);
        gameRunning = false;
    }
}

// 渲染游戏画面
void RenderGame(HDC hdc)
{
    // 创建画笔和画刷
    HPEN playerPen = CreatePen(PS_SOLID, 2, RGB(0, 120, 215)); // 蓝色边框
    HPEN obstaclePen = CreatePen(PS_SOLID, 2, RGB(220, 50, 32)); // 红色边框
    HBRUSH playerBrush = CreateSolidBrush(RGB(0, 174, 240)); // 浅蓝填充
    HBRUSH obstacleBrush = CreateSolidBrush(RGB(255, 105, 97)); // 浅红填充

    // 选择对象到设备上下文,并保存旧对象
    HGDIOBJ oldPen = SelectObject(hdc, playerPen);
    HGDIOBJ oldBrush = SelectObject(hdc, playerBrush);

    // 1. 绘制玩家方块(圆角矩形)
    RoundRect(hdc, playerX, playerY, playerX + playerSize, playerY + playerSize, 10, 10);

    // 2. 绘制障碍物方块
    SelectObject(hdc, obstaclePen);
    SelectObject(hdc, obstacleBrush);
    Rectangle(hdc, obstacleX, obstacleY, obstacleX + obstacleSize, obstacleY + obstacleSize);

    // 3. 绘制分数和提示文字
    SelectObject(hdc, GetStockObject(BLACK_PEN));
    SelectObject(hdc, GetStockObject(WHITE_BRUSH));
    std::wstring scoreText = L"得分: " + std::to_wstring(score);
    std::wstring hintText = L"使用方向键移动蓝色方块躲避红色方块";
    TextOut(hdc, 10, 10, scoreText.c_str(), static_cast<int>(scoreText.length()));
    TextOut(hdc, 10, windowHeight - 30, hintText.c_str(), static_cast<int>(hintText.length()));

    // 4. 恢复旧的GDI对象并删除创建的,防止资源泄漏
    SelectObject(hdc, oldPen);
    SelectObject(hdc, oldBrush);
    DeleteObject(playerPen);
    DeleteObject(obstaclePen);
    DeleteObject(playerBrush);
    DeleteObject(obstacleBrush);
}

这个程序综合运用了窗口创建、消息处理、PeekMessage循环、GDI基本绘图、简单的碰撞检测和游戏状态管理。玩家控制蓝色方块躲避移动的红色方块,碰撞则游戏结束。代码严格遵循Allman风格和驼峰命名法,结构清晰,是理解本章所有概念的绝佳实践。

4.12 命名规范的重要性

在结束本章前,必须强调命名规范。良好的命名是代码可读性、可维护性和团队协作的基石。

  • 驼峰命名法(CamelCase)
    • lowerCamelCase:用于变量和函数名,如playerHealth, calculateDamage()
    • UpperCamelCase(帕斯卡命名法):用于类名、结构体名,如GameObject, RenderSystem
  • 匈牙利命名法:在早期Windows编程中流行(如hwnd, lpszClassName),现代C++中已不鼓励使用,因其增加了代码冗余。但了解其历史有助于阅读旧代码或某些文档。
  • 常量:通常使用全大写和下划线,如MAX_PLAYERS, WINDOW_WIDTH
  • 成员变量:常见前缀m_(如m_position)或后缀_(如health_),以区分于局部变量。

建立并遵守一致的命名规范,能让你的游戏项目代码库像一本好书一样易于阅读和理解,尤其是在进行长时间开发或与他人合作时。

4.13 本章核心回顾

本章我们系统性地构建了Windows游戏编程的核心知识框架。我们从宏观体系出发,理解了API与SDK的角色;深入程序起点WinMain,认识了窗口、句柄和消息队列这些基础组件;详细拆解了窗口创建的四步流程;对比了GetMessagePeekMessage两种消息循环的适用场景;最终将所有理论付诸实践,完成了一个可交互的简易游戏。

掌握这些基础,意味着你已经拿到了进入Windows游戏开发大门的钥匙。虽然现代游戏开发越来越多地依赖高级引擎和框架,但这些底层原理始终是性能优化、深度调试和实现引擎未覆盖功能的有力武器。在下一章,我们将以此为基础,向图形绘制的世界迈进,探索如何让游戏画面真正“动”起来。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐