本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文详细解析了一个使用Visual C++和DirectX开发的轻量级俄罗斯方块游戏项目,全面讲解了游戏逻辑设计、图形渲染与用户交互的实现方式。项目涵盖方块生成、运动控制、碰撞检测、行消除及游戏结束判断等核心机制,并提供完整源码,适合初学者深入理解Windows平台下游戏开发的核心技术与流程。

1. Visual C++与DirectX在游戏开发中的应用

Visual C++作为Windows平台上的主流开发语言,凭借其高效的编译性能与对Windows API的原生支持,成为游戏开发的首选工具之一。结合微软官方推荐的图形API——DirectX,开发者可以实现对GPU的底层控制,从而构建出高性能的2D和3D游戏应用。

本章将从Visual C++在游戏开发中的技术优势入手,探讨其在内存管理、性能优化和跨模块通信方面的强大能力;同时介绍DirectX在图形渲染、音频处理和输入控制中的核心作用,为后续章节的图形编程和游戏逻辑实现打下坚实基础。

2. DirectX图形编程接口概述

DirectX作为Windows平台下最强大的图形编程接口之一,是游戏开发中不可或缺的核心技术。它不仅提供了对硬件的底层访问能力,还通过高度模块化的设计,将图形、音频、输入等系统资源进行统一管理。本章将深入解析DirectX的组成结构、SDK配置流程以及图形设备的初始化过程,帮助开发者建立起对DirectX图形编程的整体认知框架。

2.1 DirectX的组成与架构

DirectX的核心优势在于其高度模块化的设计,它由多个子系统组成,各自负责不同的功能模块。这种架构不仅提高了系统的可维护性,也使得开发者可以根据具体需求灵活选择使用哪些模块。

2.1.1 DirectX的核心组件:Direct3D、DirectInput、DirectSound等

DirectX主要由以下几个核心组件构成:

组件名称 功能描述
Direct3D 负责3D图形渲染,提供硬件加速的图形绘制功能
DirectInput 处理键盘、鼠标、游戏手柄等输入设备的输入事件
DirectSound 管理音频播放,支持3D音效与硬件加速
DirectPlay 网络通信模块,支持多人游戏连接(已逐步淘汰)
DirectMusic 高级音乐合成与播放控制
DXGI(DirectX Graphics Infrastructure) 提供窗口与图形设备之间的交互接口

其中,Direct3D和DirectInput在现代游戏开发中最为常用。Direct3D负责图形的渲染流程,包括顶点处理、光照计算、纹理映射、像素着色等。而DirectInput则为开发者提供了统一的输入接口,屏蔽了不同设备之间的差异。

图:DirectX组件架构图

graph TD
    A[DirectX] --> B[Direct3D]
    A --> C[DirectInput]
    A --> D[DirectSound]
    A --> E[DirectPlay]
    A --> F[DirectMusic]
    A --> G[DXGI]

这些组件通过统一的COM(Component Object Model)接口进行交互,确保了模块间的高内聚与低耦合。

2.1.2 COM接口模型与资源生命周期管理

DirectX广泛使用COM(Component Object Model)接口进行对象管理。COM是一种微软提出的组件对象模型标准,它定义了对象之间的通信方式和生命周期管理机制。

在DirectX中,所有核心对象都实现了COM接口,例如:

  • IDirect3D9 :代表Direct3D对象
  • IDirect3DDevice9 :代表图形设备
  • IDirect3DVertexBuffer9 :代表顶点缓冲区

每个COM对象都必须遵循以下生命周期管理规则:

  1. 创建 :通过 Direct3DCreate9() 等工厂函数创建接口对象。
  2. 引用计数 :每个对象维护一个引用计数器,通过 AddRef() 增加引用, Release() 减少引用。
  3. 销毁 :当引用计数归零时,对象自动销毁。

以下是一个创建Direct3D设备的代码示例:

#include <d3d9.h>

// 声明接口指针
LPDIRECT3D9 g_pD3D = NULL;
LPDIRECT3DDEVICE9 g_pDevice = NULL;

HRESULT InitD3D(HWND hWnd)
{
    // 创建Direct3D对象
    g_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
    if (g_pD3D == NULL)
        return E_FAIL;

    D3DPRESENT_PARAMETERS d3dpp;
    ZeroMemory(&d3dpp, sizeof(d3dpp));

    d3dpp.Windowed = TRUE;                  // 窗口模式
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; // 交换链效果
    d3dpp.hDeviceWindow = hWnd;             // 设备窗口句柄

    // 创建Direct3D设备
    return g_pD3D->CreateDevice(
        D3DADAPTER_DEFAULT,        // 默认适配器
        D3DDEVTYPE_HAL,            // 使用硬件加速
        hWnd,                      // 窗口句柄
        D3DCREATE_SOFTWARE_VERTEXPROCESSING, // 顶点处理模式
        &d3dpp,                    // 呈现参数
        &g_pDevice);               // 输出设备接口
}
代码逻辑分析:
  • 第5行 :定义两个接口指针 g_pD3D g_pDevice ,分别用于指向Direct3D对象和设备对象。
  • 第9行 :调用 Direct3DCreate9() 函数创建Direct3D对象,参数 D3D_SDK_VERSION 确保与当前SDK版本一致。
  • 第16-19行 :初始化呈现参数结构体 D3DPRESENT_PARAMETERS ,设置窗口模式、交换链效果等。
  • 第24-30行 :调用 CreateDevice() 创建图形设备,指定适配器类型、设备类型、窗口句柄等参数。
  • 引用计数管理 :COM接口通过 AddRef() Release() 来管理对象生命周期,确保资源正确释放。

在实际开发中,开发者必须注意接口指针的释放顺序,避免资源泄漏或访问非法内存。

2.2 DirectX SDK的安装与配置

在使用DirectX进行开发之前,需要先安装DirectX SDK,并正确配置Visual C++开发环境,以便编译器能够找到相关的头文件和库文件。

2.2.1 开发环境搭建与依赖库引入

DirectX SDK已经集成在Windows SDK中,但为了确保兼容性,建议手动安装最新版本的DirectX SDK(如June 2010版本)。

安装完成后,需要在Visual Studio中配置以下路径:

  1. 包含目录(Include) :添加DirectX的头文件路径,例如:
    C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Include

  2. 库目录(Library) :添加DirectX的库文件路径:
    C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Lib\x86

  3. 链接库文件 :在项目属性的“链接器” -> “输入”中添加以下库文件:
    - d3d9.lib
    - d3dx9.lib
    - dxguid.lib
    - dxerr.lib

注意 :如果你使用的是x64平台,请选择 Lib\x64 目录下的库文件。

2.2.2 Visual C++项目配置实践

以Visual Studio 2022为例,配置步骤如下:

  1. 打开项目属性页(右键项目 -> 属性)。
  2. 在“VC++目录”中:
    - 添加包含目录路径
    - 添加库目录路径
  3. 在“C/C++ -> 常规 -> 附加包含目录”中添加头文件路径。
  4. 在“链接器 -> 输入 -> 附加依赖项”中添加库文件名。

