C++入门系列博客七 俄罗斯方块小游戏制作


作者:AceTan,转载请标明出处!


俄罗斯方块游戏可谓童年经典,遥想当年拿着那种掌机,玩一下午的俄罗斯方块,是多么惬意和悠闲的事情啊,满满地都是回忆啊(等等,是不是无意之间暴露了什么……)。今天带大家来实现这款小游戏,也是对前面博客所讲内容的一个综合实践。 效果图如下:

俄罗斯方块小游戏

0x00 游戏开发##

先扯一些没用的,一款游戏一般由游戏策划、游戏程序员和美术人员来共同完成。游戏开发的主流语言还是C/C++。你平时可以写一些小游戏来提高你的C/C++的水平。现代大型游戏多在游戏引擎基础之上开发,目前主流的游戏引擎有U3D、UE4和CE3等等。各大游戏引擎的优缺点我们也不做讨论,唯一的共同点就是他们都非常复杂。游戏引擎开发是一项极具挑战的工作,牛叉的游戏引擎一般由团队共同合作完成(国产电视剧《微微一笑很倾城》 男主角貌似自己开发了一款游戏引擎,呵呵)。关于游戏引擎的更多知识可以读一下这本书—《游戏引擎构架》。嗯,你没猜错,笔者就是国内某游戏公司的程序猿。

回归正题,这款简单的俄罗斯方块游戏肯定不基于任何游戏引擎啦,甚至它不使用任何渲染库。我们来写一个控制台版的小游戏。


0x01 如何下手##

前面提到过,一款游戏的制作一般由游戏策划、游戏程序员和美术来共同完成。其中,游戏策划一般负责游戏的玩法、规则、界面、数值等设计,美术人员负责模型、动画、原画,插图和游戏整体风格的把握等。游戏程序员负责实现游戏策划所提的需求。那么,这款小游戏我们也可以从这几个方面入手:

  • 游戏的规则是什么?

  • 游戏的界面应该是什么样子的,计分面板、说明面板放在哪里?

  • 如何控制游戏(按键控制)?

  • 游戏的整体风格应该是什么样子?

上面的这几个问题解决了,就可以交给程序员去搞了。


0x02 程序设计##

接到策划的需求后,程序如何设计呢?通过需求分析,仔细查看策划人员给的设计图(例如上面的效果图),你可以很容易得出以下结论:这是一个在Windows平台下跑的一个控制台游戏。显然,你可能需要设计一个Console类和Window类(其中Console类是Window类的成员)。这两个类应该具有如下的能力:

  • 控制窗口的标题,窗口大小,缓冲区大小,光标等

  • 完全的控制输出的能力,包括但不限于文字的位置,颜色,前景色和背景色。

有了这些信息,你就可以Google和百度一下相关的API了,看哪些是已经有的,哪些需要自己设计的。比如,你就可以查到以下的一些函数:

  • GetStdHandle() // 获得句柄

  • SetConsoleCursorInfo() // 设置光标信息

  • SetConsoleWindowInfo() // 设置窗口信息

  • SetConsoleScreenBufferSize() //设置窗口缓冲区大小

  • SetConsoleTitle() //设置标题

  • WriteConsoleOutputCharacter() 和 WriteConsoleOutputAttribute() //控制输出的函数

其中,WriteConsoleOutputCharacter()和WriteConsoleOutputAttribute()函数你也许并不熟悉,这就需要你查看相关文档,弄懂这两个函数了,因为这两个函数至关重要,承担了游戏的打印(渲染)任务。

简单的查一下,很快就能得到该函数的原型和相关参数说明:

// 函数原型:
BOOL WriteConsoleOutputCharacter( // 在指定位置处插入指定数量的字符
HANDLE hConsoleOutput, // 句柄
LPCTSTR lpCharacter, // 字符串
DWORD nLength, // 字符个数
COORD dwWriteCoord, // 起始位置
LPDWORD lpNumberOfCharsWritten // 已写个数
);

/* 参数简介:
hConsoleOutput:控制台输出句柄,通过调用GetStdHandle函数获得
HANDLE hnd;
hnd=GetStdHandle(STD_INPUT_HANDLE);
lpCharacter:要输出的字符串
nLength:输出长度
dwWriteCoord:起始位置
pNumberOfCharsWritten:已写个数,通常置为NULL
其中,COORD是个结构体变量类型*/
typedef struct _COORD 
{
    SHORT X;
    SHORT Y;
} COORD;

上面这个是来自百度百科。其实更权威的说明应该查询MSDN,例如WriteConsoleOutputAttribute的传送门。 MSDN上对这个函数讲解的非常详细,也非常权威,前提是你有阅读英文文献的能力。还有就是在VS里打出这个函数名,然后按F12直接查看这个函数的原型,根据参数的命名,大概了解一下这个函数。

