TETRIS—基于現(xiàn)代官方規(guī)則俄羅斯方塊實(shí)現(xiàn)案例(c/c++,EasyX,附完整代碼)

簡介:
作為方塊圈的玩家,和剛接觸 EasyX 的萌新。我希望大家能夠重新認(rèn)識一下這款經(jīng)久不衰的游戲。
官方規(guī)則是基于俄羅斯方塊公司(The Tetris Company,TTC)公司授權(quán)的方塊游戲規(guī)則。如俄羅斯方塊效應(yīng),噗喲噗喲VS俄羅斯方塊等游戲采用的規(guī)則。
大部分人對方塊的印象大多還停留在上個(gè)世紀(jì)的? ”消消樂“ 吧?我將一邊科普現(xiàn)代方塊規(guī)則, 一邊介紹個(gè)人的實(shí)現(xiàn)思路,而不是隨隨便便寫一個(gè)消消樂。
也希望大家可以去體驗(yàn)一下方塊的魅力 !
先來介紹一下現(xiàn)代方塊界面吧

噗喲噗喲 VS 俄羅斯方塊 2 游戲界面。
介紹 / 規(guī)則
標(biāo)準(zhǔn)場地是 10 X 20 個(gè)小方塊場地,共有 7 個(gè)方塊,一般也都采用固定 7 種顏色。比如我看到黃色就知道是正方形來了。
來認(rèn)識一下各個(gè)方塊,現(xiàn)代方塊以 “ T ” 方塊為尊(紫色),“ I ” 方塊居其次(棍子/長條),紅色 Z 綠色 S,一個(gè)正方形 O 和 JL。
七個(gè)字母對應(yīng)形狀,方塊也是有名字的,TIOZSJL 每個(gè)方塊都由 4 個(gè)小方塊組成,場地里填滿一行就會消除,然后相愛相殺!
HOLD:儲存一個(gè)方塊,可與當(dāng)前操作的方塊交換一下。(是從上面重新下來的不是直接和當(dāng)前位置交換,每次鎖定前只能交換一次)
NEXT:這個(gè)好理解看見下一個(gè)方塊是什么。(官方的好像沒有超過 5next 的)
GHOST:影子,可以看見當(dāng)前方塊落下后的樣子。(游戲里都是可以選擇開啟/關(guān)閉的)
7bg:七巡,方塊的出塊規(guī)則。隨機(jī)排列這 7 個(gè)不同方塊為一包,一包一包出方塊。
操作:軟降(加速下落)/硬降(當(dāng)前方塊直接下落到最底部鎖定)左移動(dòng)/右移動(dòng)/軟/順時(shí)針旋轉(zhuǎn)/逆時(shí)針旋轉(zhuǎn)/ HOLD(暫存按鍵)。標(biāo)準(zhǔn)七個(gè)按鍵。
方塊落到最下面不會鎖定,還可以旋轉(zhuǎn)和移動(dòng)如果 1.5 秒(大約)不操作才會鎖定,硬降就是直接落下鎖定。
對戰(zhàn)傷害系統(tǒng)就先不提了,可以來看看下面的視頻鏈接。(與人斗其樂無窮!)
【國區(qū)最強(qiáng)噗喲噗喲俄羅斯方塊2比賽(上)】
代碼思路解析(個(gè)人理解)
場地:想必大家也都理解要用二維數(shù)組儲存場地?cái)?shù)據(jù),xy 坐標(biāo)和 ij 數(shù)組下標(biāo)方便后面功能判斷。
#define Column 10 // 10 列
#define RowNum 22 // 22 行,現(xiàn)代方塊場地理論上應(yīng)該更高
const int Radius = 20; // 方塊大小
// 枚舉每一個(gè)小方塊的填充和未填充狀態(tài)
enum T_STATE {NoBlock, IsBlock};
// 表示方向
enum DIRECTION {Up1, Right2, Down3, left4};
// 表示當(dāng)前方塊種類
enum TKIND {T, I, O, S, Z, L, J};
// 創(chuàng)建單個(gè)方塊結(jié)構(gòu)體
class A_TETRIS
{
public:
int x, y; // 儲存小方塊的中心 X Y 坐標(biāo)
int i, j; // 儲存小方塊的在地圖數(shù)組下標(biāo) i j
T_STATE T_state; // 每一個(gè)小方塊的狀態(tài),存在和不存在
IMAGE T_im; // 一個(gè)小方塊的皮膚
}
// 打包的 4 個(gè)方塊都有一個(gè)旋轉(zhuǎn)中心點(diǎn)
struct FOUR_TETRIS
{
A_TETRIS TheFourT[4]; // 打包 4 個(gè)為一個(gè)
int centerX, centerY; // 儲存旋轉(zhuǎn)中心位置
float centeri, centerj; // 儲存旋轉(zhuǎn)中心 ij 下標(biāo),為什么是 float 呢,特殊方塊中心點(diǎn)不規(guī)則
DIRECTION Direction = Up1;
TKIND Kind; // 方塊種類
}
// 定義地圖數(shù)據(jù)結(jié)構(gòu)體
class TETRIS_MAP
{
public:
A_TETRIS Map_T[RowNum][Column]; // 地圖二維數(shù)組
}
由每一個(gè)小方塊的數(shù)據(jù)構(gòu)成一個(gè)地圖數(shù)據(jù),然后畫出來。3 個(gè)枚舉也好理解,地圖里就用一個(gè)是否有方塊狀態(tài),有就顯示圖片沒有就不顯示。
你也可以直接使用簡單的填充矩形,后期隨變改皮膚。數(shù)組構(gòu)成一個(gè)地圖,每個(gè)方塊由 4 個(gè)小方塊組成。
那么我再定義一個(gè)結(jié)構(gòu)體,里面由包含 4 個(gè)小方塊數(shù)據(jù)的一維數(shù)組組成。
我直接寫進(jìn)去 4 個(gè)元素?cái)?shù)據(jù),就組成了我要的任意形狀方塊。
方塊一個(gè)一個(gè)按順序出來。那么我定義一個(gè)類,表示序列。
class SEVEN_BG// 創(chuàng)建七循相關(guān)結(jié)構(gòu)體
{
public:
FOUR_TETRIS Four_T; // 用于初始化加載的方塊
vector<FOUR_TETRIS> SevenList; // 儲存初始化的 7 個(gè)方塊鏈表
vector<FOUR_TETRIS> PutSevenT; // 用來存放固定的 7 個(gè)方塊
vector<FOUR_TETRIS> NextList; // 存放 NEXT 序列
}
序列存放方塊數(shù)據(jù),我想到的就是鏈表,#include <vector> //矢量模板也是鏈表,我用的這個(gè)。
這里用了 3 個(gè)鏈表儲存單個(gè)方塊信息,第一個(gè)里只有 7 個(gè)方塊數(shù)據(jù),我直接暴力寫入 7 個(gè)方塊數(shù)據(jù),存到這鏈表里
void initialize() // 簡單粗暴的直接寫入 7bg 初始數(shù)據(jù)
{ // 加載順序 T I O S Z L J 超出邊界的方塊手動(dòng)計(jì)算賦值一下
Four_T.TheFourT[0] = MapDate.Map_T[1][4];
Four_T.TheFourT[1] = MapDate.Map_T[2][3];
Four_T.TheFourT[2] = MapDate.Map_T[2][4];
Four_T.TheFourT[3] = MapDate.Map_T[2][5];
for (int i =0; i<4; i++)
{
Four_T.TheFourT[i].T_state = IsBlock; // 也就第一次要加載這個(gè)
loadimage(&Four_T.TheFourT[i].T_im, _T("image / T.png"));
}
Four_T.centerX = Four_T.TheFourT[2].x; // T 的中心點(diǎn)
Four_T.centerY = Four_T.TheFourT[2].y;
Four_T.centeri = Four_T.TheFourT[2].i; // 記錄下標(biāo)
Four_T.centerj = Four_T.TheFourT[2].j;
Four_T.Kind = T;
SevenList.push_back(Four_T); // 將一個(gè) T 方塊信息儲存到了 7BG 鏈表
}
手寫七·個(gè)方塊數(shù)據(jù)到第一個(gè)鏈表里,這里省略。
第二個(gè)鏈表復(fù)制一遍第一個(gè)鏈表,然后寫一個(gè)簡單的隨機(jī)取出鏈表里的函數(shù)
int Rand7BG(int Max)// 生成兩個(gè)數(shù)之間的隨機(jī)數(shù)。就是選擇下一個(gè)方塊
{ // max 不能為 0。思考一下取 0 的余是啥?
????int num =int(rand()%Max);
????return num;
}
7 個(gè)方塊嘛,循環(huán) 7 次不就全取完了,隨機(jī)取 7 次,取一次 Max 減一。就得到隨機(jī)方塊順序,然后存到 第三個(gè)序列鏈表里。
取完了再第二鏈表再復(fù)制一遍第一個(gè)鏈表,再隨機(jī)取出給第三個(gè)序列鏈表,這就是出塊規(guī)則了。
當(dāng)前操作的方塊就等于第三個(gè)鏈表里的第一個(gè)數(shù)據(jù),我定義了一個(gè)當(dāng)前方塊類,依然引用 4 個(gè)單個(gè)方塊構(gòu)成的一個(gè)方塊結(jié)構(gòu)體。
class NOW_TETRIS// 創(chuàng)建當(dāng)前方塊結(jié)構(gòu)體
{
public:
int EraseRow[4]; // 要消除的行的 i 標(biāo)
int whatRL = 0; // 右旋轉(zhuǎn)為 1,左旋轉(zhuǎn)為-1
FOUR_TETRIS Now_FourT; // 當(dāng)前方塊信息
FOUR_TETRIS Ghost_FourT; // 存放影子信息
FOUR_TETRIS Rotate_FourT; // 用來計(jì)算旋轉(zhuǎn)是否成立
FOUR_TETRIS SeekSpin_FourT; // 正常旋轉(zhuǎn)不行就來計(jì)算偏移
}
當(dāng)前方塊信息算一個(gè),影子算一個(gè),也就等于鏈表里的第一個(gè)數(shù)據(jù)。鎖定后刪除鏈表第一個(gè)數(shù)據(jù)然后再復(fù)制第一個(gè)數(shù)據(jù),源源不斷的生成。
void OnceDown() // 一次下落執(zhí)行的數(shù)據(jù)
{ y + = 2 * Radius;
i + = 1; }
單個(gè)方塊結(jié)構(gòu)體的移動(dòng)
void OnceUp() // 一個(gè)整體方塊的一次向上移動(dòng)
{
for (int i =0; i<4; i++)
TheFourT[i].OnceUp();
centeri - = 1;
centerY - = 2 * Radius;
}
移動(dòng)簡單,下落還是移動(dòng)都是這種一格一格的,調(diào)用然后放到循環(huán)里更新就好了。判斷能不能移動(dòng)和下落稍微麻煩一點(diǎn)點(diǎn)而已。
int InspectDown(A_TETRIS &Now, A_TETRIS &Next) // 檢查下一個(gè)下降位置是否有沖突
{
if (Now.i > = RowNum - 1 || Next.T_state ! = NoBlock)
return 1;
else return 0;
}
int JudgeWhetherDown() // 判斷是否可以下落
{
for (int i = 0; i<4; i++)
{ // 下面這串是檢查當(dāng)前方塊下路是否和地圖里的有沖突
if (InspectDown(Now_FourT.TheFourT[i], MapDate.Map_T[Now_FourT.TheFourT[i].i + 1][Now_FourT.TheFourT[i].j]))
return 0;
}
return 1;
}
每一個(gè)小方塊里都有一個(gè)枚舉表示當(dāng)前是否有方塊,也有數(shù)組下標(biāo)數(shù)據(jù),提前判斷一下它移動(dòng)后的位置有沒有存在方塊,和邊界判斷,如上。
我這里旋轉(zhuǎn)也是提前將旋轉(zhuǎn)過后的數(shù)據(jù)存一個(gè)然后和地圖數(shù)據(jù)比對。
旋轉(zhuǎn)在現(xiàn)代方塊規(guī)則里是非常重要的!繞旋轉(zhuǎn)中心點(diǎn)旋轉(zhuǎn),旋轉(zhuǎn)中心各位可以運(yùn)行后面源文件里程序觀看,如下。