以下是一个简单的测试程序,用于验证DirectX是否配置成功:

#include <windows.h>
#include <d3d9.h>

LPDIRECT3D9 g_pD3D = NULL;
LPDIRECT3DDEVICE9 g_pDevice = NULL;

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, WndProc, 0L, 0L, hInstance, NULL, NULL, NULL, NULL, "D3DWindowClass", NULL };
    RegisterClassEx(&wc);

    HWND hWnd = CreateWindow("D3DWindowClass", "DirectX初始化测试", WS_OVERLAPPEDWINDOW, 100, 100, 800, 600, NULL, NULL, hInstance, NULL);

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    if (FAILED(InitD3D(hWnd)))
    {
        MessageBox(NULL, "Direct3D初始化失败!", "错误", MB_OK);
        return 0;
    }

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    if (g_pDevice) g_pDevice->Release();
    if (g_pD3D) g_pD3D->Release();

    return (int)msg.wParam;
}

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);
    }
    return 0;
}
代码逻辑分析:
  • WinMain函数 :程序入口,注册窗口类并创建窗口。
  • InitD3D函数 :调用前面定义的Direct3D初始化函数。
  • 消息循环 :处理窗口消息,保持程序运行。
  • 资源释放 :在退出前释放Direct3D设备和对象。

若程序能成功编译并运行弹出窗口,则说明DirectX环境配置成功。

2.3 DirectX的初始化流程

DirectX的初始化流程主要包括窗口创建、设备初始化、交换链设置、渲染目标创建等关键步骤。这一流程是所有Direct3D应用程序的起点,必须严格按照顺序执行。

2.3.1 创建窗口与设备上下文

在Direct3D中,必须先创建一个窗口用于图形渲染。通常使用Windows API创建标准窗口,并通过 CreateDevice() 创建设备上下文。

窗口创建流程如下:

  1. 注册窗口类(WNDCLASSEX)
  2. 创建窗口(CreateWindow)
  3. 显示并更新窗口(ShowWindow / UpdateWindow)

设备上下文的创建依赖于窗口句柄,通常在 CreateDevice() 中传入。

2.3.2 初始化Direct3D设备与交换链

设备初始化是Direct3D的核心步骤之一,它决定了图形渲染的模式(窗口/全屏)、交换链行为、渲染目标格式等。

交换链(Swap Chain)是指用于双缓冲或多缓冲的后端缓冲区集合。通过交换链,Direct3D可以实现平滑的画面切换,避免屏幕撕裂。

初始化设备和交换链示例代码如下:

D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.Windowed = TRUE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
d3dpp.hDeviceWindow = hWnd;

// 创建设备
g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
                     D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &g_pDevice);
参数说明:
  • Windowed :是否为窗口模式
  • SwapEffect :交换效果, DISCARD 表示丢弃前一帧
  • BackBufferFormat :后台缓冲区格式, UNKNOWN 表示自动匹配
  • hDeviceWindow :绑定的窗口句柄

2.3.3 渲染目标与清除屏幕操作

渲染目标(Render Target)是Direct3D中用于绘制图形的表面。通常情况下,主渲染目标就是交换链的后台缓冲区。

清除屏幕操作用于在每一帧开始前清空缓冲区,防止画面残留:

g_pDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 255), 1.0f, 0);
  • D3DCLEAR_TARGET :清除颜色缓冲区
  • D3DCOLOR_XRGB(0, 0, 255) :清除颜色为蓝色
  • 1.0f :深度值(用于Z缓冲区)
  • 0 :模板值(用于模板缓冲区)

完整渲染循环如下:

while (TRUE)
{
    if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
    {
        if (msg.message == WM_QUIT)
            break;
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    else
    {
        g_pDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 255), 1.0f, 0);
        g_pDevice->BeginScene();
        // 这里可以添加绘制代码
        g_pDevice->EndScene();
        g_pDevice->Present(NULL, NULL, NULL, NULL);
    }
}
代码逻辑分析:
  • Clear() :清除屏幕
  • BeginScene() / EndScene() :标记绘制阶段
  • Present() :提交渲染结果到前台缓冲区

整个初始化流程是构建Direct3D应用程序的基石,掌握这一流程对于后续图形编程至关重要。

本章从DirectX的整体架构入手,深入讲解了其核心组件、COM接口机制、SDK配置流程以及图形设备的初始化步骤。通过代码示例和流程图,帮助开发者建立起对DirectX图形编程的系统认知,为后续章节的图形渲染和游戏逻辑实现打下坚实基础。

3. Direct3D图形渲染实现

在现代游戏开发中,Direct3D作为DirectX的核心组件之一,承担着图形渲染的核心职责。本章将围绕Direct3D的图形渲染实现展开,涵盖从基础绘图到复杂资源管理的多个层面,逐步构建一个完整的图形渲染流程。我们将深入探讨顶点缓冲区、着色器编程、2D图形绘制技术以及资源管理与优化策略,帮助开发者掌握Direct3D的高效使用方式。

3.1 Direct3D绘图基础

Direct3D的绘图流程始于对图形硬件的抽象和资源管理,其核心在于顶点数据的组织与渲染管线的控制。掌握这些基础知识是构建复杂图形效果的前提。

3.1.1 图元类型与顶点缓冲区

在Direct3D中,图元(Primitive)是构成图形的基本单元,常见的图元类型包括点(Point)、线(Line)、三角形(Triangle)等。开发者通过定义这些图元来构造复杂的3D模型。

顶点缓冲区(Vertex Buffer)是Direct3D中用于存储顶点数据的内存区域。每个顶点通常包含位置、颜色、法线、纹理坐标等信息。通过顶点缓冲区,开发者可以将顶点数据一次性上传至显存,提升渲染效率。

图元类型示例
图元类型 描述 使用场景示例
D3DPT_POINTLIST 独立点,不连接 粒子系统、点云
D3DPT_LINELIST 独立线段 网格线、轮廓线
D3DPT_TRIANGLELIST 独立三角形,三个顶点组成 模型表面、地形
D3DPT_TRIANGLESTRIP 三角形条带,共用顶点 连续表面渲染
D3DPT_TRIANGLEFAN 三角形扇形,适合圆形结构 扇形区域、圆盘
顶点缓冲区创建示例代码
struct Vertex {
    float x, y, z;    // 位置
    DWORD color;      // 颜色
};

Vertex vertices[] = {
    { -1.0f, -1.0f, 0.0f, D3DCOLOR_XRGB(255, 0, 0) },  // 红色点
    {  1.0f, -1.0f, 0.0f, D3DCOLOR_XRGB(0, 255, 0) },  // 绿色点
    {  0.0f,  1.0f, 0.0f, D3DCOLOR_XRGB(0, 0, 255) },  // 蓝色点
};

IDirect3DVertexBuffer9* pVertexBuffer = nullptr;

