SFML1 俄罗斯方块代码解析

 原始代码呈现:

#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. 另外从游戏体验上,每个新方块总是从游戏窗格的最右侧出现。这个感觉不太好,应该从中间或者合理的随机位置出现比较好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>