目录
一. 俄罗斯方块简易版的实现
1. 图形的存储
俄罗斯方块一共7种基础图形,旋转后会得到19种形态。那么可以认为我们有7种基础图形,通过旋转操作这7种基础图形又会得到不同的状态,我们可以只存储这7种基本的图形,但是讨论图形旋转的状态变化会比较麻烦。我们换一种思路,那就是存储每一种状态的坐标,这个时候我们旋转变换便是状态的变换,也就是说旋转后的状态我们已经记录下来了,在旋转的碰撞检测时就省去了不必要的计算。
思路有了接下来就实现一下,假设我们使用4 * 4的网格来存放所有的状态,那么每种状态就会对应4个坐标对,假设从左到右依次为0~3,从上到下页依次为0~3,那么我们根据每种状态四个小方块的位置可以写出如下代码。
// 声明这19种图形
// 图形4个像素点的声明顺序无关紧要
// 但每种图形必须按旋转变换的顺序依次声明
Point From[19][4] = {
// 条形
{{0, 0}, {1, 0}, {2, 0}, {3, 0}},
{{0, 0}, {0, 1}, {0, 2}, {0, 3}},
// 方形
{{0, 0}, {1, 0}, {0, 1}, {1, 1}},
// L形
{{0, 0}, {0, 1}, {0, 2}, {1, 2}},
{{2, 0}, {1, 0}, {0, 0}, {0, 1}},
{{2, 2}, {2, 1}, {2, 0}, {1, 0}},
{{0, 2}, {1, 2}, {2, 2}, {2, 1}},
// 镜像L形
{{2, 0}, {2, 1}, {2, 2}, {1, 2}},
{{2, 2}, {1, 2}, {0, 2}, {0, 1}},
{{0, 2}, {0, 1}, {0, 0}, {1, 0}},
{{0, 0}, {1, 0}, {2, 0}, {2, 1}},
// T型
{{1, 1}, {0, 2}, {1, 2}, {2, 2}},
{{1, 1}, {0, 0}, {0, 1}, {0, 2}},
{{1, 1}, {2, 0}, {1, 0}, {0, 0}},
{{1, 1}, {2, 0}, {2, 1}, {2, 2}},
// 闪电形
{{0, 0}, {0, 1}, {1, 1}, {1, 2}},
{{2, 0}, {1, 0}, {1, 1}, {0, 1}},
// 镜像闪电形
{{0, 0}, {1, 0}, {1, 1}, {2, 1}},
{{1, 0}, {1, 1}, {0, 1}, {0, 2}}};
4个像素点的声明顺序无关紧要,但每种图形必须按旋转变换的顺序依次声明,结合碰撞检测代码和旋转代码的实现想想为什么。
这里我们需要存储的坐标对并不算多,并不需要在堆上申请内存,也没必要压缩存储。
2. 图形的显示
我们首先思考一下我们需要哪些变量。按惯例,首先是场景的宽度BW、场景的高度BH,紧接着是图形的初始坐标HomeX、HomeY,然后是方块的显示数组Board。最后,针对俄罗斯方块这个游戏,我们需要声明俄罗斯方块的形状sharp,俄罗斯方块的坐标x、y。
const int BW(20); // BW表示场景宽度
const int BH(27); // BH表示场景高度
const int HomeX(BW - 1 >> 1); // HomeX表示图形初始横坐标
const int HomeY(1); // HomeY表示图形初始纵坐标
bool Board[BW * BH]; // Board[x + y * BW]表示(x, y)处是否为小方块
int sharp; // 图形的形状(形状随机产生)
int x, y; // 图形的横纵坐标
按惯例我们先完成main()函数。
int main()
{
SetConsole("俄罗斯方块", 40, 27, "80");
srand((int)time(0));
run();
}
紧接着我们按照之前的模版快速写出run()函数。Initialize()、Drop()、Left()、Right()、Ratote()这5个函数先声明不实现。
// 运行游戏
void run()
{
Initialize();
int t = 0;
while (true)
{
// 时间复杂度O(BW * BH * BH)
Sleep(1);
if (t++ > 50)
{
Drop();
t = 0;
}
if (kbhit())
{
int ch = getch();
if (ch == 224)
{
switch (getch())
{
case 72: // 按下键盘上键
Ratote(); // 旋转图形
break;
case 80: // 按下键盘下键
Drop(); // 下降图形
break;
case 75: // 按下键盘左键
Left(); // 左移图形
break;
case 77: // 按下键盘右键
Right(); // 右移图形
break;
}
}
else if (ch == 32)
Pause();
}
}
}
接下来就要开始动一下脑子了。我们知道图形的坐标(x, y),图形的形状sharp,那么我们如何显示图形呢,我们依次打印表示图形的4个像素点即可,将我们存储的小方块的相对坐标转成绝对坐标,接下来我们将像素点绝对坐标处对应的Board值设置为true(该处存在小方块),然后用FillStr()函数来打印小方块即可。考虑到代码重复度较高,我们再写一个辅助函数ShowOrClear()来完成重复的工作。
// 相对坐标转绝对坐标
Point SiteChange(Point p)
{
return {x + p.x, y + p.y};
}
// 显示、清除图形的辅助函数
void ShowOrClear(bool exist, const std::string &fill)
{
for (int i = 0; i < 4; ++i)
{
Point Pixel = SiteChange(From[sharp][i]);
Board[Pixel.x + Pixel.y * BW] = exist;
FillStr(Pixel.x, Pixel.y, fill);
}
}
// 显示图形
void ShowGraph() { ShowOrClear(true, "■"); }
// 清除图形
void ClearGraph() { ShowOrClear(false, " "); }
实现一下Initialize()函数。
// 初始化游戏
void Initialize()
{
system("cls");
AddGraph();
score = 0;
speed = 1;
MaxScore = 0;
for (bool b : Board)
b = false;
}
实现一下AddGraph()函数。
// 添加图形
void AddGraph()
{
sharp = rand() % 19;
x = HomeX;
y = HomeY;
ShowGraph();
}
3. 图形的操作
接下来我们实现一下基本操作。
下降函数Drop()。先清除屏幕上的图形,改变纵坐标的位置,重新打印屏幕上的图形。该图形无法继续下降了,先检测是否有满行可以消除,再添加新的图形到屏幕上。
// 图形下降
void Drop()
{
if (!Drop_CD())
{
ClearGraph();
++y;
ShowGraph();
}
else
{
LineRemove();
AddGraph();
}
}
左移函数Left()。和下降函数同理。
// 图形左移
void Left()
{
if (!Left_CD())
{
ClearGraph();
--x;
ShowGraph();
}
}
右移函数Right()。和下降函数同理。
// 图形右移
void Right()
{
if (!Right_CD())
{
ClearGraph();
++x;
ShowGraph();
}
}
旋转函数Ratote()。先记录旋转后的状态,再判断旋转后的状态是否可行。如果理解不了可以用switch()语句来表示旋转的状态转换,这里巧妙的避开了坐标的变换计算,因为每种状态都记录了相应的相对坐标。
因为我们已经记录了每种状态对应的相对坐标了,我们这里只需要搞清楚每种状态的下一个状态是什么就行了,我最初并不是这么做的,而是分情况讨论了每种状态的旋转操作对应的坐标转换公式,也就是通过sharp重新计算了sharp旋转变换后对应的相对坐标。当然我们可以巧妙地避开这些繁琐的计算,于是我们得到了下面这个十分简洁的旋转函数。
// 图形旋转
void Ratote()
{
int NextSharp = sharp;
if (sharp == 6 || sharp == 10 || sharp == 14)
NextSharp -= 3;
else if (sharp == 1 || sharp == 16 || sharp == 18)
--NextSharp;
else
++NextSharp;
if (!Ratote_CD(NextSharp))
{
ClearGraph();
sharp = NextSharp;
ShowGraph();
}
}
此时代码如下。
#include "../Engine/BrickEngine.h"
const int BW(20); // BW表示场景宽度
const int BH(27); // BH表示场景高度
const int HomeX(BW - 1 >> 1); // HomeX表示图形初始横坐标
const int HomeY(1); // HomeY表示图形初始纵坐标
bool Board[BW * BH]; // Board[x + y * BW]表示(x, y)处是否为小方块
int sharp; // 图形的形状(形状随机产生)
int x, y; // 图形的横纵坐标
// 声明这19种图形
// 图形4个像素点的声明顺序无关紧要
// 但每种图形必须按旋转变换的顺序依次声明
Point From[19][4] = {
// 条形
{{0, 0}, {1, 0}, {2, 0}, {3, 0}},
{{0, 0}, {0, 1}, {0, 2}, {0, 3}},
// 方形
{{0, 0}, {1, 0}, {0, 1}, {1, 1}},
// L形
{{0, 0}, {0, 1}, {0, 2}, {1, 2}},
{{2, 0}, {1, 0}, {0, 0}, {0, 1}},
{{2, 2}, {2, 1}, {2, 0}, {1, 0}},
{{0, 2}, {1, 2}, {2, 2}, {2, 1}},
// 镜像L形
{{2, 0}, {2, 1}, {2, 2}, {1, 2}},
{{2, 2}, {1, 2}, {0, 2}, {0, 1}},
{{0, 2}, {0, 1}, {0, 0}, {1, 0}},
{{0, 0}, {1, 0}, {2, 0}, {2, 1}},
// T型
{{1, 1}, {0, 2}, {1, 2}, {2, 2}},
{{1, 1}, {0, 0}, {0, 1}, {0, 2}},
{{1, 1}, {2, 0}, {1, 0}, {0, 0}},
{{1, 1}, {2, 0}, {2, 1}, {2, 2}},
// 闪电形
{{0, 0}, {0, 1}, {1, 1}, {1, 2}},
{{2, 0}, {1, 0}, {1, 1}, {0, 1}},
// 镜像闪电形
{{0, 0}, {1, 0}, {1, 1}, {2, 1}},
{{1, 0}, {1, 1}, {0, 1}, {0, 2}}};
void run();
// 相对坐标转绝对坐标
Point SiteChange(Point p)
{
return {x + p.x, y + p.y};
}
// 显示、清除图形的辅助函数
void ShowOrClear(bool exist, const std::string &fill)
{
for (int i = 0; i < 4; ++i)
{
Point Pixel = SiteChange(From[sharp][i]);
Board[Pixel.x + Pixel.y * BW] = exist;
FillStr(Pixel.x, Pixel.y, fill);
}
}
// 显示图形
void ShowGraph() { ShowOrClear(true, "■"); }
// 清除图形
void ClearGraph() { ShowOrClear(false, " "); }
// 添加图形
void AddGraph()
{
sharp = rand() % 19;
x = HomeX;
y = HomeY;
ShowGraph();
}
// 整行消除
void LineRemove() {}
// 碰撞检测:图形下降
bool Drop_CD() { return false; }
// 图形下降
void Drop()
{
if (!Drop_CD())
{
ClearGraph();
++y;
ShowGraph();
}
else
{
LineRemove();
AddGraph();
}
}
// 碰撞检测:图形左移
bool Left_CD() { return false; }
// 图形左移
void Left()
{
if (!Left_CD())
{
ClearGraph();
--x;
ShowGraph();
}
}
// 碰撞检测:图形右移
bool Right_CD() { return false; }
// 图形右移
void Right()
{
if (!Right_CD())
{
ClearGraph();
++x;
ShowGraph();
}
}
// 碰撞检测:图形旋转
bool Ratote_CD(int NextSharp) { return false; }
// 图形旋转
void Ratote()
{
int NextSharp = sharp;
if (sharp == 6 || sharp == 10 || sharp == 14)
NextSharp -= 3;
else if (sharp == 1 || sharp == 16 || sharp == 18)
--NextSharp;
else
++NextSharp;
if (!Ratote_CD(NextSharp))
{
ClearGraph();
sharp = NextSharp;
ShowGraph();
}
}
// 初始化游戏
void Initialize()
{
system("cls");
AddGraph();
for (bool b : Board)
b = false;
}
// 运行游戏
void run()
{
Initialize();
int t = 0;
while (true)
{
Sleep(1);
if (t++ > 50)
{
Drop();
t = 0;
}
if (kbhit())
{
int ch = getch();
if (ch == 224)
{
switch (getch())
{
case 72: // 按下键盘上键
Ratote(); // 旋转图形
break;
case 80: // 按下键盘下键
Drop(); // 下降图形
break;
case 75: // 按下键盘左键
Left(); // 左移图形
break;
case 77: // 按下键盘右键
Right(); // 右移图形
break;
}
}
else if (ch == 32)
Pause();
}
}
}
int main()
{
SetConsole("俄罗斯方块", 40, 27, "80");
srand((int)time(0));
run();
}
左移、右移、下降、旋转,4个基本功能就完成了,且达到了预期的效果。
4. 碰撞检测
最重要也是最难实现的模块就是碰撞检测了。
1. 碰撞检测:移动
下降、左移、右移的碰撞检测代码差不多。我们以下降为例。
不管哪种碰撞检测,我们都需要判断图形中每一个小方块下一个位置是否有其他小方块(不能是图形本身的),另外还需要判断小方块是否会越界。在第一类碰撞检测的判断中,对于下降来说我们可以只判断该图形最下面的点下降后是否会碰撞到障碍物,因此大致上是两个步骤,先判断该小方块是否是最下面的点,再判断该点下一位置是否有小方块。不难发现对于是否越界的判断我们可以给出更加通用的判断条件,但是在整个程序中我们只有4处越界判断,所以我们可以只写每种情况对应的越界判断条件,以节省多余判断所带来的额外开销,复用性降低地同时提高了性能。
// 碰撞检测:图形下降
bool Drop_CD()
{
for (int i = 0; i < 4; ++i)
{
bool Point_CD = true; // 检测该点是否为该行下边的像素点
Point Pixel0 = SiteChange(From[sharp][i]);
// 是否越界
if (Pixel0.y > BH - 2)
return true;
// 是否碰到障碍物
for (int j = 0; j < 4; ++j)
{
Point Pixel1 = SiteChange(From[sharp][j]);
if (Pixel1.x == Pixel0.x && Pixel1.y > Pixel0.y)
{
Point_CD = false;
break;
}
}
if (Point_CD && Board[Pixel0.x + (Pixel0.y + 1) * BW])
return true;
}
return false;
}
2. 碰撞检测:旋转
旋转碰撞检测其实和移动的碰撞检测也差不多,唯一的差别是这里的Pixel0是旋转之后的小方块,而之前的Pixel0是移动前的小方块。相当于这里是先操作再看行不行,移动那里是先看行不行再操作,本质上是一样的。
// 碰撞检测:图形旋转
bool Ratote_CD(int NextSharp)
{
for (int i = 0; i < 4; ++i)
{
bool Point_CD = true; // 检测该点下一位置是否无其他像素点
Point Pixel0 = SiteChange(From[NextSharp][i]);
// 是否越界
if (Pixel0.x < 0 || BW - 1 < Pixel0.x || Pixel0.y > BH - 1)
return true;
// 是否碰到障碍物
for (int j = 0; j < 4; ++j)
{
Point Pixel1 = SiteChange(From[sharp][j]);
if (Pixel1.x == Pixel0.x && Pixel1.y == Pixel0.y)
{
Point_CD = false;
break;
}
}
if (Point_CD && Board[Pixel0.x + Pixel0.y * BW])
return true;
}
return false;
}
5. 消除整行
从最下面那一行开始扫描,如果发现满行就消除该行并重新扫描该行,否则就扫描上一行,直到扫描完所有行且没发现满行时退出函数。有一个细节是:因为我们要将消除的那一行上面的所有行下移,这里我们从右向左、从下到上依次移动就好;如果是从左上到右下遍历,则会覆盖还未操作的小方块,我之前为了解决这个问题,将所有符合条件的移动后的坐标缓存下来之后再取出,那样的话需要遍历2次。另外需要注意的是,消除该行后一定要退回到该行重新扫描。否则,该函数1次就只能消除1行。
// 整行消除
void LineRemove()
{
int JudgeY = BH - 1; // 当前判断的行数
while (JudgeY >= 0)
{
int cnt = 0;
for (int i = 0; i < BW; ++i)
if (Board[BW * JudgeY + i])
++cnt;
if (cnt != BW) // 未满行, 判断上一行
--JudgeY;
else // 满行消除
{
// 移除JudgeY这一行
for (int i = 0; i < BW; ++i)
{
Board[i + JudgeY * BW] = false;
FillStr(i, JudgeY, " ");
}
// 将JudgeY这行上方的所有方块下移一行
for (int i = BW * JudgeY - 1; i >= 0; --i)
if (Board[i])
{
Board[i] = false;
FillStr(i % BW, i / BW, " ");
Board[i + BW] = true;
FillStr(i % BW, i / BW + 1, "■");
}
}
}
}
6. 游戏结束
判断游戏结束的代码也十分巧妙。仅仅是我们在之前的函数里添加了一个if()语句。
// 显示、清除图形的辅助函数
void ShowOrClear(bool exist, const std::string &fill)
{
for (int i = 0; i < 4; ++i)
{
Point Pixel = SiteChange(From[sharp][i]);
if (Board[Pixel.x + Pixel.y * BW] == exist) // 该处已存在图形, 游戏结束
{
SetPos(BW - 3 >> 1, BH >> 1);
std::cout << "Game Over!";
Pause();
run();
}
Board[Pixel.x + Pixel.y * BW] = exist;
FillStr(Pixel.x, Pixel.y, fill);
}
}
不难理解,这里就是判断将要显示的位置是否已经存在图形,如果过存在则游戏结束。为什么这样可行呢,回想一下之前写的显示和清除语句,它们都包含在4个基本操作的函数里面了,也就是说我们已经进行了严格的碰撞检测,出现使上述if()成立的情况只有一种,那就是旧图形触底后发现没有添加新图形的空间,有就是俄罗斯方块中的游戏结束。
7. 完整代码
#include "../Engine/BrickEngine.h"
const int BW(20); // BW表示场景宽度
const int BH(27); // BH表示场景高度
const int HomeX(BW - 1 >> 1); // HomeX表示图形初始横坐标
const int HomeY(1); // HomeY表示图形初始纵坐标
bool Board[BW * BH]; // Board[x + y * BW]表示(x, y)处是否为小方块
int sharp; // 图形的形状(形状随机产生)
int x, y; // 图形的横纵坐标
// 声明这19种图形
// 图形4个像素点的声明顺序无关紧要
// 但每种图形必须按旋转变换的顺序依次声明
Point From[19][4] = {
// 条形
{{0, 0}, {1, 0}, {2, 0}, {3, 0}},
{{0, 0}, {0, 1}, {0, 2}, {0, 3}},
// 方形
{{0, 0}, {1, 0}, {0, 1}, {1, 1}},
// L形
{{0, 0}, {0, 1}, {0, 2}, {1, 2}},
{{2, 0}, {1, 0}, {0, 0}, {0, 1}},
{{2, 2}, {2, 1}, {2, 0}, {1, 0}},
{{0, 2}, {1, 2}, {2, 2}, {2, 1}},
// 镜像L形
{{2, 0}, {2, 1}, {2, 2}, {1, 2}},
{{2, 2}, {1, 2}, {0, 2}, {0, 1}},
{{0, 2}, {0, 1}, {0, 0}, {1, 0}},
{{0, 0}, {1, 0}, {2, 0}, {2, 1}},
// T型
{{1, 1}, {0, 2}, {1, 2}, {2, 2}},
{{1, 1}, {0, 0}, {0, 1}, {0, 2}},
{{1, 1}, {2, 0}, {1, 0}, {0, 0}},
{{1, 1}, {2, 0}, {2, 1}, {2, 2}},
// 闪电形
{{0, 0}, {0, 1}, {1, 1}, {1, 2}},
{{2, 0}, {1, 0}, {1, 1}, {0, 1}},
// 镜像闪电形
{{0, 0}, {1, 0}, {1, 1}, {2, 1}},
{{1, 0}, {1, 1}, {0, 1}, {0, 2}}};
void run();
// 相对坐标转绝对坐标
Point SiteChange(Point p)
{
return {x + p.x, y + p.y};
}
// 显示、清除图形的辅助函数
void ShowOrClear(bool exist, const std::string &fill)
{
for (int i = 0; i < 4; ++i)
{
Point Pixel = SiteChange(From[sharp][i]);
if (Board[Pixel.x + Pixel.y * BW] == exist) // 该处已存在图形, 游戏结束
{
SetPos(BW - 3 >> 1, BH >> 1);
std::cout << "Game Over!";
Pause();
run();
}
Board[Pixel.x + Pixel.y * BW] = exist;
FillStr(Pixel.x, Pixel.y, fill);
}
}
// 显示图形
void ShowGraph() { ShowOrClear(true, "■"); }
// 清除图形
void ClearGraph() { ShowOrClear(false, " "); }
// 添加图形
void AddGraph()
{
sharp = rand() % 19;
x = HomeX;
y = HomeY;
ShowGraph();
}
// 整行消除
void LineRemove()
{
int JudgeY = BH - 1; // 当前判断的行数
while (JudgeY >= 0)
{
int cnt = 0;
for (int i = 0; i < BW; ++i)
if (Board[BW * JudgeY + i])
++cnt;
if (cnt != BW) // 未满行, 判断上一行
--JudgeY;
else // 满行消除
{
// 移除JudgeY这一行
for (int i = 0; i < BW; ++i)
{
Board[i + JudgeY * BW] = false;
FillStr(i, JudgeY, " ");
}
// 将JudgeY这行上方的所有方块下移一行
for (int i = BW * JudgeY - 1; i >= 0; --i)
if (Board[i])
{
Board[i] = false;
FillStr(i % BW, i / BW, " ");
Board[i + BW] = true;
FillStr(i % BW, i / BW + 1, "■");
}
}
}
}
// 碰撞检测:图形下降
bool Drop_CD()
{
for (int i = 0; i < 4; ++i)
{
bool Point_CD = true; // 检测该点是否为该行下边的像素点
Point Pixel0 = SiteChange(From[sharp][i]);
// 是否越界
if (Pixel0.y > BH - 2)
return true;
// 是否碰到障碍物
for (int j = 0; j < 4; ++j)
{
Point Pixel1 = SiteChange(From[sharp][j]);
if (Pixel1.x == Pixel0.x && Pixel1.y > Pixel0.y)
{
Point_CD = false;
break;
}
}
if (Point_CD && Board[Pixel0.x + (Pixel0.y + 1) * BW])
return true;
}
return false;
}
// 图形下降
void Drop()
{
if (!Drop_CD())
{
ClearGraph();
++y;
ShowGraph();
}
else
{
LineRemove();
AddGraph();
}
}
// 碰撞检测:图形左移
bool Left_CD()
{
for (int i = 0; i < 4; ++i)
{
bool Point_CD = true; // 检测该点是否为该行最左边的像素点
Point Pixel0 = SiteChange(From[sharp][i]);
// 是否越界
if (Pixel0.x < 1)
return true;
// 是否碰到障碍物
for (int j = 0; j < 4; ++j)
{
Point Pixel1 = SiteChange(From[sharp][j]);
if (Pixel1.y == Pixel0.y && Pixel1.x < Pixel0.x)
{
Point_CD = false;
break;
}
}
if (Point_CD && Board[Pixel0.x - 1 + Pixel0.y * BW])
return true;
}
return false;
}
// 图形左移
void Left()
{
if (!Left_CD())
{
ClearGraph();
--x;
ShowGraph();
}
}
// 碰撞检测:图形右移
bool Right_CD()
{
for (int i = 0; i < 4; ++i)
{
bool Point_CD = true; // 检测该点是否为该行最右边的像素点
Point Pixel0 = SiteChange(From[sharp][i]);
// 是否越界
if (Pixel0.x > BW - 2)
return true;
// 是否碰到障碍物
for (int j = 0; j < 4; ++j)
{
Point Pixel1 = SiteChange(From[sharp][j]);
if (Pixel1.y == Pixel0.y && Pixel1.x > Pixel0.x)
{
Point_CD = false;
break;
}
}
if (Point_CD && Board[Pixel0.x + 1 + Pixel0.y * BW])
return true;
}
return false;
}
// 图形右移
void Right()
{
if (!Right_CD())
{
ClearGraph();
++x;
ShowGraph();
}
}
// 碰撞检测:图形旋转
bool Ratote_CD(int NextSharp)
{
for (int i = 0; i < 4; ++i)
{
bool Point_CD = true; // 检测该点下一位置是否无其他像素点
Point Pixel0 = SiteChange(From[NextSharp][i]);
// 是否越界
if (Pixel0.x < 0 || BW - 1 < Pixel0.x || Pixel0.y > BH - 1)
return true;
// 是否碰到障碍物
for (int j = 0; j < 4; ++j)
{
Point Pixel1 = SiteChange(From[sharp][j]);
if (Pixel1.x == Pixel0.x && Pixel1.y == Pixel0.y)
{
Point_CD = false;
break;
}
}
if (Point_CD && Board[Pixel0.x + Pixel0.y * BW])
return true;
}
return false;
}
// 图形旋转
void Ratote()
{
int NextSharp = sharp;
if (sharp == 6 || sharp == 10 || sharp == 14)
NextSharp -= 3;
else if (sharp == 1 || sharp == 16 || sharp == 18)
--NextSharp;
else
++NextSharp;
if (!Ratote_CD(NextSharp))
{
ClearGraph();
sharp = NextSharp;
ShowGraph();
}
}
// 初始化游戏
void Initialize()
{
system("cls");
AddGraph();
for (bool b : Board)
b = false;
}
// 运行游戏
void run()
{
Initialize();
int t = 0;
while (true)
{
Sleep(1);
if (t++ > 50)
{
Drop();
t = 0;
}
if (kbhit())
{
int ch = getch();
if (ch == 224)
{
switch (getch())
{
case 72: // 按下键盘上键
Ratote(); // 旋转图形
break;
case 80: // 按下键盘下键
Drop(); // 下降图形
break;
case 75: // 按下键盘左键
Left(); // 左移图形
break;
case 77: // 按下键盘右键
Right(); // 右移图形
break;
}
}
else if (ch == 32)
Pause();
}
}
}
int main()
{
SetConsole("俄罗斯方块", 40, 27, "80");
srand((int)time(0));
run();
}
二. 俄罗斯方块简易版的升级
1. 新增属性
① 游戏得分score
② 图形下降速度speed
③ 场景左上角的横纵坐标BX、BY
const int BX(1); // BX表示场景的横坐标
const int BY(0); // BY表示场景的纵坐标
int score; // 游戏得分
int speed; // 图形下落的速度
2. 更改初始化函数
给场景加上边框;在场景右边显示速度和分数。
// 初始化场景
FillRec(BX - 1, BY, 1, BH, "│");
FillRec(BX + BW, BY, 1, BH, "┃");
FillRec(BX, BY + BH, BW * 2 - 1, 1, "━");
FillStr(BX - 1, BY + BH, "╰");
FillStr(BX + BW, BY + BH, "╯");
FillStr(BW + 2, 3, " SCORE");
std::cout << std::setw(4);
FillStr(BW + 3, 4, std::to_string(score));
FillStr(BW + 2, 7, " SPEED");
std::cout << std::setw(2);
FillStr(BW + 4, 8, std::to_string(speed));
3. 更改消除整行函数
更改填充函数的参数,使场景移动到指定的位置;添加更新玩家得分操作。
// 移除JudgeY这一行
for (int i = 0; i < BW; ++i)
{
Board[i + JudgeY * BW] = false;
FillStr(i + BX, JudgeY + BY, " ");
}
// 将JudgeY这行上方的所有方块下移一行
for (int i = BW * JudgeY - 1; i >= 0; --i)
if (Board[i])
{
Board[i] = false;
FillStr(i % BW + BX, i / BW + BY, " ");
Board[i + BW] = true;
FillStr(i % BW + BX, i / BW + 1 + BY, "■");
}
++score;
std::cout << std::setw(4);
FillStr(BW + 3, 4, std::to_string(score));
std::cout << std::setw(2);
FillStr(BW + 4, 8, std::to_string(speed));
4. 更改显示、清除图形辅助函数
更改填充函数的参数,使场景移动到指定的位置。
FillStr(Pixel.x + BX, Pixel.y + BY, fill);
5. 更改控制速度的语句
稍作修改使得score决定speed,speed影响方块下降的速度。
speed = score / 5 + 1;
if (t++ > (30 - speed))
{
Drop();
t = 0;
}
6. 完整代码
#include "../Engine/BrickEngine.h"
const int BW(14); // BW表示场景宽度
const int BH(24); // BH表示场景高度
const int BX(1); // BX表示场景的横坐标
const int BY(0); // BY表示场景的纵坐标
const int HomeX(BW - 1 >> 1); // HomeX表示图形初始横坐标
const int HomeY(1); // HomeY表示图形初始纵坐标
bool Board[BW * BH]; // Board[x + y * BW]表示(x, y)处是否为小方块
int sharp; // 图形的形状(形状随机产生)
int x, y; // 图形的横纵坐标
int score; // 游戏得分
int speed; // 图形下落的速度
// 声明这19种图形
// 图形4个像素点的声明顺序无关紧要
// 但每种图形必须按旋转变换的顺序依次声明
Point From[19][4] = {
// 条形
{{0, 0}, {1, 0}, {2, 0}, {3, 0}},
{{0, 0}, {0, 1}, {0, 2}, {0, 3}},
// 方形
{{0, 0}, {1, 0}, {0, 1}, {1, 1}},
// L形
{{0, 0}, {0, 1}, {0, 2}, {1, 2}},
{{2, 0}, {1, 0}, {0, 0}, {0, 1}},
{{2, 2}, {2, 1}, {2, 0}, {1, 0}},
{{0, 2}, {1, 2}, {2, 2}, {2, 1}},
// 镜像L形
{{2, 0}, {2, 1}, {2, 2}, {1, 2}},
{{2, 2}, {1, 2}, {0, 2}, {0, 1}},
{{0, 2}, {0, 1}, {0, 0}, {1, 0}},
{{0, 0}, {1, 0}, {2, 0}, {2, 1}},
// T型
{{1, 1}, {0, 2}, {1, 2}, {2, 2}},
{{1, 1}, {0, 0}, {0, 1}, {0, 2}},
{{1, 1}, {2, 0}, {1, 0}, {0, 0}},
{{1, 1}, {2, 0}, {2, 1}, {2, 2}},
// 闪电形
{{0, 0}, {0, 1}, {1, 1}, {1, 2}},
{{2, 0}, {1, 0}, {1, 1}, {0, 1}},
// 镜像闪电形
{{0, 0}, {1, 0}, {1, 1}, {2, 1}},
{{1, 0}, {1, 1}, {0, 1}, {0, 2}}};
void run();
// 相对坐标转绝对坐标
Point SiteChange(Point p)
{
return {x + p.x, y + p.y};
}
// 显示、清除图形的辅助函数
void ShowOrClear(bool exist, const std::string &fill)
{
for (int i = 0; i < 4; ++i)
{
Point Pixel = SiteChange(From[sharp][i]);
if (Board[Pixel.x + Pixel.y * BW] == exist) // 该处已存在图形, 游戏结束
{
SetPos(7, BH + 1);
std::cout << "Game Over!";
Pause();
run();
}
Board[Pixel.x + Pixel.y * BW] = exist;
FillStr(Pixel.x + BX, Pixel.y + BY, fill);
}
}
// 显示图形
void ShowGraph() { ShowOrClear(true, "■"); }
// 清除图形
void ClearGraph() { ShowOrClear(false, " "); }
// 添加图形
void AddGraph()
{
sharp = rand() % 19;
x = HomeX;
y = HomeY;
ShowGraph();
}
// 整行消除
void LineRemove()
{
int JudgeY = BH - 1; // 当前判断的行数
while (JudgeY >= 0)
{
int cnt = 0;
for (int i = 0; i < BW; ++i)
if (Board[BW * JudgeY + i])
++cnt;
if (cnt != BW) // 未满行, 判断上一行
--JudgeY;
else // 满行消除
{
// 移除JudgeY这一行
for (int i = 0; i < BW; ++i)
{
Board[i + JudgeY * BW] = false;
FillStr(i + BX, JudgeY + BY, " ");
}
// 将JudgeY这行上方的所有方块下移一行
for (int i = BW * JudgeY - 1; i >= 0; --i)
if (Board[i])
{
Board[i] = false;
FillStr(i % BW + BX, i / BW + BY, " ");
Board[i + BW] = true;
FillStr(i % BW + BX, i / BW + 1 + BY, "■");
}
++score;
std::cout << std::setw(4);
FillStr(BW + 3, 4, std::to_string(score));
std::cout << std::setw(2);
FillStr(BW + 4, 8, std::to_string(speed));
}
}
}
// 碰撞检测:图形下降
bool Drop_CD()
{
for (int i = 0; i < 4; ++i)
{
bool Point_CD = true; // 检测该点是否为该行下边的像素点
Point Pixel0 = SiteChange(From[sharp][i]);
// 是否越界
if (Pixel0.y > BH - 2)
return true;
// 是否碰到障碍物
for (int j = 0; j < 4; ++j)
{
Point Pixel1 = SiteChange(From[sharp][j]);
if (Pixel1.x == Pixel0.x && Pixel1.y > Pixel0.y)
{
Point_CD = false;
break;
}
}
if (Point_CD && Board[Pixel0.x + (Pixel0.y + 1) * BW])
return true;
}
return false;
}
// 图形下降
void Drop()
{
if (!Drop_CD())
{
ClearGraph();
++y;
ShowGraph();
}
else
{
LineRemove();
AddGraph();
}
}
// 碰撞检测:图形左移
bool Left_CD()
{
for (int i = 0; i < 4; ++i)
{
bool Point_CD = true; // 检测该点是否为该行最左边的像素点
Point Pixel0 = SiteChange(From[sharp][i]);
// 是否越界
if (Pixel0.x < 1)
return true;
// 是否碰到障碍物
for (int j = 0; j < 4; ++j)
{
Point Pixel1 = SiteChange(From[sharp][j]);
if (Pixel1.y == Pixel0.y && Pixel1.x < Pixel0.x)
{
Point_CD = false;
break;
}
}
if (Point_CD && Board[Pixel0.x - 1 + Pixel0.y * BW])
return true;
}
return false;
}
// 图形左移
void Left()
{
if (!Left_CD())
{
ClearGraph();
--x;
ShowGraph();
}
}
// 碰撞检测:图形右移
bool Right_CD()
{
for (int i = 0; i < 4; ++i)
{
bool Point_CD = true; // 检测该点是否为该行最右边的像素点
Point Pixel0 = SiteChange(From[sharp][i]);
// 是否越界
if (Pixel0.x > BW - 2)
return true;
// 是否碰到障碍物
for (int j = 0; j < 4; ++j)
{
Point Pixel1 = SiteChange(From[sharp][j]);
if (Pixel1.y == Pixel0.y && Pixel1.x > Pixel0.x)
{
Point_CD = false;
break;
}
}
if (Point_CD && Board[Pixel0.x + 1 + Pixel0.y * BW])
return true;
}
return false;
}
// 图形右移
void Right()
{
if (!Right_CD())
{
ClearGraph();
++x;
ShowGraph();
}
}
// 碰撞检测:图形旋转
bool Ratote_CD(int NextSharp)
{
for (int i = 0; i < 4; ++i)
{
bool Point_CD = true; // 检测该点下一位置是否无其他像素点
Point Pixel0 = SiteChange(From[NextSharp][i]);
// 是否越界
if (Pixel0.x < 0 || BW - 1 < Pixel0.x || Pixel0.y > BH - 1)
return true;
// 是否碰到障碍物
for (int j = 0; j < 4; ++j)
{
Point Pixel1 = SiteChange(From[sharp][j]);
if (Pixel1.x == Pixel0.x && Pixel1.y == Pixel0.y)
{
Point_CD = false;
break;
}
}
if (Point_CD && Board[Pixel0.x + Pixel0.y * BW])
return true;
}
return false;
}
// 图形旋转
void Ratote()
{
int NextSharp = sharp;
if (sharp == 6 || sharp == 10 || sharp == 14)
NextSharp -= 3;
else if (sharp == 1 || sharp == 16 || sharp == 18)
--NextSharp;
else
++NextSharp;
if (!Ratote_CD(NextSharp))
{
ClearGraph();
sharp = NextSharp;
ShowGraph();
}
}
// 初始化游戏
void Initialize()
{
system("cls");
AddGraph();
score = 0;
speed = 1;
for (bool b : Board)
b = false;
// 初始化场景
FillRec(BX - 1, BY, 1, BH, "│");
FillRec(BX + BW, BY, 1, BH, "┃");
FillRec(BX, BY + BH, BW * 2 - 1, 1, "━");
FillStr(BX - 1, BY + BH, "╰");
FillStr(BX + BW, BY + BH, "╯");
FillStr(BW + 2, 3, " SCORE");
std::cout << std::setw(4);
FillStr(BW + 3, 4, std::to_string(score));
FillStr(BW + 2, 7, " SPEED");
std::cout << std::setw(2);
FillStr(BW + 4, 8, std::to_string(speed));
}
// 运行游戏
void run()
{
Initialize();
int t = 0;
while (true)
{
// 时间复杂度O(BW * BH * BH)
Sleep(1);
speed = score / 5 + 1;
if (t++ > (30 - speed))
{
Drop();
t = 0;
}
if (kbhit())
{
int ch = getch();
if (ch == 224)
{
switch (getch())
{
case 72: // 按下键盘上键
Ratote(); // 旋转图形
break;
case 80: // 按下键盘下键
Drop(); // 下降图形
break;
case 75: // 按下键盘左键
Left(); // 左移图形
break;
case 77: // 按下键盘右键
Right(); // 右移图形
break;
}
}
else if (ch == 32)
Pause();
}
}
}
int main()
{
SetConsole("俄罗斯方块", 40, 27, "80");
srand((int)time(0));
run();
}