设计这个小游戏剩下的就是游戏的逻辑了。我们设计Tetris类来进行游戏的逻辑控制。我们还需要设计一个数据结构来表示方块。单个方块如何表示呢?通过我们队游戏规则的了解和对图形的观察,我们可以使用4*4的矩阵来表示一个方块。例如:


单个方块的表示

我们使用一个四维数组表示所有的方块。 diamonds[x][y][4][4],其中x表示有几种方块,y表示这种方块有几种变形,[4][4]表示这个方块。


0x03 工程结构##

这个小游戏很简单,没有那么多模块。现在列一下这个工程的结构,并做简要说明。其中.h头文件为声明,定义在对应的.cpp文件中。

  • Console 控制台类

  • GameDefine 定义游戏的一些常量。

  • StringUtil 字符串工具类

  • Tetris 俄罗斯方块类

  • Window 窗体类

其中的字符串工具类,最后并没有用到。


0x04 code##

懒得放github上了,直接上代码了。代码注释还是比较详尽的,应该能看得懂。

Talk is Cheap, show you the code.

Console.h文件:

//--------------------------------------------------------------------
// 文件名:        Console.h
// 内  容:        控制台类
// 说  明:        控制台类的一些声明
// 创建日期:        2016年9月6日
//--------------------------------------------------------------------

#pragma once            // 保证该文件只被包含一次

#include <wchar.h>
#include <windows.h>    // 使用windows系统下的东西需要引入的头文件

class Console
{
    friend class Window;

public:

    /// \brief 初始化控制台
    /// \param caption 控制台标题
    /// \param coordinate 控制台的高和宽
    void Init(const wchar_t* caption, COORD coordinate);

public:
    HANDLE m_hStdInput;            // 标准输入句柄

private:
    HANDLE m_hStdOutput;        // 标准输出句柄
    COORD  m_coord;                // 位置信息(x,y)

};

Console.cpp文件

#include "Console.h"

#ifndef INVALID_RETURN_VOID
#define INVALID_RETURN_VOID(condition) if((condition)) {return;}
#endif

// 一些常量的定义
const DWORD CURSOR_SIZE = 25;
const SHORT SMALL_RECT_TOP = 0;
const SHORT SMALL_RECT_LEFT = 0;


/// \brief 打开控制台
/// \param caption 控制台标题
/// \param coordinate 控制台的高和宽
void Console::Init(const wchar_t* caption, COORD coordinate)
{
    // 如果所给坐标不合法,则直接退出
    INVALID_RETURN_VOID(coordinate.X <= 0 || coordinate.Y <= 0);

    // 获得输出句柄
    m_hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    m_hStdInput = GetStdHandle(STD_INPUT_HANDLE);
    // 判断得到的句柄是否合法
    INVALID_RETURN_VOID(INVALID_HANDLE_VALUE == m_hStdOutput);
    INVALID_RETURN_VOID(INVALID_HANDLE_VALUE == m_hStdInput);
    
    // 去除光标
    CONSOLE_CURSOR_INFO cci = { CURSOR_SIZE, false };
    SetConsoleCursorInfo(m_hStdOutput, &cci);
    
    // 设置窗体大小
    SMALL_RECT sr = { SMALL_RECT_TOP, SMALL_RECT_LEFT, coordinate.X - 1, coordinate.Y - 1 };
    SetConsoleWindowInfo(m_hStdOutput, true, &sr);
    
    // 设置缓冲区大小
    m_coord = coordinate;
    SetConsoleScreenBufferSize(m_hStdOutput, m_coord);

    // 设置窗口标题
    SetConsoleTitle(caption);
}

Window.h文件

//--------------------------------------------------------------------
// 文件名:        Window.h
// 内  容:        窗体类
// 说  明:        它是控制台的一个子部分
// 创建日期:        2016年9月6日
//--------------------------------------------------------------------

#pragma once
#include "Console.h"

class Window
{
public:
    /// \brief 初始化窗口
    /// \param console 控制台引用
    /// \param rect 位置信息
    void Init(Console& console, SMALL_RECT rect);

    /// \brief 输出信息
    /// \param str 要输出的字符串
    /// \param coordinate 位置信息 x, y
    /// \param color 颜色
    /// \param len 字符串长度
    void Output(const char* str, COORD coordinate, WORD color, size_t len = INT_MAX);

private:
    Console* m_pConsole;
    SMALL_RECT m_rect;
};

Window.cpp文件

#include <Windows.h>
#include "Window.h"
#include "StringUtil.h"

#ifndef INVALID_RETURN_VOID
#define INVALID_RETURN_VOID(condition) if((condition)) {return;}
#endif

