原始代码呈现:
#include <SFML/Graphics.hpp>
#include <time.h>
using namespace sf;
const int M = 20;
const int N = 10;
int field[M][N] = {0};
struct Point
{int x,y;} a[4], b[4];
int figures[7][4] =
{
1,3,5,7, // I
2,4,5,7, // Z
3,5,4,6, // S
3,5,4,7, // T
2,3,5,7, // L
3,5,7,6, // J
2,3,4,5, // O
};
bool check()
{
for (int i=0;i<4;i++)
if (a[i].x<0 || a[i].x>=N || a[i].y>=M) return 0;
else if (field[a[i].y][a[i].x]) return 0;
return 1;
};
int main()
{
srand(time(0));
RenderWindow window(VideoMode(320, 480), "The Game!");
Texture t1,t2,t3;
t1.loadFromFile("images/tiles.png");
t2.loadFromFile("images/background.png");
t3.loadFromFile("images/frame.png");
Sprite s(t1), background(t2), frame(t3);
int dx=0; bool rotate=0; int colorNum=1;
float timer=0,delay=0.3;
Clock clock;
while (window.isOpen())
{
float time = clock.getElapsedTime().asSeconds();
clock.restart();
timer+=time;
Event e;
while (window.pollEvent(e))
{
if (e.type == Event::Closed)
window.close();
if (e.type == Event::KeyPressed)
if (e.key.code==Keyboard::Up) rotate=true;
else if (e.key.code==Keyboard::Left) dx=-1;
else if (e.key.code==Keyboard::Right) dx=1;
}
if (Keyboard::isKeyPressed(Keyboard::Down)) delay=0.05;
<- Move -> ///
for (int i=0;i<4;i++) { b[i]=a[i]; a[i].x+=dx; }
if (!check()) for (int i=0;i<4;i++) a[i]=b[i];
//Rotate//
if (rotate)
{
Point p = a[1]; //center of rotation
for (int i=0;i<4;i++)
{
int x = a[i].y-p.y;
int y = a[i].x-p.x;
a[i].x = p.x - x;
a[i].y = p.y + y;
}
if (!check()) for (int i=0;i<4;i++) a[i]=b[i];
}
///Tick//
if (timer>delay)
{
for (int i=0;i<4;i++) { b[i]=a[i]; a[i].y+=1; }
if (!check())
{
for (int i=0;i<4;i++) field[b[i].y][b[i].x]=colorNum;
colorNum=1+rand()%7;
int n=rand()%7;
for (int i=0;i<4;i++)
{
a[i].x = figures[n][i] % 2;
a[i].y = figures[n][i] / 2;
}
}
timer=0;
}
///check lines//
int k=M-1;
for (int i=M-1;i>0;i--)
{
int count=0;
for (int j=0;j<N;j++)
{
if (field[i][j]) count++;
field[k][j]=field[i][j];
}
if (count<N) k--;
}
dx=0; rotate=0; delay=0.3;
/draw//
window.clear(Color::White);
window.draw(background);
for (int i=0;i<M;i++)
for (int j=0;j<N;j++)
{
if (field[i][j]==0) continue;
s.setTextureRect(IntRect(field[i][j]*18,0,18,18));
s.setPosition(j*18,i*18);
s.move(28,31); //offset
window.draw(s);
}
for (int i=0;i<4;i++)
{
s.setTextureRect(IntRect(colorNum*18,0,18,18));
s.setPosition(a[i].x*18,a[i].y*18);
s.move(28,31); //offset
window.draw(s);
}
window.draw(frame);
window.display();
}
return 0;
}
简单分析:
1. 首先是各种初始化变量的含义
const int M = 20;//行数
const int N = 10;//列数
//游戏共有20行,每行10个格子
//随着游戏进行,这些格子里面可能有方块,可能没有
//0表示当前格子没有方块
//除0以外其它的数字表示这里应该有方格,且方格的样子是通过这个数字计算出来的。
int field[M][N] = { 0 };//初始化
//数组 a[i]是方块经过一次变化后的新状态(例如经过平移,下落,旋转之后的状态)
//b[i]是a[i]变化前的状态,相当于是a[i]的备份。
//一旦经过变化的a[i]是不允许的形态,则要借助b[i]恢复到变化前的样子
Vector2i a[4], b[4];
int figures[7][4] = //方块的初始形状
{
1,3,5,7, // I
2,4,5,7, // Z
3,5,4,6, // S
3,5,4,7, // T
2,3,5,7, // L
3,5,7,6, // J
2,3,4,5, // O
};
2.check方法解读:
/*检测a[i]记录的方块形态是否可行
0:表示不可行
有四种情况是不可行的:
i. 横向超出了左边界,
ii. 横向超出了右边界,
iii. 纵向越过了下边界,
iv. 新形态要使用的方格中已有其它方块
1:表示可行
*/
bool check()
{
for (int i = 0; i < 4; i++)
if (a[i].x < 0 || a[i].x >= N || a[i].y >= M) return 0;
else if (field[a[i].y][a[i].x]) return 0;
return 1;
};
3. 进入循环后,首先看事件获取部分的代码
Event e;
/*
按照俄罗斯方块游戏的按键习惯
一般左键、右键、上键习惯性都是一下一下按
所以采用window.pollEvent的方式来获取
而下键一般都是按住不动
所以可以采用Keyboard::isKeyPressed来获取
*/
while (window.pollEvent(e))
{
if (e.type == Event::Closed)
window.close();
if (e.type == Event::KeyPressed)
if (e.key.code == Keyboard::Up) rotate = true;
else if (e.key.code == Keyboard::Left) dx = -1;
else if (e.key.code == Keyboard::Right) dx = 1;
}
//按住下键不松手
if (Keyboard::isKeyPressed(Keyboard::Down)) delay = 0.05;
4.随后是重头戏,update部分的内容。update主要分为四部分:方块的左右移动,方块的旋转,方块的下落,和整行的检测,先看关于方块左右移动的处理:
/*
b[i]是a[i]的备份
如果一旦a[i]表示的方块新状态不能通过check的检测
那么a[i]通过b[i]恢复到原状
*/
for (int i = 0; i < 4; i++) { b[i] = a[i]; a[i].x += dx; }
//如果check的返回值为0(0是false)
if (!check()) for (int i = 0; i < 4; i++) a[i] = b[i];
然后是方块的旋转:
/*
游戏的旋转采用顺时针旋转45度的方式
方块的旋转中心点是a[1]
无论旋转多少次a[1]的(x,y)始终不会改变
而a[0],a[2],a[3]的(x,y)会产生有规律的变化
*/
if (rotate)//顺时针45度 组成方块的4个tile会发生x,y的互换变化
{
//方块的a[1]TILE是核心方块,无论怎么rotate,它不会变
Vector2i p = a[1]; //center of rotation
//组成方块的每个tile(x,y)的变化规律,有点像旋转后,y变x,而x变为y
for (int i = 0; i < 4; i++)
{
int x = a[i].y - p.y;
int y = a[i].x - p.x;
a[i].x = p.x - x;
a[i].y = p.y + y;
}
//a[i]如果无法通过check,就只能继续保持原状
if (!check()) for (int i = 0; i < 4; i++) a[i] = b[i];
}
方块的下落:
if (timer > delay)//下落是程序根据时间控制的(下键会缩短方块下落的delay)
{
for (int i = 0; i < 4; i++) { b[i] = a[i]; a[i].y += 1; }
if (!check())//下落的时候如果没有通过check,只可能是触底了或者碰到field[][]已有内容了
{
//那么此时方块的四个tile就是field中的内容了
//修改field[][]的数值,记录该方块四个tile的颜色
for (int i = 0; i < 4; i++) field[b[i].y][b[i].x] = colorNum;
//开始生成下一个新的方块
colorNum = 1 + rand() % 7;
int n = rand() % 7;
for (int i = 0; i < 4; i++)
{
//根据变量figures中记载的数字,生成新方块的4个tile的位置情况
a[i].x = figures[n][i] % 2;//1 1 0 1
a[i].y = figures[n][i] / 2;//1 2 2 3
}
}
//游戏计时归零,等待下一次累计到delay,新方块下落
timer = 0;
}
检测现在在游戏方格中是否有整行的方块出现了,如果出现要想办法消除:
//从下往上查是否有满行的
//M的设定值是20,那么最后一行就是19
int k = M - 1;
for (int i = M - 1; i > 0; i--)
{
int count = 0;
for (int j = 0; j < N; j++)
{
//统计这一行field[i][j]不为0的个数
if (field[i][j]) count++;
//最精髓的一行在这里!
//我们假设现在游戏中,随着这次方块下落,18行19行满了应该消除
//现在是第一次循环,k为19,i为19
//将第19行内容复制到了第19行
//因为19行满行,所以不会k--,第二次循环
//k为19,i为18
//这样就将第18行的内容复制到了第19行
//因为第18行满行,所以不会k--,开始第三次循环
//k为19,i为17
//这样就将第17行的内容复制到了第19行
//按照我们的假定,第17行不是满行,所以会k--,k变为18后,开始第三次循环
//k为18,i为16
//这样就将第16行的内容复制到了第18行
//依次类推
//在后面执行绘制环节的时候,会发现要绘制的最后一行是这次整行检查前第17行
field[k][j] = field[i][j];
}
if (count < N) k--;//不满行的情况下k--
}
5. 最后绘制环节唯一要注意的就是偏移(28,31)
for (int i = 0; i < M; i++)
for (int j = 0; j < N; j++)
{
if (field[i][j] == 0) continue;
//一个色块18*18
s.setTextureRect(IntRect(field[i][j] * 18, 0, 18, 18));
s.setPosition(j * 18, i * 18);
//offset(有背景,所以方块并不是从(0,0)开始出现的。
//以(0,0)计算好位置后,偏移(28,31)真正绘制)
s.move(28, 31);
window.draw(s);
}
通过以上的分析,我们就能发现目前代码中存在的纰漏:
1. 绘制方块时,组成方块的四个tile的位置是记录在a[i]中的。但是a[i]初始化之后,第一次绘制之前,并没有根据figures中的预定内容进行四个tile的位置设置。所以,第一个绘制出来的图形是四个tile重叠在一起的一个“小方块”。
解决方法就是在while总线启动前,为方块做一次正式的初始化。
colorNum = 1 + rand() % 7;
int n = rand() % 7;
for (int i = 0; i < 4; i++)
{
a[i].x = figures[n][i] % 2;
a[i].y = figures[n][i] / 2;
}
while (window.isOpen())
....
2.显然这个游戏没有结束机制。根据update中的整行消除机制我们可以推测出游戏的结束机制。按照现在的整行消除机制,其实隐含了一个条件,就是至少要有一个全空的行才可以(也就是至少第0行,即游戏窗格的最上面一行要保持全空才可以),否则是无法实现下移效果的。换句话说,为了避免这个bug的出现,一旦第0行出现了不全空,就意味着游戏应该结束了。
解决方法就是在update时,加入检测对游戏窗格第一行的内容检测机制:
for (int i = 0; i < N; i++)
{
if (field[0][i])
{
//因为没有适当的Texture和字体
//所以这里就粗暴的关闭游戏窗,并在控制台输出游戏结束的提示
window.close();
printf("Game Over");
}
}
3. 另外从游戏体验上,每个新方块总是从游戏窗格的最右侧出现。这个感觉不太好,应该从中间或者合理的随机位置出现比较好。