旋轉(zhuǎn)中心每個(gè)方塊都不一樣,這個(gè)數(shù)據(jù)也在一開始鏈表里寫數(shù)據(jù)的直接寫了,在 4 個(gè)小方塊結(jié)構(gòu)體里也定義了中心點(diǎn)數(shù)據(jù)。以這個(gè)點(diǎn)是以為官方標(biāo)準(zhǔn),旋轉(zhuǎn)系統(tǒng)稱為 SRS 系統(tǒng)。
如何通過旋轉(zhuǎn)中心坐標(biāo)計(jì)算旋轉(zhuǎn)后 4 個(gè)小方塊的坐標(biāo)。下面這個(gè)可是我打了不少草稿發(fā)現(xiàn)規(guī)律簡化的!
A_TETRIS LoadRspin(A_TETRIS &oneT) // 載入右旋轉(zhuǎn),輸入一個(gè)單方塊用來計(jì)算他旋轉(zhuǎn)后的坐標(biāo)位置
{
float xd = centerX - oneT.x;
float yd = centerY - oneT.y;
float jd = centerj - oneT.j;
float id = centeri - oneT.i;
oneT.x = centerX + yd;
oneT.j = centerj + id;
oneT.y = centerY - xd;
oneT.i = centeri - jd;
return oneT;
}
A_TETRIS LoadLspin(A_TETRIS &oneT) // 載入左旋轉(zhuǎn),輸入一個(gè)單方塊用來計(jì)算他旋轉(zhuǎn)后的坐標(biāo)位置
{
float xd = centerX - oneT.x;
float yd = centerY - oneT.y;
float jd = centerj - oneT.j;
float id = centeri - oneT.i;
oneT.x = centerX - yd;
oneT.j = centerj - id;
oneT.y = centerY + xd;
oneT.i = centeri + jd;
return oneT;
}
上面就是如何通過旋轉(zhuǎn)中心坐標(biāo)計(jì)算旋轉(zhuǎn)后 4 個(gè)小方塊的坐標(biāo)。這個(gè)可是我打了不少草稿發(fā)現(xiàn)規(guī)律簡化的!
得到旋轉(zhuǎn)過后的數(shù)據(jù)了,要和地圖里的數(shù)據(jù)比較。防止重疊嘛,但是當(dāng)方塊移動(dòng)到邊上后,旋轉(zhuǎn)過后的樣子肯定超出邊界了。
此外還有特殊旋轉(zhuǎn),你可能會覺得下面這個(gè)有點(diǎn)離譜。但這都是真的,當(dāng)然也是有條件的,也有別的奇怪旋轉(zhuǎn)。