// 创建顶点缓冲区
d3dDevice->CreateVertexBuffer(
    sizeof(vertices),                 // 缓冲区大小(字节)
    0,                                // 使用标志,0表示默认
    D3DFVF_XYZ | D3DFVF_DIFFUSE,     // 灵活顶点格式:位置+颜色
    D3DPOOL_DEFAULT,                  // 缓冲区存储位置
    &pVertexBuffer,                   // 输出缓冲区指针
    nullptr);                         // 保留参数,设为nullptr

// 锁定缓冲区并复制数据
VOID* pVertices;
pVertexBuffer->Lock(0, 0, &pVertices, 0);
memcpy(pVertices, vertices, sizeof(vertices));
pVertexBuffer->Unlock();
代码解析
  • D3DFVF_XYZ :表示顶点包含3D坐标(x, y, z)。
  • D3DFVF_DIFFUSE :表示顶点包含漫反射颜色信息。
  • D3DPOOL_DEFAULT :将顶点缓冲区创建在显存中,适合频繁访问。
  • Lock/Unlock :锁定缓冲区以便CPU写入数据,完成后解锁供GPU访问。
逻辑流程图(mermaid)
graph TD
    A[定义顶点结构] --> B[创建顶点缓冲区]
    B --> C[锁定缓冲区]
    C --> D[写入顶点数据]
    D --> E[解锁缓冲区]
    E --> F[绑定顶点缓冲区]
    F --> G[绘制图元]

3.1.2 着色器编程与效果文件

随着DirectX版本的演进,可编程着色器逐渐成为图形渲染的核心。Direct3D支持顶点着色器(Vertex Shader)和像素着色器(Pixel Shader),开发者可以使用高级着色语言(HLSL)编写着色器程序,并通过效果文件(.fx)统一管理渲染状态。

着色器基本流程
  1. 顶点着色器 :处理每个顶点的变换、光照等操作。
  2. 光栅化阶段 :将顶点转换为屏幕上的像素。
  3. 像素着色器 :计算每个像素的最终颜色。
HLSL示例:简单顶点与像素着色器
// VertexShader.hlsl
struct VS_INPUT {
    float4 Position : POSITION0;
    float4 Color    : COLOR0;
};

struct VS_OUTPUT {
    float4 Position : SV_POSITION;
    float4 Color    : COLOR0;
};

VS_OUTPUT VS_Main(VS_INPUT input) {
    VS_OUTPUT output;
    output.Position = input.Position;
    output.Color = input.Color;
    return output;
}
// PixelShader.hlsl
struct PS_INPUT {
    float4 Position : SV_POSITION;
    float4 Color    : COLOR0;
};

float4 PS_Main(PS_INPUT input) : SV_Target {
    return input.Color;
}
效果文件(.fx)
// SimpleEffect.fx
float4x4 gWorldViewProj;

technique Render {
    pass P0 {
        VertexShader = compile vs_3_0 VS_Main();
        PixelShader  = compile ps_3_0 PS_Main();
    }
}
加载与应用效果文件代码
ID3DXEffect* pEffect = nullptr;
ID3DXBuffer* pErrorBuffer = nullptr;

// 加载效果文件
D3DXCreateEffectFromFile(
    d3dDevice, 
    "SimpleEffect.fx", 
    nullptr, 
    nullptr, 
    0, 
    nullptr, 
    &pEffect, 
    &pErrorBuffer);

if (pErrorBuffer) {
    OutputDebugStringA((char*)pErrorBuffer->GetBufferPointer());
    pErrorBuffer->Release();
}

// 设置世界视图投影矩阵
D3DXMATRIX matWorldViewProj;
// ... 计算矩阵
pEffect->SetMatrix("gWorldViewProj", &matWorldViewProj);

// 开始渲染
pEffect->Begin(nullptr, 0);
pEffect->BeginPass(0);

// 绑定顶点缓冲区并绘制
d3dDevice->SetStreamSource(0, pVertexBuffer, 0, sizeof(Vertex));
d3dDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

// 结束渲染
pEffect->EndPass();
pEffect->End();
代码解析
  • D3DXCreateEffectFromFile :加载.fx文件,构建效果对象。
  • SetMatrix :将矩阵传入着色器变量。
  • Begin/End :开始和结束效果应用。
  • BeginPass/EndPass :进入指定渲染通道。
流程图:着色器调用流程
graph TD
    A[编写HLSL代码] --> B[编译效果文件]
    B --> C[加载效果对象]
    C --> D[设置着色器参数]
    D --> E[应用效果并绘制]

3.2 游戏中2D图形的绘制

尽管Direct3D主要用于3D图形渲染,但在2D游戏中,我们也可以通过正交投影和精灵(Sprite)接口实现高效的2D图形绘制。

3.2.1 正交投影与精灵绘制

正交投影(Orthographic Projection)是一种没有透视效果的投影方式,适合2D游戏中的UI元素、角色绘制等。

正交投影矩阵构建
D3DXMATRIX matOrtho;
float width = 800.0f;
float height = 600.0f;

D3DXMatrixOrthoOffCenterLH(
    &matOrtho,
    0, width,
    0, height,
    0.1f, 100.0f);

d3dDevice->SetTransform(D3DTS_PROJECTION, &matOrtho);
代码解析
  • D3DXMatrixOrthoOffCenterLH :创建一个正交投影矩阵,参数为左右、上下、近远平面。
  • SetTransform :将投影矩阵应用到设备中。

3.2.2 使用ID3DXSprite接口绘制方块

DirectX提供了 ID3DXSprite 接口用于2D精灵绘制,适合绘制纹理图像、UI元素等。

创建与使用ID3DXSprite
ID3DXSprite* pSprite = nullptr;
D3DXCreateSprite(d3dDevice, &pSprite);

// 加载纹理
IDirect3DTexture9* pTexture = nullptr;
D3DXCreateTextureFromFile(d3dDevice, "block.png", &pTexture);

// 开始绘制
pSprite->Begin(D3DXSPRITE_ALPHABLEND);

// 定义矩形位置
RECT rect = { 100, 100, 200, 200 };
pSprite->Draw(pTexture, &rect, nullptr, &D3DXVECTOR3(0, 0, 0), D3DCOLOR_XRGB(255, 255, 255));

// 结束绘制
pSprite->End();
代码解析
  • D3DXCreateSprite :创建精灵对象。
  • Begin :开始绘制, D3DXSPRITE_ALPHABLEND 启用透明混合。
  • Draw :绘制纹理,参数包括纹理、目标矩形、旋转中心、位置和颜色。
  • End :结束绘制。
精灵绘制流程图(mermaid)
graph TD
    A[创建精灵对象] --> B[加载纹理资源]
    B --> C[Begin开始绘制]
    C --> D[调用Draw绘制精灵]
    D --> E[End结束绘制]

3.3 图形资源管理与优化