// 一些常量的定义
const DWORD CURSOR_SIZE = 25;
const SHORT SMALL_RECT_TOP = 0;
const SHORT SMALL_RECT_LEFT = 0;

/// \brief 初始化窗口
/// \param console 控制台引用
/// \param rect 位置信息
void Window::Init(Console& console, SMALL_RECT rect)
{
    // 检测位置信息是否合法
    INVALID_RETURN_VOID(rect.Left >= rect.Right 
        && rect.Top >= rect.Bottom
        && rect.Left < 0
        && rect.Right > console.m_coord.X
        && rect.Top > console.m_coord.Y);
        
    m_pConsole = &console;
    m_rect = rect;
}

/// \brief 输出信息
/// \param str 要输出的字符串
/// \param coordinate 位置信息 x, y
/// \param color 颜色
/// \param len 字符串长度
void Window::Output(const char* str, COORD coordinate, WORD color, size_t len)
{
    // 先检测位置信息是否合法
    INVALID_RETURN_VOID(coordinate.X < 0
        || coordinate.Y < 0
        || coordinate.X > (m_rect.Right - m_rect.Left)
        || coordinate.Y > (m_rect.Bottom - m_rect.Top));

    COORD coord = {m_rect.Left + coordinate.X, m_rect.Top + coordinate.Y};
    DWORD num = 0;
    WORD colorArray[2] = { color, color };

    // 字符串转换
    for (const char* p = str; len != 0 && *p != 0; --len, ++p, ++coord.X)
    {
        // 需要换行
        if (coord.X >= m_rect.Right)    
        {
            coord.X = m_rect.Left + coordinate.X;
            ++coord.Y;
            INVALID_RETURN_VOID(coord.Y >= m_rect.Bottom);
        }

        // 单字节字符
        if (*p > 0)
        {
            WriteConsoleOutputCharacterA(m_pConsole->m_hStdOutput, p, 1, coord, &num);
            INVALID_RETURN_VOID(num != 1);
            WriteConsoleOutputAttribute(m_pConsole->m_hStdOutput, colorArray, 1, coord, &num);
            INVALID_RETURN_VOID(num != 1);
        }
        // 双字节字符
        else
        {
            INVALID_RETURN_VOID( len < 2 || *(p + 1) == 0 || (coord.X + 1) >= m_rect.Right);
            WriteConsoleOutputCharacterA(m_pConsole->m_hStdOutput, p, 2, coord, &num);
            INVALID_RETURN_VOID(num != 2);
            WriteConsoleOutputAttribute(m_pConsole->m_hStdOutput, colorArray, 2, coord, &num);
            INVALID_RETURN_VOID(num != 2);

            --len;
            ++p; 
            ++coord.X;
        }
    }
}

Tetris.h文件

//--------------------------------------------------------------------
// 文件名:        Tetris.h
// 内  容:        俄罗斯方块类
// 说  明:        
// 创建日期:        2016年9月6日
//--------------------------------------------------------------------

#pragma once
#include "Console.h"
#include "Window.h"
#include "GameDefine.h"

class Tetris
{
public:
    
    /// \brief 构造函数
    /// \param console 控制台
    /// \param coordinate 控制台的高和宽
    Tetris(Console& console, COORD coordinate);

    /// \brief 初始化游戏
    /// \param keys 按键
    /// \param keyDesc 按键描述
    /// \param frequency 声效频率
    /// \param duration 延续时间
    void Init(int keys[KeyNum], char keyDesc[KeyNum][5], DWORD frequency, DWORD duration);

    /// \brief 是否正在运行游戏
    bool IsRun();

    /// \brief 获取当前等级
    int GetLevel() const;

    /// \brief 方块下落
    bool Fall();

    /// \brief 消息处理
    /// \param key 按键
    /// \return 游戏结束返回false
    bool MessageProc(const Cmd cmd);

private:
    /// \brief 声效
    void VoiceBeep();

    /// \brief 绘制得分
    void DrawScoreLevel();

    /// \brief 绘制下一个将要出现的图形
    void DrawNext();

    /// \brief 绘制游戏结束界面
    void DrawGameOver();

    /// \brief 绘制颜色
    void Draw(WORD color);

    /// \brief 给定的是否可行
    bool IsFit(int x, int y, int c, int z);

    /// \brief 消除行
    void RemoveRow();

    /// \brief 旋转(逆时针)
    void MoveTrans();

    /// \brief 向左移动
    void MoveLeft();

    /// \brief 向右移动
    void MoveRight();

    /// \brief 向下移动
    /// \return 0: 游戏结束; -1:触底; 1:没有触底
    int MoveDown();

    /// \brief 下落到底
    bool FallToBottom();

private:
    char bg[GAME_HIGHT * GAME_WIDTH + 1];
    char bk[DIAMONDS_TYPES][DIAMONDS_TRANS][DIAMONDS_IFNO_ROW][DIAMONDS_IFNO_COL];

private:
    // 声效频率
    DWORD m_voiceFrequency;
    
