作者:AceTan,轉(zhuǎn)載請(qǐng)標(biāo)明出處形入!
俄羅斯方塊游戲可謂童年經(jīng)典浓若,遙想當(dāng)年拿著那種掌機(jī)挪钓,玩一下午的俄羅斯方塊耳舅,是多么愜意和悠閑的事情啊碌上,滿滿地都是回憶啊(等等浦徊,是不是無意之間暴露了什么……)馏予。今天帶大家來實(shí)現(xiàn)這款小游戲,也是對(duì)前面博客所講內(nèi)容的一個(gè)綜合實(shí)踐盔性。 效果圖如下:
0x00 游戲開發(fā)##
先扯一些沒用的,一款游戲一般由游戲策劃冕香、游戲程序員和美術(shù)人員來共同完成蛹尝。游戲開發(fā)的主流語言還是C/C++。你平時(shí)可以寫一些小游戲來提高你的C/C++的水平∠の玻現(xiàn)代大型游戲多在游戲引擎基礎(chǔ)之上開發(fā)突那,目前主流的游戲引擎有U3D、UE4和CE3等等构眯。各大游戲引擎的優(yōu)缺點(diǎn)我們也不做討論愕难,唯一的共同點(diǎn)就是他們都非常復(fù)雜。游戲引擎開發(fā)是一項(xiàng)極具挑戰(zhàn)的工作惫霸,牛叉的游戲引擎一般由團(tuán)隊(duì)共同合作完成(國產(chǎn)電視劇《微微一笑很傾城》 男主角貌似自己開發(fā)了一款游戲引擎猫缭,呵呵)。關(guān)于游戲引擎的更多知識(shí)可以讀一下這本書—《游戲引擎構(gòu)架》它褪。嗯饵骨,你沒猜錯(cuò),筆者就是國內(nèi)某游戲公司的程序猿茫打。
回歸正題居触,這款簡(jiǎn)單的俄羅斯方塊游戲肯定不基于任何游戲引擎啦,甚至它不使用任何渲染庫老赤。我們來寫一個(gè)控制臺(tái)版的小游戲轮洋。
0x01 如何下手##
前面提到過,一款游戲的制作一般由游戲策劃抬旺、游戲程序員和美術(shù)來共同完成弊予。其中,游戲策劃一般負(fù)責(zé)游戲的玩法开财、規(guī)則汉柒、界面误褪、數(shù)值等設(shè)計(jì),美術(shù)人員負(fù)責(zé)模型碾褂、動(dòng)畫兽间、原畫,插圖和游戲整體風(fēng)格的把握等正塌。游戲程序員負(fù)責(zé)實(shí)現(xiàn)游戲策劃所提的需求嘀略。那么,這款小游戲我們也可以從這幾個(gè)方面入手:
游戲的規(guī)則是什么乓诽?
游戲的界面應(yīng)該是什么樣子的帜羊,計(jì)分面板、說明面板放在哪里鸠天?
如何控制游戲(按鍵控制)讼育?
游戲的整體風(fēng)格應(yīng)該是什么樣子?
上面的這幾個(gè)問題解決了粮宛,就可以交給程序員去搞了窥淆。
0x02 程序設(shè)計(jì)##
接到策劃的需求后卖宠,程序如何設(shè)計(jì)呢巍杈?通過需求分析,仔細(xì)查看策劃人員給的設(shè)計(jì)圖(例如上面的效果圖)扛伍,你可以很容易得出以下結(jié)論:這是一個(gè)在Windows平臺(tái)下跑的一個(gè)控制臺(tái)游戲筷畦。顯然,你可能需要設(shè)計(jì)一個(gè)Console類和Window類(其中Console類是Window類的成員)刺洒。這兩個(gè)類應(yīng)該具有如下的能力:
控制窗口的標(biāo)題鳖宾,窗口大小,緩沖區(qū)大小逆航,光標(biāo)等
完全的控制輸出的能力鼎文,包括但不限于文字的位置,顏色因俐,前景色和背景色拇惋。
有了這些信息,你就可以Google和百度一下相關(guān)的API了抹剩,看哪些是已經(jīng)有的撑帖,哪些需要自己設(shè)計(jì)的。比如澳眷,你就可以查到以下的一些函數(shù):
GetStdHandle() // 獲得句柄
SetConsoleCursorInfo() // 設(shè)置光標(biāo)信息
SetConsoleWindowInfo() // 設(shè)置窗口信息
SetConsoleScreenBufferSize() //設(shè)置窗口緩沖區(qū)大小
SetConsoleTitle() //設(shè)置標(biāo)題
WriteConsoleOutputCharacter() 和 WriteConsoleOutputAttribute() //控制輸出的函數(shù)
其中胡嘿,WriteConsoleOutputCharacter()和WriteConsoleOutputAttribute()函數(shù)你也許并不熟悉,這就需要你查看相關(guān)文檔钳踊,弄懂這兩個(gè)函數(shù)了衷敌,因?yàn)檫@兩個(gè)函數(shù)至關(guān)重要勿侯,承擔(dān)了游戲的打印(渲染)任務(wù)。
簡(jiǎn)單的查一下缴罗,很快就能得到該函數(shù)的原型和相關(guān)參數(shù)說明:
// 函數(shù)原型:
BOOL WriteConsoleOutputCharacter( // 在指定位置處插入指定數(shù)量的字符
HANDLE hConsoleOutput, // 句柄
LPCTSTR lpCharacter, // 字符串
DWORD nLength, // 字符個(gè)數(shù)
COORD dwWriteCoord, // 起始位置
LPDWORD lpNumberOfCharsWritten // 已寫個(gè)數(shù)
);
/* 參數(shù)簡(jiǎn)介:
hConsoleOutput:控制臺(tái)輸出句柄罐监,通過調(diào)用GetStdHandle函數(shù)獲得
HANDLE hnd;
hnd=GetStdHandle(STD_INPUT_HANDLE);
lpCharacter:要輸出的字符串
nLength:輸出長度
dwWriteCoord:起始位置
pNumberOfCharsWritten:已寫個(gè)數(shù),通常置為NULL
其中瞒爬,COORD是個(gè)結(jié)構(gòu)體變量類型*/
typedef struct _COORD
{
SHORT X;
SHORT Y;
} COORD;
上面這個(gè)是來自百度百科弓柱。其實(shí)更權(quán)威的說明應(yīng)該查詢MSDN,例如WriteConsoleOutputAttribute的傳送門侧但。 MSDN上對(duì)這個(gè)函數(shù)講解的非常詳細(xì)矢空,也非常權(quán)威,前提是你有閱讀英文文獻(xiàn)的能力禀横。還有就是在VS里打出這個(gè)函數(shù)名屁药,然后按F12直接查看這個(gè)函數(shù)的原型,根據(jù)參數(shù)的命名柏锄,大概了解一下這個(gè)函數(shù)酿箭。
設(shè)計(jì)這個(gè)小游戲剩下的就是游戲的邏輯了。我們?cè)O(shè)計(jì)Tetris類來進(jìn)行游戲的邏輯控制趾娃。我們還需要設(shè)計(jì)一個(gè)數(shù)據(jù)結(jié)構(gòu)來表示方塊缭嫡。單個(gè)方塊如何表示呢?通過我們隊(duì)游戲規(guī)則的了解和對(duì)圖形的觀察抬闷,我們可以使用4*4的矩陣來表示一個(gè)方塊妇蛀。例如:
我們使用一個(gè)四維數(shù)組表示所有的方塊。 diamonds[x][y][4][4],其中x表示有幾種方塊笤成,y表示這種方塊有幾種變形,[4][4]表示這個(gè)方塊评架。
0x03 工程結(jié)構(gòu)##
這個(gè)小游戲很簡(jiǎn)單,沒有那么多模塊】挥荆現(xiàn)在列一下這個(gè)工程的結(jié)構(gòu)纵诞,并做簡(jiǎn)要說明。其中.h頭文件為聲明培遵,定義在對(duì)應(yīng)的.cpp文件中浙芙。
Console 控制臺(tái)類
GameDefine 定義游戲的一些常量。
StringUtil 字符串工具類
Tetris 俄羅斯方塊類
Window 窗體類
其中的字符串工具類荤懂,最后并沒有用到茁裙。
0x04 code##
懶得放github上了,直接上代碼了节仿。代碼注釋還是比較詳盡的晤锥,應(yīng)該能看得懂。
Talk is Cheap, show you the code.
Console.h文件:
//--------------------------------------------------------------------
// 文件名: Console.h
// 內(nèi) 容: 控制臺(tái)類
// 說 明: 控制臺(tái)類的一些聲明
// 創(chuàng)建日期: 2016年9月6日
//--------------------------------------------------------------------
#pragma once // 保證該文件只被包含一次
#include <wchar.h>
#include <windows.h> // 使用windows系統(tǒng)下的東西需要引入的頭文件
class Console
{
friend class Window;
public:
/// \brief 初始化控制臺(tái)
/// \param caption 控制臺(tái)標(biāo)題
/// \param coordinate 控制臺(tái)的高和寬
void Init(const wchar_t* caption, COORD coordinate);
public:
HANDLE m_hStdInput; // 標(biāo)準(zhǔn)輸入句柄
private:
HANDLE m_hStdOutput; // 標(biāo)準(zhǔn)輸出句柄
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 打開控制臺(tái)
/// \param caption 控制臺(tái)標(biāo)題
/// \param coordinate 控制臺(tái)的高和寬
void Console::Init(const wchar_t* caption, COORD coordinate)
{
// 如果所給坐標(biāo)不合法,則直接退出
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);
// 去除光標(biāo)
CONSOLE_CURSOR_INFO cci = { CURSOR_SIZE, false };
SetConsoleCursorInfo(m_hStdOutput, &cci);
// 設(shè)置窗體大小
SMALL_RECT sr = { SMALL_RECT_TOP, SMALL_RECT_LEFT, coordinate.X - 1, coordinate.Y - 1 };
SetConsoleWindowInfo(m_hStdOutput, true, &sr);
// 設(shè)置緩沖區(qū)大小
m_coord = coordinate;
SetConsoleScreenBufferSize(m_hStdOutput, m_coord);
// 設(shè)置窗口標(biāo)題
SetConsoleTitle(caption);
}
Window.h文件
//--------------------------------------------------------------------
// 文件名: Window.h
// 內(nèi) 容: 窗體類
// 說 明: 它是控制臺(tái)的一個(gè)子部分
// 創(chuàng)建日期: 2016年9月6日
//--------------------------------------------------------------------
#pragma once
#include "Console.h"
class Window
{
public:
/// \brief 初始化窗口
/// \param console 控制臺(tái)引用
/// \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 控制臺(tái)引用
/// \param rect 位置信息
void Window::Init(Console& console, SMALL_RECT rect)
{
// 檢測(cè)位置信息是否合法
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)
{
// 先檢測(cè)位置信息是否合法
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 };
// 字符串轉(zhuǎn)換
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);
}
// 單字節(jié)字符
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);
}
// 雙字節(jié)字符
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
// 內(nèi) 容: 俄羅斯方塊類
// 說 明:
// 創(chuàng)建日期: 2016年9月6日
//--------------------------------------------------------------------
#pragma once
#include "Console.h"
#include "Window.h"
#include "GameDefine.h"
class Tetris
{
public:
/// \brief 構(gòu)造函數(shù)
/// \param console 控制臺(tái)
/// \param coordinate 控制臺(tái)的高和寬
Tetris(Console& console, COORD coordinate);
/// \brief 初始化游戲
/// \param keys 按鍵
/// \param keyDesc 按鍵描述
/// \param frequency 聲效頻率
/// \param duration 延續(xù)時(shí)間
void Init(int keys[KeyNum], char keyDesc[KeyNum][5], DWORD frequency, DWORD duration);
/// \brief 是否正在運(yùn)行游戲
bool IsRun();
/// \brief 獲取當(dāng)前等級(jí)
int GetLevel() const;
/// \brief 方塊下落
bool Fall();
/// \brief 消息處理
/// \param key 按鍵
/// \return 游戲結(jié)束返回false
bool MessageProc(const Cmd cmd);
private:
/// \brief 聲效
void VoiceBeep();
/// \brief 繪制得分
void DrawScoreLevel();
/// \brief 繪制下一個(gè)將要出現(xiàn)的圖形
void DrawNext();
/// \brief 繪制游戲結(jié)束界面
void DrawGameOver();
/// \brief 繪制顏色
void Draw(WORD color);
/// \brief 給定的是否可行
bool IsFit(int x, int y, int c, int z);
/// \brief 消除行
void RemoveRow();
/// \brief 旋轉(zhuǎn)(逆時(shí)針)
void MoveTrans();
/// \brief 向左移動(dòng)
void MoveLeft();
/// \brief 向右移動(dòng)
void MoveRight();
/// \brief 向下移動(dòng)
/// \return 0: 游戲結(jié)束矾瘾; -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;
// 延續(xù)時(shí)間
DWORD m_voiceDuration;
// 控制按鍵
int m_keys[KeyNum];
// 控制按鍵的描述
char m_keyDesc[KeyNum][5];
// 游戲是否結(jié)束
bool m_gameover;
// 游戲暫停
bool m_pause;
// 游戲聲效開關(guān)
bool m_voice;
// 游戲得分
int m_score;
// 游戲速度
int m_speed;
// 游戲數(shù)據(jù)(實(shí)際方塊的存放數(shù)據(jù))
char m_data[ROWS][COLS];
// 下一個(gè)方塊
int m_next;
// 位置(x, y)
int m_x, m_y;
// 當(dāng)前方塊
int m_currentDiamonds;
// 當(dāng)前方向
int m_currentDir;
// 窗口
Window win;
};
Tetris.cpp文件
#include "Tetris.h"
#include <time.h>
#include <stdio.h>
/// \brief 構(gòu)造函數(shù)
/// \param console 控制臺(tái)
/// \param coordinate 控制臺(tái)的高和寬
Tetris::Tetris(Console & console, COORD coordinate)
{
// 創(chuàng)建一個(gè)矩形
SMALL_RECT rect = { coordinate.X, coordinate.Y, coordinate.X + GAME_WIDTH, coordinate.Y + GAME_HIGHT };
// 初始化這個(gè)窗口
win.Init(console, rect);
}
/// \brief 初始化游戲
/// \param keys 按鍵
/// \param keyDesc 按鍵描述
/// \param frequency 聲效頻率
/// \param duration 延續(xù)時(shí)間
void Tetris::Init(int keys[KeyNum], char keyDesc[KeyNum][5], DWORD frequency, DWORD duration)
{
// 初始化游戲的數(shù)據(jù)
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;
// 方塊數(shù)據(jù)部分置0
memset(m_data, 0, sizeof(m_data));
// 設(shè)置隨機(jī)種子
srand((unsigned)time(NULL));
// 下一個(gè)方塊
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);
}
// 繪制下一個(gè)將要出現(xiàn)的方塊
DrawNext();
}
/// \brief 是否正在運(yùn)行游戲
bool Tetris::IsRun()
{
return !m_gameover && !m_pause;
}
/// \brief 獲取當(dāng)前等級(jí)
int Tetris::GetLevel() const
{
return m_speed;
}
/// \brief 方塊下落
bool Tetris::Fall()
{
return MessageProc(CMD_DOWN);
}
/// \brief 消息處理
/// \param key 按鍵
/// \return 游戲結(jié)束返回false
bool Tetris::MessageProc(const Cmd cmd)
{
int const key = m_keys[cmd];
// 游戲結(jié)束
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 繪制下一個(gè)將要出現(xiàn)的圖形
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 繪制游戲結(jié)束界面
void Tetris::DrawGameOver()
{
COORD coord = { 28, 1 };
win.Output("游戲結(jié)束", 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 旋轉(zhuǎn)(逆時(shí)針)
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 向左移動(dòng)
void Tetris::MoveLeft()
{
if (IsFit(m_x - 1, m_y, m_currentDiamonds, m_currentDir))
{
VoiceBeep();
Draw(COLOR_BLANK);
--m_x;
Draw(COLOR_MOVE);
}
}
/// \brief 向右移動(dòng)
void Tetris::MoveRight()
{
if (IsFit(m_x + 1, m_y, m_currentDiamonds, m_currentDir))
{
VoiceBeep();
Draw(COLOR_BLANK);
++m_x;
Draw(COLOR_MOVE);
}
}
/// \brief 向下移動(dòng)
/// \return 0: 游戲結(jié)束; -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;
}
// 游戲結(jié)束
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
// 內(nèi) 容: 字符串工具類
// 說 明: 提供字符串操作的一些便捷工具類
// 創(chuàng)建日期: 2016年9月6日
// 創(chuàng)建人: AceTan
// 版權(quán)所有: AceTan
//--------------------------------------------------------------------
#pragma once
#include <string>
#include <wchar.h>
#include <Windows.h>
// 字符串處理
class StringUtil
{
public:
// 字符串轉(zhuǎn)換成寬字符串
static const wchar_t* StringToWideStr(const char* info, wchar_t* buf,
size_t size, long codepage = CP_UTF8);
// 寬字符串轉(zhuǎn)換成字符串
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>
// 字符串轉(zhuǎn)換到寬字符串
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;
}
// 寬字符串轉(zhuǎn)換成字符串
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
// 內(nèi) 容: 游戲定義文件
// 說 明: 定義游戲的一些常量蛉迹,比如窗口大小等
// 創(chuàng)建日期: 2016年9月6日
//--------------------------------------------------------------------
#pragma once
#include <windows.h>
// 高度
const SHORT GAME_HIGHT = 21;
// 寬度
const SHORT GAME_WIDTH = 38;
// 方塊的行數(shù)
const SHORT ROWS = 21;
// 方塊的列數(shù)
const SHORT COLS = 13;
// 運(yùn)動(dòng)中的顏色
const WORD COLOR_MOVE = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY;
// 固定不動(dòng)的顏色
const WORD COLOR_STILL = FOREGROUND_GREEN;
// 空白處的顏色
const WORD COLOR_BLANK = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
// 方塊種類
const unsigned int DIAMONDS_TYPES = 7;
// 每個(gè)方塊有幾種變形
const unsigned int DIAMONDS_TRANS = 4;
// 表示單個(gè)方塊的行數(shù)
const unsigned int DIAMONDS_IFNO_ROW = 4;
// 表示單個(gè)方塊的列數(shù)
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;
// 得分滿,加一個(gè)速度
const int SPEED_ADD_SCORE = 10000;
// 默認(rèn)聲效頻率
const DWORD DEFAULT_FREQUENCY = 1760;
// 默認(rèn)聲效延續(xù)時(shí)間
const DWORD DEFAULT_DURATION = 20;
// 超時(shí)下落
const DWORD TIME_OUT = 1000;
// 休眠間隔時(shí)間(毫秒)
const int SLEEP_TIME = 200;
// 游戲按鍵對(duì)應(yīng)的索引
enum KeyIndex
{
GameBegin = 0, // 游戲開始
GamePause, // 游戲暫停
GameVoice, // 游戲聲效
Up, // 方向鍵-上
Left, // 方向鍵-左
Right, // 方向鍵-右
Down, // 方向鍵-下
FallDown, // 方塊直接落地
KeyNum, // 按鍵總數(shù)
};
// 對(duì)應(yīng)的鍵值(這個(gè)需要查表或者自己實(shí)驗(yàn)所得)
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 };
// 方塊用一個(gè)4維數(shù)組表示:共7種不同方塊放妈,4種變形北救。每個(gè)方塊用 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┃"
"┗━━━━━━━━━━━┛┗━━━━┛";
// 游戲開始時(shí)的X坐標(biāo)
const unsigned int GameStartX = 38;
// 游戲開始時(shí)的Y坐標(biāo)
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)
{
// 延時(shí)芜抒,減少CPU占用率
Sleep(SLEEP_TIME);
DWORD newTime = GetTickCount();
// 超時(shí)下落
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;
}
}
}
// 分發(fā)按鍵命令處理
void DispatchCmd(Tetris& tetris, Console& console, Cmd cmd)
{
switch (cmd)
{
case CMD_QUIT:
exit(0);
break;
default:
tetris.MessageProc(cmd);
break;
}
}
int main()
{
// 創(chuàng)建一個(gè)控制臺(tái)
Console console;
// 創(chuàng)建一個(gè)坐標(biāo)
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 結(jié)束語##
以上文件在VS2015下編譯通過珍策,并且可以運(yùn)行,其他版本沒有試過宅倒。另外攘宙,這個(gè)小游戲參考了我很久之前寫的代碼,現(xiàn)在進(jìn)行了代碼重構(gòu)和調(diào)整拐迁。記得之前寫的時(shí)候是參考了網(wǎng)上的設(shè)計(jì)蹭劈,無奈找不到源出處了,侵刪线召。
這里面用到的知識(shí)都是我在之前的博客里講到的铺韧,如果讀者感覺有疑惑或者困難,請(qǐng)移步去看一下前面的博客內(nèi)容灶搜。如果你完全看不懂這寫的啥祟蚀,那么我建議你多敲代碼多看書工窍。