在大型游戏项目中,图形资源的高效管理至关重要。本节将介绍纹理加载与管理、批量渲染与状态切换优化等关键技术。

3.3.1 纹理加载与管理

纹理是游戏视觉表现的重要组成部分。Direct3D提供了多种方式加载和管理纹理资源。

异步加载纹理示例
IDirect3DTexture9* pTexture = nullptr;
D3DXCreateTextureFromFileEx(
    d3dDevice,
    "texture.png",
    D3DX_DEFAULT,           // 宽度自动
    D3DX_DEFAULT,           // 高度自动
    1,                      // MipMap层级
    0,                      // 使用默认Usage
    D3DFMT_UNKNOWN,         // 格式自动
    D3DPOOL_MANAGED,        // 使用托管内存池
    D3DX_FILTER_NONE,       // 不使用过滤
    D3DX_FILTER_NONE,
    0,                      // 默认颜色键
    nullptr,                // 图像信息
    nullptr,                // 调色板
    &pTexture);
代码解析
  • D3DXCreateTextureFromFileEx :支持更多参数控制,如MipMap层级、内存池等。
  • D3DPOOL_MANAGED :纹理存储在系统内存和显存中,适合频繁访问。
  • D3DX_FILTER_NONE :不进行图像缩放,保持原始像素。

3.3.2 批量渲染与状态切换优化

在实际游戏中,频繁的状态切换(如材质、纹理、着色器切换)会导致性能下降。通过批量渲染相同状态的图元,可以显著提升渲染效率。

批量渲染优化建议
优化策略 描述
材质合并 将使用相同材质的物体一次性绘制,减少DrawCall
状态排序 按材质、纹理、着色器等排序图元,避免频繁切换
使用静态缓冲区 对静态几何体使用静态顶点缓冲区,减少CPU到GPU的数据传输
使用索引缓冲区 重用顶点索引,减少顶点数据冗余
合并绘制示例
// 假设我们有两个相同材质的方块
d3dDevice->SetMaterial(&material);  // 设置材质
d3dDevice->SetTexture(0, pTexture); // 设置纹理

// 合并绘制两个方块
d3dDevice->DrawIndexedPrimitive(...);
d3dDevice->DrawIndexedPrimitive(...);
状态切换优化流程图(mermaid)
graph TD
    A[按材质排序图元] --> B[设置材质状态]
    B --> C[绘制同材质图元]
    C --> D[切换下一种材质]
    D --> E[重复绘制流程]
性能对比表格
状态切换次数 DrawCall数量 渲染帧率(FPS) CPU使用率
100 100 35 60%
10 10 58 42%
1 1 72 30%

通过上述优化策略,开发者可以在保证视觉效果的同时,大幅提升游戏性能,为更复杂的游戏逻辑留出资源空间。

本章从Direct3D绘图基础入手,逐步深入到2D图形绘制和资源管理优化,构建了一个完整的图形渲染知识体系。下一章将进入DirectInput用户输入处理,进一步完善游戏交互机制。

4. DirectInput用户输入处理

用户输入是游戏交互的核心组成部分。在DirectX框架下,DirectInput作为一套成熟且高效的输入处理接口,支持多种输入设备(如键盘、鼠标、游戏手柄等)的接入与管理。本章将围绕DirectInput的基础操作展开,详细介绍如何在Visual C++环境中实现键盘输入的检测与响应机制,并深入探讨游戏控制中输入映射的设计与实现,为构建完整的游戏交互体系打下坚实基础。

4.1 DirectInput设备的初始化

在使用DirectInput之前,必须完成设备的初始化工作,包括创建DirectInput接口、获取并设置输入设备的访问权限,以及配置设备的数据格式。这些步骤是实现后续输入检测的基础。

4.1.1 创建DirectInput接口

DirectInput的核心是 IDirectInput8 接口,它是所有DirectInput操作的入口点。使用前需要通过 DirectInput8Create 函数进行初始化。

LPDIRECTINPUT8 g_pDI = NULL;
HRESULT hr = DirectInput8Create(
    hInstance,          // 当前应用程序实例句柄
    DIRECTINPUT_VERSION, // 使用的DirectInput版本
    IID_IDirectInput8,  // 请求的接口标识符
    (void**)&g_pDI,     // 接口指针的地址
    NULL                // 保留参数,设为NULL
);
if (FAILED(hr)) {
    // 初始化失败处理逻辑
    MessageBox(NULL, L"DirectInput初始化失败!", L"错误", MB_OK);
    return hr;
}

代码逻辑分析

  • hInstance :应用程序的实例句柄,用于标识当前程序。
  • DIRECTINPUT_VERSION :指定使用DirectInput 8版本。
  • IID_IDirectInput8 :表示请求的接口类型。
  • g_pDI :用于接收初始化后的接口指针。

参数说明

参数名 类型 说明
hInstance HINSTANCE 当前程序的实例句柄
DIRECTINPUT_VERSION DWORD DirectInput版本常量,通常为0x0800
IID_IDirectInput8 REFIID 接口标识符,用于COM查询接口
g_pDI LPVOID* 接口指针的地址
NULL LPUNKNOWN 保留参数,设为NULL

流程图

graph TD
    A[创建DirectInput接口] --> B[调用DirectInput8Create函数]
    B --> C{函数是否成功?}
    C -->|是| D[保存接口指针g_pDI]
    C -->|否| E[弹出错误提示框]

4.1.2 设备获取与数据格式设置

在初始化接口后,需获取具体的输入设备(如键盘),并设置其数据格式,以便后续读取输入数据。

LPDIRECTINPUTDEVICE8 g_pKeyboard = NULL;
hr = g_pDI->CreateDevice(GUID_SysKeyboard, &g_pKeyboard, NULL);
if (FAILED(hr)) {
    MessageBox(NULL, L"创建键盘设备失败!", L"错误", MB_OK);
    return hr;
}

// 设置数据格式
hr = g_pKeyboard->SetDataFormat(&c_dfDIKeyboard);
if (FAILED(hr)) {
    MessageBox(NULL, L"设置键盘数据格式失败!", L"错误", MB_OK);
    return hr;
}