    // 延续时间
    DWORD m_voiceDuration;

    // 控制按键
    int m_keys[KeyNum];

    // 控制按键的描述
    char m_keyDesc[KeyNum][5];

    // 游戏是否结束
    bool m_gameover;

    // 游戏暂停
    bool m_pause;

    // 游戏声效开关
    bool m_voice;

    // 游戏得分
    int m_score;

    // 游戏速度
    int m_speed;

    // 游戏数据(实际方块的存放数据)
    char m_data[ROWS][COLS];

    // 下一个方块
    int m_next;

    // 位置(x, y)
    int m_x, m_y;

    // 当前方块
    int m_currentDiamonds;

    // 当前方向
    int m_currentDir;

    // 窗口
    Window win;

};

Tetris.cpp文件

#include "Tetris.h"
#include <time.h>
#include <stdio.h>

/// \brief 构造函数
/// \param console 控制台
/// \param coordinate 控制台的高和宽
Tetris::Tetris(Console & console, COORD coordinate)
{
    // 创建一个矩形
    SMALL_RECT rect = { coordinate.X, coordinate.Y, coordinate.X + GAME_WIDTH, coordinate.Y + GAME_HIGHT };

    // 初始化这个窗口
    win.Init(console, rect);
    
}

/// \brief 初始化游戏
/// \param keys 按键
/// \param keyDesc 按键描述
/// \param frequency 声效频率
/// \param duration 延续时间
void Tetris::Init(int keys[KeyNum], char keyDesc[KeyNum][5], DWORD frequency, DWORD duration)
{
    // 初始化游戏的数据
    memcpy(m_keys, keys, sizeof(m_keys));
    memcpy(m_keyDesc, keyDesc, sizeof(m_keyDesc));
    memcpy(bk, Diamonds, sizeof(bk));
    memcpy(bg, Background, sizeof(bg));

    m_voiceFrequency = frequency;
    m_voiceDuration = duration;
    m_gameover = false;
    m_pause = true;
    m_voice = true;
    m_score = 0;
    m_speed = 0;

    // 方块数据部分置0
    memset(m_data, 0, sizeof(m_data));

    // 设置随机种子
    srand((unsigned)time(NULL));

    // 下一个方块
    m_next = rand() % DIAMONDS_TYPES;

    m_x = 4;
    m_y = 2;
    m_currentDiamonds = -1;
    m_currentDir = 0;

    COORD coord = { 0, 0 };

    win.Output(bg + 0, coord, COLOR_STILL, GAME_WIDTH);

    for (int i = 1; i < ROWS - 1; ++i)
    {
        coord = { 0, (SHORT)i };
        win.Output(bg + GAME_WIDTH * i + 0, coord, COLOR_STILL, 2);
        coord = { 2, (SHORT)i };
        win.Output(bg + GAME_WIDTH * i + 2, coord, COLOR_BLANK, 22);
        coord = { 24, (SHORT)i };
        win.Output(bg + GAME_WIDTH * i + 24, coord, COLOR_STILL, 14);
    }

    coord = { 0, 20 };
    win.Output(bg + GAME_WIDTH * 20, coord, COLOR_STILL, GAME_WIDTH);

    for (int j = 0; j < KeyNum; ++j)
    {
        coord = { 33, (SHORT)j + 7 };
        win.Output(m_keyDesc[j], coord, COLOR_STILL, 4);
    }

    // 绘制下一个将要出现的方块
    DrawNext();
}

/// \brief 是否正在运行游戏
bool Tetris::IsRun()
{
    return !m_gameover && !m_pause;
}

/// \brief 获取当前等级
int Tetris::GetLevel() const
{
    return m_speed;
}

/// \brief 方块下落
bool Tetris::Fall()
{
    return MessageProc(CMD_DOWN);
}

