作者: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下编译通过,并且可以运行,其他版本没有试过。另外,这个小游戏参考了我很久之前写的代码,现在进行了代码重构和调整。记得之前写的时候是参考了网上的设计,无奈找不到源出处了,侵删。
这里面用到的知识都是我在之前的博客里讲到的,如果读者感觉有疑惑或者困难,请移步去看一下前面的博客内容。如果你完全看不懂这写的啥,那么我建议你多敲代码多看书。