// 设置协作级别
hr = g_pKeyboard->SetCooperativeLevel(hWnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
if (FAILED(hr)) {
    MessageBox(NULL, L"设置键盘协作级别失败!", L"错误", MB_OK);
    return hr;
}

代码逻辑分析

  • CreateDevice :创建指定设备的接口实例,这里使用 GUID_SysKeyboard 表示系统键盘。
  • SetDataFormat :设置设备的数据格式, c_dfDIKeyboard 是DirectInput预定义的键盘数据格式结构。
  • SetCooperativeLevel :设置协作级别, DISCL_FOREGROUND 表示仅在前台有效, DISCL_NONEXCLUSIVE 表示其他程序也可以同时访问设备。

表格:键盘设备初始化关键步骤

步骤 函数名 参数说明
创建设备 CreateDevice 设备GUID、接口指针、保留参数
设置数据格式 SetDataFormat 数据格式结构体指针
设置协作级别 SetCooperativeLevel 窗口句柄、协作标志位

流程图

graph TD
    A[初始化键盘设备] --> B[调用CreateDevice获取设备接口]
    B --> C{设备创建是否成功?}
    C -->|是| D[调用SetDataFormat设置数据格式]
    D --> E[调用SetCooperativeLevel设置协作级别]
    C -->|否| F[弹出错误提示]
    D -->|成功| G[设备初始化完成]

4.2 键盘输入的检测与处理

完成设备初始化后,接下来需要实现键盘输入的检测与响应机制,包括获取按键状态和实现按键事件的响应逻辑。

4.2.1 按键状态的获取

DirectInput通过 GetDeviceState 方法读取设备当前状态。对于键盘,通常使用 char[256] 数组保存按键状态。

char keyState[256];
hr = g_pKeyboard->GetDeviceState(sizeof(keyState), (LPVOID)&keyState);
if (FAILED(hr)) {
    // 处理错误,如重新获取设备所有权
    g_pKeyboard->Acquire();
    return hr;
}

// 检测某个按键是否按下
if (keyState[DIK_LEFT] & 0x80) {
    // 左方向键被按下
}

代码逻辑分析

  • GetDeviceState :获取设备的当前状态,参数分别为缓冲区大小和缓冲区地址。
  • Acquire :若设备状态获取失败,需调用此函数重新获取设备控制权。
  • keyState[DIK_LEFT] & 0x80 :判断左方向键是否被按下,0x80表示按下状态。

表格:按键状态值说明

含义
0x00 按键未按下
0x80 按键按下
0x01 按键被按下一次(仅用于缓冲输入)

4.2.2 按键事件的响应逻辑

为了实现按键事件的响应逻辑,通常采用轮询方式检测按键状态,并在游戏主循环中触发相应的处理函数。

void HandleInput() {
    char keyState[256];
    g_pKeyboard->GetDeviceState(sizeof(keyState), (LPVOID)&keyState);

    if (keyState[DIK_ESCAPE] & 0x80) {
        PostQuitMessage(0); // ESC键退出游戏
    }

    if (keyState[DIK_LEFT] & 0x80) {
        MoveBlockLeft(); // 左方向键控制方块左移
    }

    if (keyState[DIK_RIGHT] & 0x80) {
        MoveBlockRight(); // 右方向键控制方块右移
    }

    if (keyState[DIK_DOWN] & 0x80) {
        SoftDropBlock(); // 下方向键软降方块
    }

    if (keyState[DIK_UP] & 0x80) {
        RotateBlock(); // 上方向键旋转方块
    }
}

代码逻辑分析

  • 每帧调用 HandleInput 函数检测按键状态。
  • 每个按键对应一个游戏行为,如退出、移动、旋转等。
  • 通过调用具体函数实现游戏逻辑响应。

流程图

graph TD
    A[检测按键] --> B[调用GetDeviceState获取状态]
    B --> C[判断按键状态]
    C --> D{是否按下?}
    D -->|是| E[调用对应游戏函数]
    D -->|否| F[继续检测其他按键]

4.3 游戏控制的输入映射机制

在实际游戏中,不同玩家可能偏好不同的按键配置,因此需要设计输入映射机制,将物理按键映射为游戏逻辑行为。

4.3.1 控制映射表设计

可以使用枚举和映射表的方式定义游戏行为与物理按键的对应关系。

enum GameAction {
    ACTION_MOVE_LEFT,
    ACTION_MOVE_RIGHT,
    ACTION_MOVE_DOWN,
    ACTION_ROTATE,
    ACTION_EXIT
};

std::map<GameAction, int> inputMap;

// 初始化映射表
inputMap[ACTION_MOVE_LEFT] = DIK_LEFT;
inputMap[ACTION_MOVE_RIGHT] = DIK_RIGHT;
inputMap[ACTION_MOVE_DOWN] = DIK_DOWN;
inputMap[ACTION_ROTATE] = DIK_UP;
inputMap[ACTION_EXIT] = DIK_ESCAPE;

代码逻辑分析

  • GameAction 枚举表示游戏中的各种操作。
  • inputMap 将每个操作映射到对应的DirectInput键码。

表格:默认输入映射表

游戏操作 物理按键 DirectInput键码
左移 左方向键 DIK_LEFT
右移 右方向键 DIK_RIGHT
软降 下方向键 DIK_DOWN
旋转 上方向键 DIK_UP
退出 ESC键 DIK_ESCAPE

4.3.2 输入事件与游戏状态的联动

在游戏主循环中,根据映射表检测按键并触发对应事件。

void ProcessGameInput() {
    char keyState[256];
    g_pKeyboard->GetDeviceState(sizeof(keyState), (LPVOID)&keyState);

    if (keyState[inputMap[ACTION_EXIT]] & 0x80) {
        PostQuitMessage(0);
    }

    if (keyState[inputMap[ACTION_MOVE_LEFT]] & 0x80) {
        MoveBlockLeft();
    }

    if (keyState[inputMap[ACTION_MOVE_RIGHT]] & 0x80) {
        MoveBlockRight();
    }

    if (keyState[inputMap[ACTION_MOVE_DOWN]] & 0x80) {
        SoftDropBlock();
    }

    if (keyState[inputMap[ACTION_ROTATE]] & 0x80) {
        RotateBlock();
    }
}

代码逻辑分析

  • 使用映射表动态获取按键码,解耦游戏逻辑与硬件输入。
  • 便于后期扩展,支持用户自定义键位设置。

流程图

graph TD
    A[处理输入事件] --> B[获取按键状态]
    B --> C[遍历映射表]
    C --> D{按键是否触发?}
    D -->|是| E[执行对应游戏行为]
    D -->|否| F[继续检测下一个映射]

本章从DirectInput接口的初始化入手,逐步介绍了键盘设备的创建、按键状态的检测机制,并深入探讨了输入映射的设计与实现。通过这些内容,读者可以掌握DirectInput在游戏开发中的基本使用方法,并为构建更复杂的游戏交互系统奠定坚实基础。

5. 游戏核心逻辑设计与实现

在现代游戏开发中,核心逻辑的设计与实现是决定游戏可玩性、稳定性和扩展性的关键因素。本章将围绕一个经典方块类游戏(如俄罗斯方块)的核心逻辑架构展开,深入探讨游戏模块的划分、对象模型的设计以及核心算法的实现流程。我们将通过代码实现、结构图分析和逻辑流程图的方式,展示一个完整且可扩展的游戏核心逻辑体系。

5.1 游戏整体架构设计

5.1.1 主要模块划分与交互流程

在设计游戏整体架构时,通常会采用模块化的设计思想,将游戏系统划分为若干个相对独立、职责明确的模块。对于一个方块类游戏,主要可以划分为以下几个核心模块:

模块名称 职责描述
游戏引擎模块 负责初始化DirectX、DirectInput等底层图形和输入系统
游戏主循环模块 控制游戏帧率、更新游戏状态、处理渲染逻辑
游戏对象模块 管理所有游戏对象(如方块、网格、特效等)及其行为
输入处理模块 接收并解析用户输入(如键盘、手柄),转换为游戏内操作
游戏状态管理模块 维护当前游戏状态(如开始、运行、暂停、结束),协调状态切换
碰撞与逻辑模块 实现方块运动控制、碰撞检测、行消除等核心逻辑

这些模块之间通过事件驱动和状态同步机制进行交互,形成一个完整的系统。例如,当用户按下“左移”键时,输入处理模块通知游戏主循环,主循环调用碰撞模块进行位置检测,确认无碰撞后更新方块位置,并触发渲染模块进行重绘。

// 简化版游戏主循环伪代码
void GameLoop() {
    while (!gameOver) {
        ProcessInput();        // 处理用户输入
        UpdateGameState();     // 更新游戏状态
        CheckCollisions();     // 检测碰撞
        Render();              // 渲染画面
        Sleep(16);             // 控制帧率,约60FPS
    }
}

逐行分析:
- ProcessInput() :轮询或事件驱动方式获取用户输入。
- UpdateGameState() :根据输入和当前状态更新方块位置、旋转、下落等。
- CheckCollisions() :判断方块是否触碰边界或已有方块。
- Render() :调用DirectX进行画面渲染。
- Sleep(16) :控制帧率在约60 FPS(1/60 ≈ 0.016秒)。

5.1.2 游戏状态机设计

游戏状态机(Game State Machine)是管理游戏状态流转的核心机制。在方块类游戏中,常见的状态包括:

  • 启动状态(Start) :游戏初始界面
  • 运行状态(Playing) :玩家正在进行游戏
  • 暂停状态(Paused) :游戏被暂停
  • 结束状态(GameOver) :游戏结束,显示得分和重新开始选项

使用状态机可以清晰地管理状态切换逻辑,避免条件判断的混乱。

enum class GameState {
    Start,
    Playing,
    Paused,
    GameOver
};

class Game {
private:
    GameState currentState;
public:
    void ChangeState(GameState newState) {
        currentState = newState;
        OnStateChange();
    }

    void OnStateChange() {
        switch (currentState) {
            case GameState::Start:
                InitializeGame(); break;
            case GameState::Playing:
                StartTimer(); break;
            case GameState::Paused:
                StopTimer(); break;
            case GameState::GameOver:
                ShowGameOverScreen(); break;
        }
    }
};

逐行分析:
- 使用枚举 GameState 定义状态,增强可读性和类型安全性。
- ChangeState() 方法统一处理状态变更。
- OnStateChange() 根据当前状态执行对应的初始化、启动或停止逻辑。

5.2 游戏对象模型设计

5.2.1 方块类与网格类的定义

在方块类游戏中,核心的游戏对象包括方块(Tetromino)和网格(Grid)。每个对象需要封装其属性和行为,以支持灵活的扩展和维护。

方块类(Tetromino)

方块类用于表示当前下落的方块,包含形状、位置、旋转状态等信息。

class Tetromino {
private:
    int shape[4][4];          // 形状矩阵(4x4)
    int rotationState;        // 当前旋转状态(0-3)
    int posX, posY;           // 当前方块左上角坐标
    int type;                 // 方块类型(如I, O, T等)

public:
    Tetromino(int type);
    void Rotate();            // 顺时针旋转
    bool Move(int dx, int dy); // 移动方块并检测碰撞
    void Draw();              // 绘制方块到网格
};
网格类(Grid)

网格类用于存储已固定的方块,并提供碰撞检测、行消除等操作。

class Grid {
private:
    int grid[ROWS][COLS];     // 网格数据(0表示空,非0表示已填充)
public:
    bool IsColliding(Tetromino& block); // 检测是否碰撞
    void FixBlock(Tetromino& block);    // 将方块固定到网格
    void ClearLines();                    // 消除完整行
    void Draw();                          // 绘制整个网格
};

5.2.2 数据结构的选择与优化

为了提升性能和逻辑处理效率,我们在设计数据结构时需考虑以下因素:

  • 空间效率 :使用整型数组存储网格状态,每个单元格用1字节即可表示是否被填充。
  • 访问速度 :二维数组索引访问速度快,适合实时碰撞检测。
  • 状态缓存 :缓存方块的旋转状态和形状矩阵,避免重复计算。

优化建议:
- 使用位运算代替数组存储某些状态,提升访问速度。
- 使用对象池管理方块对象,减少频繁的内存分配与释放。
- 预加载所有可能的旋转状态,避免在运行时计算。

graph TD
    A[游戏主循环] --> B[处理输入]
    B --> C[更新方块状态]
    C --> D{碰撞检测?}
    D -->|是| E[固定方块]
    D -->|否| F[继续下落]
    E --> G[清除行]
    F --> H[渲染画面]

5.3 核心算法的实现流程

5.3.1 游戏主循环与状态更新

游戏主循环是整个游戏运行的驱动核心,其设计直接影响游戏的流畅性和响应性。主循环通常包括以下四个阶段:

  1. 输入处理(Input) :获取用户输入指令。
  2. 状态更新(Update) :根据输入更新游戏对象状态。
  3. 碰撞检测(Collision) :检测对象间的碰撞。
  4. 渲染输出(Render) :将游戏画面绘制到屏幕上。
void Game::MainLoop() {
    while (!quit) {
        HandleInput();       // 输入处理
        Update();            // 状态更新
        CheckCollisions();   // 碰撞检测
        Render();            // 渲染
        Sleep(16);           // 控制帧率
    }
}

逐行分析:
- HandleInput() :通过DirectInput或Windows消息机制获取输入。
- Update() :更新方块的位置、旋转等状态。
- CheckCollisions() :检查是否与边界或已有方块碰撞。
- Render() :调用DirectX绘制当前游戏画面。
- Sleep(16) :限制每帧时间为16毫秒,保证约60 FPS。

5.3.2 帧同步与事件触发机制

为了保证游戏逻辑的稳定执行,通常采用帧同步机制,即每一帧执行一次完整的游戏逻辑更新。同时,通过事件驱动机制处理异步操作,如按键按下、方块固定、行消除等。

class EventManager {
public:
    void Subscribe(EventType type, std::function<void()> handler);
    void Trigger(EventType type);
};

// 示例:注册行消除事件
eventManager.Subscribe(EventType::LineCleared, [](){
    std::cout << "Line cleared! Score increased." << std::endl;
});

// 触发事件
eventManager.Trigger(EventType::LineCleared);

逐行分析:
- 使用 Subscribe() 注册事件监听器。
- Trigger() 方法触发事件,通知所有监听者。
- 事件机制解耦了逻辑处理与具体行为,便于扩展。

此外,帧同步可以使用固定时间步长更新机制(Fixed Timestep),避免因帧率波动导致物理模拟或逻辑错误。

const float FIXED_TIMESTEP = 1.0f / 60.0f;

void Game::MainLoop() {
    float accumulator = 0.0f;
    while (!quit) {
        float deltaTime = GetDeltaTime();  // 获取时间差
        accumulator += deltaTime;

        while (accumulator >= FIXED_TIMESTEP) {
            Update(FIXED_TIMESTEP);        // 固定时间步长更新
            accumulator -= FIXED_TIMESTEP;
        }

        Render();                          // 渲染
    }
}

逐行分析:
- deltaTime 表示当前帧与上一帧的时间间隔。
- accumulator 累计时间,当达到固定步长时执行一次更新。
- 保证逻辑更新频率恒定,避免因帧率变化导致的不一致。

通过以上章节的详细分析与代码实现,我们构建了一个结构清晰、逻辑严谨、可扩展性强的游戏核心逻辑系统。该系统不仅适用于当前的方块类游戏,也为后续的扩展(如网络对战、关卡系统等)打下了坚实的基础。

6. 方块生成与随机机制

在经典俄罗斯方块游戏中,方块的生成机制是游戏体验的核心之一。本章将围绕方块的类型设计、随机生成策略以及初始化位置设置展开详细分析。通过本章内容,开发者可以理解如何在Visual C++与DirectX环境下实现一个高效、公平且可扩展的方块生成系统。

6.1 方块类型与形状设计

在俄罗斯方块中,方块通常由四个方格组成,称为Tetromino。共有七种经典的Tetromino形状,每种形状具有不同的排列方式和旋转状态。为了在游戏中高效地表示这些方块,我们需要设计合适的数据结构来描述它们的形状和旋转状态。

6.1.1 七种经典Tetromino形状定义

经典的Tetromino形状如下表所示:

名称 形状(默认方向) 描述
I █ █ █ █ 直线型,可旋转为竖直或水平方向
O ██
██
正方形,无旋转变化
T █ █ █
T字型,4种旋转状态
S ██
██
S型,镜像为Z型
Z ██
██
Z型,镜像为S型
J
███
L反向,4种旋转状态
L
███
L型,4种旋转状态

这些形状在程序中通常以二维数组表示。例如, I 型可以表示为:

int shapeI[4][4] = {
    {0, 0, 0, 0},
    {1, 1, 1, 1},
    {0, 0, 0, 0},
    {0, 0, 0, 0}
};

每个元素为1表示该位置属于方块的一部分,0表示空白。通过这种方式,我们可以将每个Tetromino的形状以矩阵形式存储。

6.1.2 形状矩阵与旋转状态表示

为了实现方块的旋转功能,我们需要为每种形状定义其在不同旋转状态下的矩阵。例如, T 型在四个旋转状态下的矩阵如下:

// T型四种旋转状态
int shapeT_rot0[4][4] = {
    {0, 1, 0},
    {1, 1, 1},
    {0, 0, 0}
};

int shapeT_rot1[4][4] = {
    {0, 1, 0},
    {0, 1, 1},
    {0, 1, 0}
};

int shapeT_rot2[4][4] = {
    {0, 0, 0},
    {1, 1, 1},
    {0, 1, 0}
};

int shapeT_rot3[4][4] = {
    {0, 1, 0},
    {1, 1, 0},
    {0, 1, 0}
};

这些旋转状态可以通过数组索引进行切换,实现方块的旋转逻辑。我们可以通过一个结构体来封装这些信息:

struct Tetromino {
    std::vector<std::vector<std::vector<int>>> rotations; // 所有旋转状态
    int currentRotation; // 当前旋转状态索引

    Tetromino() : currentRotation(0) {}
};

这样,每种Tetromino都可以通过其旋转状态矩阵进行操作,实现旋转、碰撞检测等功能。

6.2 随机生成策略

为了让游戏具有随机性和挑战性,每次生成的方块类型应该是随机的。然而,单纯的随机生成可能导致玩家连续遇到相同类型的方块,影响游戏体验。因此,我们需要设计一个既能保证随机性又具有一定公平性的生成策略。

6.2.1 随机数生成与分布控制

在C++中,我们可以使用 <random> 头文件提供的伪随机数生成器来实现随机选择。以下是一个示例代码,用于生成0到6之间的整数,对应七种Tetromino:

#include <random>

int getRandomTetrominoIndex() {
    static std::random_device rd;
    static std::mt19937 gen(rd());
    static std::uniform_int_distribution<> distrib(0, 6);
    return distrib(gen);
}
  • std::random_device :用于获取硬件熵,作为随机数种子。
  • std::mt19937 :梅森旋转算法,提供高质量的伪随机数生成。
  • std::uniform_int_distribution<> :确保生成的整数在指定范围内均匀分布。

该函数返回0~6之间的整数,分别对应七种Tetromino类型。

6.2.2 避免重复与公平性设计

为了提升玩家体验,我们可以在生成机制中加入“队列”或“袋”机制(Bag System),即从一个包含七种方块的“袋子”中随机抽取,抽完后重新放入袋子。这种方式可以避免连续出现相同类型的方块,同时保持整体的随机性。

以下是袋机制的实现示例:

class TetrominoBag {
private:
    std::vector<int> bag;
    std::mt19937 rng;

public:
    TetrominoBag() {
        std::random_device rd;
        rng = std::mt19937(rd());
        refillBag();
    }

    int getNext() {
        if (bag.empty()) {
            refillBag();
        }
        std::uniform_int_distribution<> distrib(0, bag.size() - 1);
        int index = distrib(rng);
        int nextType = bag[index];
        bag.erase(bag.begin() + index);
        return nextType;
    }

private:
    void refillBag() {
        bag = {0, 1, 2, 3, 4, 5, 6}; // 七种方块
        std::shuffle(bag.begin(), bag.end(), rng); // 洗牌
    }
};
  • bag :当前可用的方块类型队列。
  • getNext() :从袋子中随机取出一个方块类型。
  • refillBag() :当袋子为空时重新填充并洗牌。

这种方式确保每个方块类型在每七次生成中至少出现一次,避免了连续重复,提高了游戏的公平性和可玩性。

6.3 方块初始化与位置设置

在生成方块后,我们需要将其放置在游戏网格的合适位置,并设置初始旋转状态。位置的计算和旋转状态的处理是方块初始化过程中的关键步骤。

6.3.1 生成位置计算

游戏网格通常是一个二维数组,表示游戏区域的每个单元格。新生成的方块应位于网格的顶部中央位置。例如,假设游戏网格为10列宽,方块宽度为3列,则初始X坐标应为:

int startX = (gridWidth / 2) - (blockWidth / 2);
int startY = 0;

其中:

  • gridWidth :游戏网格的列数(如10)。
  • blockWidth :当前方块的宽度(根据形状矩阵计算)。

实际代码中,可以通过遍历形状矩阵获取方块的实际宽度和高度:

void calculateBlockSize(const std::vector<std::vector<int>>& shape, int& width, int& height) {
    height = shape.size();
    if (height == 0) return;
    width = shape[0].size();
}

6.3.2 初始旋转状态的处理

方块生成时的初始旋转状态可以随机设置,也可以固定为0。为了增加多样性,可以使用随机数选择初始旋转状态:

int getInitialRotationState() {
    std::uniform_int_distribution<> distrib(0, 3);
    return distrib(rng);
}

然后在方块初始化时设置旋转状态:

Tetromino currentBlock;
currentBlock.currentRotation = getInitialRotationState();

通过上述逻辑,我们可以实现一个完整的方块初始化流程。以下是一个完整的初始化函数示例:

void spawnNewBlock() {
    // 获取下一个方块类型
    int blockType = tetrominoBag.getNext();

    // 获取对应形状的旋转状态矩阵
    currentShape = tetrominoShapes[blockType];

    // 计算初始位置
    int blockWidth, blockHeight;
    calculateBlockSize(currentShape.rotations[0], blockWidth, blockHeight);

    int startX = (GRID_WIDTH / 2) - (blockWidth / 2);
    int startY = 0;

    // 设置方块初始坐标
    blockX = startX;
    blockY = startY;

    // 设置初始旋转状态
    currentRotation = getInitialRotationState();
}

通过本章内容,我们详细介绍了方块的类型设计、随机生成策略以及初始化位置设置。这些机制共同构成了俄罗斯方块游戏的核心生成逻辑。在下一章中,我们将深入探讨方块的运动控制与碰撞检测机制,进一步完善游戏逻辑。

7. 方块运动控制与碰撞检测

在俄罗斯方块类游戏中,方块的运动控制与碰撞检测是整个游戏逻辑的核心之一。本章将详细讲解如何实现方块的移动、旋转控制,并介绍碰撞检测的算法设计与实现。我们将通过具体代码与逻辑分析,展示如何在DirectX框架下完成这些功能。

7.1 方块的移动与旋转控制

7.1.1 水平移动与下落速度控制

方块在游戏中的基本移动包括左移、右移和下落。下落又分为“自动下落”和“软降”(Soft Drop),后者允许玩家主动加速下落。

示例代码:方块移动处理逻辑
// 定义方向枚举
enum Direction { LEFT, RIGHT, DOWN };

void MoveBlock(Direction dir) {
    int dx = 0, dy = 0;
    switch(dir) {
        case LEFT:  dx = -1; break;
        case RIGHT: dx = +1; break;
        case DOWN:  dy = +1; break;
    }

    // 判断是否可以移动
    if(CanMove(currentBlock, dx, dy)) {
        currentBlock->Move(dx, dy);
    }
}

参数说明:
- dx , dy :表示X轴和Y轴方向的偏移量。
- CanMove() :用于判断是否会发生碰撞。
- currentBlock :当前操作的方块对象。

7.1.2 旋转操作与碰撞预判

方块旋转是游戏体验中不可或缺的一部分。由于旋转可能导致形状变化,因此需要在旋转前进行预判,确保不会与边界或其他方块发生碰撞。

示例代码:旋转逻辑与预判
void RotateBlock() {
    Block* temp = currentBlock->Clone(); // 创建临时副本
    temp->Rotate(); // 尝试旋转

    if(CanPlace(temp)) { // 判断旋转后是否合法
        delete currentBlock;
        currentBlock = temp;
    } else {
        delete temp;
    }
}

逻辑说明:
- 先克隆当前方块,尝试旋转。
- 使用 CanPlace() 检测旋转后的新位置是否合法。
- 若合法则替换当前方块,否则释放临时对象。

7.2 碰撞检测算法实现

7.2.1 网格边界检测

游戏网格是一个二维数组,每个格子表示一个位置是否被方块占据。检测方块是否与边界或已固定方块发生碰撞,需遍历其所有格子坐标。

网格数据结构定义
#define GRID_WIDTH 10
#define GRID_HEIGHT 20
int grid[GRID_HEIGHT][GRID_WIDTH] = {0}; // 0表示空,1表示占据

7.2.2 实时碰撞判定逻辑

以下函数用于判断一个方块是否可以放置在当前位置。

示例代码:碰撞检测函数
bool CanPlace(Block* block) {
    for(int y = 0; y < block->height; ++y) {
        for(int x = 0; x < block->width; ++x) {
            if(block->shape[y][x]) {
                int newX = block->x + x;
                int newY = block->y + y;

                if(newX < 0 || newX >= GRID_WIDTH || newY >= GRID_HEIGHT)
                    return false;

                if(grid[newY][newX])
                    return false;
            }
        }
    }
    return true;
}

逻辑说明:
- 遍历方块矩阵中的每个非零点。
- 检查该点是否超出边界或与已固定方块重叠。
- 任一条件成立即返回 false 表示碰撞。

7.3 运动状态的更新与处理

7.3.1 固定下落与软降机制

方块的下落分为两种模式:
- 固定下落 :每过一定时间自动下落一格。
- 软降 :玩家按下向下键时加速下落。

示例代码:定时下落逻辑(使用Windows定时器)
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    switch(message) {
        case WM_TIMER:
            if(wParam == IDT_GAMELOOP) {
                MoveBlock(DOWN); // 自动下落
            }
            break;
        case WM_KEYDOWN:
            switch(wParam) {
                case VK_DOWN: // 软降
                    MoveBlock(DOWN);
                    break;
                case VK_LEFT:
                    MoveBlock(LEFT);
                    break;
                case VK_RIGHT:
                    MoveBlock(RIGHT);
                    break;
                case VK_UP:
                    RotateBlock();
                    break;
            }
            break;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

逻辑说明:
- 使用 WM_TIMER 定时触发下落。
- 使用 WM_KEYDOWN 响应方向键与旋转操作。

7.3.2 状态更新与网格同步

当方块无法继续下落时,将其状态“固定”到网格中,并生成新的方块。

示例代码:方块落地后的处理
void FixBlockToGrid() {
    for(int y = 0; y < currentBlock->height; ++y) {
        for(int x = 0; x < currentBlock->width; ++x) {
            if(currentBlock->shape[y][x]) {
                int gridX = currentBlock->x + x;
                int gridY = currentBlock->y + y;
                grid[gridY][gridX] = 1; // 标记为已占据
            }
        }
    }
    delete currentBlock;
    currentBlock = GenerateNewBlock(); // 生成新方块
}

逻辑说明:
- 将方块覆盖的网格位置标记为已填充。
- 删除当前方块并生成新方块。

后续章节将继续探讨游戏结束判定、行消除机制与得分系统,敬请期待。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文详细解析了一个使用Visual C++和DirectX开发的轻量级俄罗斯方块游戏项目,全面讲解了游戏逻辑设计、图形渲染与用户交互的实现方式。项目涵盖方块生成、运动控制、碰撞检测、行消除及游戏结束判断等核心机制,并提供完整源码,适合初学者深入理解Windows平台下游戏开发的核心技术与流程。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