這里就要用到官方設(shè)定偏移表了。(千萬不要研究什么旋轉(zhuǎn)規(guī)律!我痛苦的回憶!)
以下面 i 塊為例子,我也會在壓縮包里放上完整偏移表。

i 的偏移最特殊,雖然它只有橫豎兩個(gè)狀態(tài),注意看旋轉(zhuǎn)中心位置,它是不一樣的。
來看看消除
int JudgeErase(int &row) // 判斷當(dāng)前行是否消除
{
int clear = 0; // 判斷是否清除。滿足 10 行就清除
for (int i = 0; i< Column; i ++)
{
if(Map_T[row][i].T_state == IsBlock)
clear ++;
}
if (clear == 10)
return row;
else return - 1;
}
void eraseline(int &row) // 獲取到要消除的行,執(zhí)行消行后需要做的
{
for (int j = 0; j<Column; j ++) // 首先刪除當(dāng)前行的方塊
Map_T[row][j].T_state = NoBlock;
for (int T = row - 1; T>=0; T--) // 當(dāng)前要消除的上一行開始遍歷
for (int j = 0; j<Column; j ++) // 列
{ // 要消除的肯定得是不是方塊狀態(tài)
if (Map_T[T][j].T_state == IsBlock) // 只有遍歷到存在方塊的數(shù)據(jù)才開始執(zhí)行下落
{ // 降消除后落下的方塊的信息修改正確
Map_T[T + 1][j].T_im = Map_T[T][j].T_im;
Map_T[T + 1][j].T_state = IsBlock;
Map_T[T][j].T_state = NoBlock; // 當(dāng)前方塊給下面后刪除
}
}
}
我這里是每次落下方塊,獲取當(dāng)前落下方塊的 4 個(gè)小方塊的 i?坐標(biāo),對應(yīng)地圖里那一行有沒有滿足 10 個(gè),滿足就代表這一行要消除,讓當(dāng)前行消失,狀態(tài)為 noblock 沒有方塊。上面的方塊往下移動(dòng)一格,從當(dāng)前 i 行便利走最上面第 22 行,對應(yīng) i 下標(biāo)為 0。
HOLD 交換功能就是和當(dāng)前方塊交換一下嘛,然后顯示在右上角就好了。下落就寫個(gè)時(shí)間觸發(fā)移動(dòng)一下,不能移動(dòng)后過 1 秒不操作就鎖定,數(shù)據(jù)寫到地圖里。
GOHST 影子數(shù)據(jù)一開始肯定和當(dāng)前方塊一樣位置也一樣,直接判斷影子方塊能不能往下移動(dòng)一格,這個(gè)判斷放到 while 里,可以下落就繼續(xù)下落,直到不能下落才是影子位置的數(shù)據(jù)。
寫一個(gè)找到影子位置的函數(shù),只在方塊左右移動(dòng)的時(shí)候調(diào)用一下,別的操作不要調(diào)用。移動(dòng)還有 das 什么操作要求啥的,等我下次把別的功能做好再來更新吧。目前我寫的這個(gè)操作起來還不錯(cuò),我就是玩方塊的玩起來手感還行哦。
好就寫到這吧,源代碼都有注釋應(yīng)該容易理解,比較詳細(xì)。(有些注釋自己寫著玩的請見諒)
http://farter.cn/t/?塊圈群主“屁大爺”的屁塊?。ㄍ诰蚰J胶苡幸馑己苌项^)
http://tetr.io? ?目前圈里很火的在線對戰(zhàn)方塊
墨白的源碼 :https://pan.baidu.com/s/1FmIBp5sobAqZb-h870BWWw?pwd=gi68?
提取碼:gi68?
調(diào)試環(huán)境 VS200 (沒有EasyX庫的可以去官網(wǎng)看一下https://easyx.cn/)