/// \brief 消息处理
/// \param key 按键
/// \return 游戏结束返回false
bool Tetris::MessageProc(const Cmd cmd)
{
    int const key = m_keys[cmd];
    // 游戏结束
    if (m_gameover)
    {
        // 游戏重新开始
        if (m_keys[GameBegin] == key)
        {
            Init(m_keys, m_keyDesc, m_voiceFrequency, m_voiceDuration);
            return true;
        }

        return false;
    }

    // 游戏暂停
    if (m_pause)
    {
        // 游戏重新开始
        if (m_keys[GameBegin] == key)
        {
            m_pause = false;
            if (m_currentDiamonds == -1)
            {
                m_currentDiamonds = m_next;
                m_next = rand() % DIAMONDS_TYPES;
                DrawNext();
            }
        }
        else if (m_keys[GameVoice] == key)
        {
            m_voice = !m_voice;
        }
        else
        {
            return true;
        }

        VoiceBeep();

        return true;
    }

    if (m_keys[GamePause] == key)        // 按下暂停键
    {
        m_pause = true;
    }
    else if (m_keys[GameVoice] == key)    // 按下声效键
    {
        m_voice = !m_voice;
    }
    else if (m_keys[Up] == key)            // 按下变形键
    {
        MoveTrans();
    }
    else if (m_keys[Left] == key)        // 按下方向左键
    {
        MoveLeft();
    }
    else if (m_keys[Right] == key)        // 按下方向右键
    {
        MoveRight();
    }
    else if (m_keys[Down] == key)        // 按下方向下键
    {
        if (0 == MoveDown())
        {
            return false;            
        }
    }
    else if (m_keys[FallDown] == key)        // 按下方块直接落地键
    {
        if (!FallToBottom())
        {
            return false;
        }
    }
    
    return true;
}

/// \brief 声效
void Tetris::VoiceBeep()
{
    if (m_voice)
    {
        Beep(m_voiceFrequency, m_voiceDuration);
    }
}

/// \brief 绘制得分
void Tetris::DrawScoreLevel()
{
    char tmp[6];
    COORD coord = { 0, 0 };
    sprintf_s(tmp, "%05d", m_score);
    coord = {31, 19};
    win.Output(tmp, coord, COLOR_STILL, 5);
    sprintf_s(tmp, "%1d", m_speed);
    coord = { 28, 19 };
    win.Output(tmp, coord, COLOR_STILL, 1);
}

/// \brief 绘制下一个将要出现的图形
void Tetris::DrawNext()
{
    for (int i = 0; i < 2; ++i)
    {
        for (int j = 0; j < 4; ++j)
        {
            COORD coord = {28 + (SHORT)j * 2, 1 + (SHORT)i};
            char* tmp = bk[m_next][0][i][j] == 0 ? " " : "■";
            win.Output(tmp, coord, COLOR_STILL, 2);
        }
    }
}

/// \brief 绘制游戏结束界面
void Tetris::DrawGameOver()
{
    COORD coord = { 28, 1 };
    win.Output("游戏结束", coord, COLOR_STILL);
    coord = { 28, 2 };
    win.Output(" ", coord, COLOR_STILL);
}

/// \brief 绘制颜色
void Tetris::Draw(WORD color)
{
    COORD coord = { 0, 0 };

    for (int i = 0; i < 4; ++i)
    {
        if (m_y + i < 0 || m_y + i >= ROWS - 2)
        {
            continue;
        }
        
        for (int j = 0; j < 4; ++j)
        {
            if (bk[m_currentDiamonds][m_currentDir][i][j] == 1)
            {
                coord = { SHORT(2 + m_x * 2 + j * 2), SHORT(1 + m_y + i) };
                win.Output("■", coord, color, 2);
            }
        }
    }
}

/// \brief 给定的是否可行
bool Tetris::IsFit(int x, int y, int c, int z)
{
    for (int i = 0; i < 4; ++i)
    {
        for (int j = 0; j < 4; ++j)
        {
            if (bk[c][z][i][j] == 1)
            {
                if (y + i < 0)
                {
                    continue;
                }
                if (y + i >= (ROWS - 2) || x + j < 0 || x + j >= (COLS - 2) || m_data[y + i][x + j] == 1)
                {
                    return false;
                }
            }
        }
    }

    return true;
}

/// \brief 消除行
void Tetris::RemoveRow()
{
    int lineCount = 0;
    COORD coord = { 0, 0 };
    for (int i = 0; i < (ROWS - 2); ++i)
    {
        if (0 == memcmp(m_data[i], FULL_LINE, (COLS - 2)))
        {
            ++lineCount;
            for (int m = 0; m < (COLS - 2); ++m)
            {
                for (int n = i; n > 1; --n)
                {
                    m_data[n][m] = m_data[n - 1][m];
                    coord = {SHORT(2 + m * 2), SHORT(1 + n)};
                    WORD color = m_data[n][m] == 1 ? COLOR_STILL : COLOR_BLANK;
                    win.Output("■", coord, color, 2);
                }

                m_data[0][m] = 0;
                coord = { SHORT(2 + m * 2) , 1};
                win.Output("■", coord, COLOR_BLANK, 2);
            }
        }
    }

    char data[ROWS - 2][COLS - 2] = { 0 };
    if (lineCount == 0)
    {
        return;
    }

    int score = 0;
    switch (lineCount)
    {
    case 1:
        score = ONE_ROW_SCORE;
        break;
    case 2:
        score = TWO_ROWS_SCORE;
        break;
    case 3:
        score = THREE_ROWS_SCORE;
        break;
    case 4:
        score = FOUR_ROWS_SCORE;
        break;
    }

    m_score += score;

    if (score > MAX_SCORE)
    {
        score = MAX_SCORE;
    }

    m_speed = score / SPEED_ADD_SCORE;

    DrawScoreLevel();
}

/// \brief 旋转(逆时针)
void Tetris::MoveTrans()
{
    if (IsFit(m_x, m_y, m_currentDiamonds, (m_currentDir + 1) % 4))
    {
        VoiceBeep();
        Draw(COLOR_BLANK);

        m_currentDir = (m_currentDir + 1) % 4;
        Draw(COLOR_MOVE);
    }
}

/// \brief 向左移动
void Tetris::MoveLeft()
{
    if (IsFit(m_x - 1, m_y, m_currentDiamonds, m_currentDir))
    {
        VoiceBeep();
        Draw(COLOR_BLANK);

        --m_x;
        Draw(COLOR_MOVE);
    }
}

/// \brief 向右移动
void Tetris::MoveRight()
{
    if (IsFit(m_x + 1, m_y, m_currentDiamonds, m_currentDir))
    {
        VoiceBeep();
        Draw(COLOR_BLANK);

        ++m_x;
        Draw(COLOR_MOVE);
    }
}

/// \brief 向下移动
/// \return 0: 游戏结束; -1:触底; 1:没有触底
int Tetris::MoveDown()
{
    if (IsFit(m_x, m_y + 1, m_currentDiamonds, m_currentDir))
    {
        VoiceBeep();
        Draw(COLOR_BLANK);

        ++m_y;
        Draw(COLOR_MOVE);

        return 1;
    }

    // 触底了
    if (m_y != -2)
    {
        Draw(COLOR_STILL);
        for (int i = 0; i < 4; ++i)
        {
            if (m_y + i < 0)
            {
                continue;
            }

            for (int j = 0; j < 4; ++j)
            {
                if (bk[m_currentDiamonds][m_currentDir][i][j] == 1)
                {
                    m_data[m_y + i][m_x + j] = 1;
                }
            }

        }
        RemoveRow();

        m_x = 4;
        m_y = -2;
        m_currentDir = 0;
        m_currentDiamonds = m_next;

        m_next = rand() % DIAMONDS_TYPES;
        DrawNext();

        return -1;
    }

    // 游戏结束

    m_gameover = true;
    DrawGameOver();

    return 0;
}

/// \brief 下落到底
bool Tetris::FallToBottom()
{
    int r = MoveDown();
    while (r == 1)
    {
        r = MoveDown();
    }

    return r == -1;
}

StringUtil.h文件

//--------------------------------------------------------------------
// 文件名:        StringUtil.h
// 内  容:        字符串工具类
// 说  明:        提供字符串操作的一些便捷工具类
// 创建日期:        2016年9月6日
// 创建人:        AceTan
// 版权所有:        AceTan
//--------------------------------------------------------------------

#pragma once

#include <string>
#include <wchar.h>
#include <Windows.h>

// 字符串处理
class StringUtil
{
public:
    // 字符串转换成宽字符串
    static const wchar_t* StringToWideStr(const char* info, wchar_t* buf,
        size_t size, long codepage = CP_UTF8);

    // 宽字符串转换成字符串
    static const char* WideStrToString(const wchar_t* info, char* buf,
        size_t size, long codepage = CP_UTF8);
};

StringUtil.cpp文件

#include "StringUtil.h"
#include <windows.h>

// 字符串转换到宽字符串
const wchar_t* StringUtil::StringToWideStr(const char* info, wchar_t* buf,
    size_t size, long codepage)
{
    if (NULL == info || NULL == buf || size < sizeof(wchar_t))
    {
        return L"";
    }

    const size_t len = size / sizeof(wchar_t);

    int res = MultiByteToWideChar(codepage, 0, info, -1, buf, int(len));

    if (res == 0)
    {
        if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
        {
            buf[len - 1] = 0;
        }
        else
        {
            buf[0] = 0;
        }
    }

    return buf;
}

// 宽字符串转换成字符串
const char* StringUtil::WideStrToString(const wchar_t* info, char* buf,
    size_t size, long codepage)
{
    if (NULL == info || NULL == buf || size < sizeof(char))
    {
        return "";
    }

    int res = WideCharToMultiByte(codepage, 0, info, -1, buf, int(size),
        NULL, NULL);

    if (0 == res)
    {
        if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
        {
            buf[size - 1] = 0;
        }
        else
        {
            buf[0] = 0;
        }
    }

    return buf;
}

GameDefine.h文件

//--------------------------------------------------------------------
// 文件名:        GameDefine.h
// 内  容:        游戏定义文件
// 说  明:        定义游戏的一些常量,比如窗口大小等
// 创建日期:        2016年9月6日
//--------------------------------------------------------------------

#pragma once
#include <windows.h>

// 高度
const SHORT GAME_HIGHT = 21;

// 宽度
const SHORT GAME_WIDTH = 38;

// 方块的行数
const SHORT ROWS = 21;

// 方块的列数
const SHORT COLS = 13;

// 运动中的颜色
const WORD COLOR_MOVE = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY;

// 固定不动的颜色
const WORD COLOR_STILL = FOREGROUND_GREEN;

// 空白处的颜色
const WORD COLOR_BLANK = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;

// 方块种类
const unsigned int DIAMONDS_TYPES = 7;

// 每个方块有几种变形
const unsigned int DIAMONDS_TRANS = 4;

// 表示单个方块的行数
const unsigned int DIAMONDS_IFNO_ROW = 4;

// 表示单个方块的列数
const unsigned int DIAMONDS_IFNO_COL = 4;

// 消除1行的得分
const int ONE_ROW_SCORE = 100;

// 消除2行的得分
const int TWO_ROWS_SCORE = 300;

// 消除3行的得分
const int THREE_ROWS_SCORE = 700;

// 消除4行的得分
const int FOUR_ROWS_SCORE = 1500;

// 最大分值
const int MAX_SCORE = 99999;

// 得分满,加一个速度
const int SPEED_ADD_SCORE = 10000;

// 默认声效频率
const DWORD DEFAULT_FREQUENCY = 1760;

// 默认声效延续时间
const DWORD DEFAULT_DURATION = 20;

// 超时下落
const DWORD TIME_OUT = 1000;

// 休眠间隔时间(毫秒)
const int SLEEP_TIME = 200;

// 游戏按键对应的索引
enum KeyIndex
{
    GameBegin = 0,    // 游戏开始
    GamePause,        // 游戏暂停
    GameVoice,        // 游戏声效
    Up,                // 方向键-上
    Left,            // 方向键-左
    Right,            // 方向键-右
    Down,            // 方向键-下
    FallDown,        // 方块直接落地

    KeyNum,            // 按键总数
};


// 对应的键值(这个需要查表或者自己实验所得)
enum KeyMap
{
    KEY_ENTER = 13,
    KEY_F1 = 59,
    KEY_F2 = 60,
    KEY_UP = 72,
    KEY_LEFT = 75,
    KEY_RIGHT = 77,
    KEY_DOWN = 80,
    KEY_SPACE = 32,
    KEY_ESC = 27,
};

// 游戏操作定义
enum Cmd
{
    CMD_BEGIN,        // 游戏开始
    CMD_PAUSE,        // 游戏暂停
    CMD_VOICE,        // 游戏声效
    CMD_ROTATE,        // 方块变形
    CMD_LEFT,        // 方块左移
    CMD_RIGHT,        // 方块右移
    CMD_DOWN,        // 方块下移
    CMD_SINK,        // 方块沉底
    CMD_QUIT,        // 游戏退出
};


// 某一行满了
const char FULL_LINE[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };

// 方块用一个4维数组表示:共7种不同方块,4种变形。每个方块用 4*4 表示。
const char Diamonds[DIAMONDS_TYPES][DIAMONDS_TRANS][DIAMONDS_IFNO_ROW][DIAMONDS_IFNO_COL] =
{
{
{ { 0,1,1,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,1,0,0 },{ 0,1,0,0 },{ 0,0,0,0 } },
{ { 0,1,1,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,1,0,0 },{ 0,1,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 1,1,0,0 },{ 0,1,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 0,1,0,0 },{ 1,1,0,0 },{ 1,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 0,1,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 0,1,0,0 },{ 1,1,0,0 },{ 1,0,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 1,1,1,0 },{ 1,0,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,0,0,0 },{ 1,1,0,0 },{ 0,0,0,0 } },
{ { 0,0,1,0 },{ 1,1,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 0,1,0,0 },{ 0,1,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 1,1,1,0 },{ 0,0,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 1,0,0,0 },{ 1,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,1,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 0,1,0,0 },{ 0,1,0,0 },{ 1,1,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 1,1,0,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,1,0,0 },{ 1,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 0,1,0,0 },{ 1,1,1,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 0,1,0,0 },{ 1,1,0,0 },{ 0,1,0,0 },{ 0,0,0,0 } },
{ { 1,1,1,0 },{ 0,1,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,1,0,0 },{ 1,0,0,0 },{ 0,0,0,0 } }
}
,
{
{ { 1,1,1,1 },{ 0,0,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,0,0,0 },{ 1,0,0,0 },{ 1,0,0,0 } },
{ { 1,1,1,1 },{ 0,0,0,0 },{ 0,0,0,0 },{ 0,0,0,0 } },
{ { 1,0,0,0 },{ 1,0,0,0 },{ 1,0,0,0 },{ 1,0,0,0 } }
}
};

// 游戏背景
const char Background[GAME_HIGHT * GAME_WIDTH + 1] =
    "┏━━━━━━━━━━━┓┏━━━━┓"
    "┃■■■■■■■■■■■┃┃┃"
    "┃■■■■■■■■■■■┃┃┃"
    "┃■■■■■■■■■■■┃┗━━━━┛"
    "┃■■■■■■■■■■■┃"
    "┃■■■■■■■■■■■┃ 退出= ESC  "
    "┃■■■■■■■■■■■┃"
    "┃■■■■■■■■■■■┃ 开始=  "
    "┃■■■■■■■■■■■┃ 暂停=  "
    "┃■■■■■■■■■■■┃ 声效=  "
    "┃■■■■■■■■■■■┃ 变形=  "
    "┃■■■■■■■■■■■┃ 左移=  "
    "┃■■■■■■■■■■■┃ 右移=  "
    "┃■■■■■■■■■■■┃ 下移=  "
    "┃■■■■■■■■■■■┃ 落地=  "
    "┃■■■■■■■■■■■┃"
    "┃■■■■■■■■■■■┃"
    "┃■■■■■■■■■■■┃ 速度  得分 "
    "┃■■■■■■■■■■■┃┏━━━━┓"
    "┃■■■■■■■■■■■┃┃0  00000┃"
    "┗━━━━━━━━━━━┛┗━━━━┛";


// 游戏开始时的X坐标
const unsigned int GameStartX = 38;

// 游戏开始时的Y坐标
const unsigned int GameStartY = 21;

main.cpp文件

#include "Console.h"
#include "Window.h"
#include "GameDefine.h"
#include "Tetris.h"
#include <WinUser.h>
#include <conio.h>

DWORD oldTime = 0;

// 得到按键命令
Cmd GetCmd(Tetris& tetris, Console& console)
{
    while (true)
    {
        // 延时,减少CPU占用率
        Sleep(SLEEP_TIME);

        DWORD newTime = GetTickCount();

        // 超时下落
        if (newTime - oldTime > TIME_OUT)
        {
            oldTime = newTime;
            return CMD_DOWN;
        }

        // 有按键
        if (_kbhit())
        {
            switch (_getch())
            {
            case KEY_ENTER:
                return CMD_BEGIN;
            case KEY_SPACE:
                return CMD_SINK;
            case KEY_ESC:
                return CMD_QUIT;

            case 0:
            case 0xE0:        
                switch (_getch())
                {
                case KEY_F1:
                    return CMD_PAUSE;
                case KEY_F2:
                    return CMD_VOICE;
                case KEY_UP:
                    return CMD_ROTATE;
                case KEY_LEFT:
                    return CMD_LEFT;
                case KEY_RIGHT:
                    return CMD_RIGHT;
                case KEY_DOWN:
                    return CMD_DOWN;
                }

            }
        }

        if (tetris.IsRun() && tetris.GetLevel() <= 10)
        {
            return CMD_DOWN;
        }
    }
}

// 分发按键命令处理
void DispatchCmd(Tetris& tetris, Console& console, Cmd cmd)
{
    switch (cmd)
    {
    case CMD_QUIT:
        exit(0);
        break;
    default:
        tetris.MessageProc(cmd);
        break;

    }
}

int main()
{
    // 创建一个控制台
    Console console;

    // 创建一个坐标
    COORD coordinate = {GameStartX, GameStartY};

    const wchar_t* strGameName = L"俄罗斯方块 ---- By AceTan ";
    console.Init(strGameName, coordinate);

    int keys[KeyNum] = {KEY_ENTER, KEY_F1, KEY_F2, KEY_UP, KEY_LEFT, KEY_RIGHT, KEY_DOWN, KEY_SPACE };
    char decs[KeyNum][5] = { "回车", "F1", "F2", "↑", "←", "→", "↓", "空格"};

    COORD coord = { 0, 0 };
    Tetris tetris(console, coord);

    tetris.Init(keys, decs, DEFAULT_FREQUENCY, DEFAULT_DURATION);

    Cmd cmd;
    while (true)
    {
        cmd = GetCmd(tetris, console);
        DispatchCmd(tetris, console, cmd);
    }

    return 0;
}

0x05 结束语##

以上文件在VS2015下编译通过,并且可以运行,其他版本没有试过。另外,这个小游戏参考了我很久之前写的代码,现在进行了代码重构和调整。记得之前写的时候是参考了网上的设计,无奈找不到源出处了,侵删。

这里面用到的知识都是我在之前的博客里讲到的,如果读者感觉有疑惑或者困难,请移步去看一下前面的博客内容。如果你完全看不懂这写的啥,那么我建议你多敲代码多看书。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容