Silverlight:手把手教你写俄罗斯方块(四)
public Rect[,] board; //游戏画板
public Rect[,] readyBoard; //准备方块画板
public Block runBlock; //移动中的方块
public Block readyBlock; //准备方块
public event EventHandler GameOver; //游戏结束事件
public GameStatus status; //游戏状态
private DispatcherTimer timer; //计数器
private TextBlock score; //分数版
private bool[,] staticRect; //存储静态方块坐标
private const double speed = 300; //游戏速度
private const int width = 10;
private const int height = 20;
private const int r_width = 4;
private const int r_height = 4;
private Color color = Color.FromArgb(255, 192, 192, 192); //静态方块颜色
由于我们的画板大小是200*400,所以我们用10*20个Rect填充,这里的width和height变量实际上可以看做是横向和纵向Rect的个数。bool类型的数组staticRect用来存储画板中的Rect状态,当为false的时候,表示这一格子没有方块,为true表示已经有静态的方块,然后我们为静态的方块定义一个颜色变量。准备方块画板,是在俄罗斯方块游戏中的下一个要出现的方块的展示画板,我们为这个画板定义大小为4*4,接着我们定义构造函数:
public Control(TextBlock s)
{
this.score = s;
board = new Rect[width, height];
readyBoard = new Rect[r_width, r_height];
staticRect = new bool[width, height];
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
board[i, j] = new Rect(i, j);
}
}
for (int i = 0; i < r_width; i++)
{
for (int j = 0; j < r_height; j++)
{
readyBoard[i, j] = new Rect(i, j);
}
}
timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(speed);
timer.Tick += new EventHandler(timer_Tick);
}
在构造函数中, 我们先接收UI层传来的TextBlock控件,用于记录得分 ,然后用Rect填充2个画板,最后定义定时器,定时器主要是起到方块自动下移的作用。
二.在游戏中俄罗斯方块的产生是随机的,所以我们写一个随机产生俄罗斯方块的方法,
/// <summary>
/// 取得俄罗斯方块
/// </summary>
/// <param name="startX">起始横坐标</param>
/// <param name="startY">起始纵坐标</param>
/// <returns></returns>
private Block GetBlock(int startX, int startY)
{
Random rd = new Random();
int index = rd.Next(0, 7);
switch (index)
{
case 0: return new Block_I(startX, startY);
case 1: return new Block_L(startX, startY);
case 2: return new Block_L2(startX, startY);
case 3: return new Block_O(startX, startY);
case 4: return new Block_T(startX, startY);
case 5: return new Block_Z(startX, startY);
case 6: return new Block_Z2(startX, startY);
default: return new Block_I(startX, startY);
}
}
方块有了,接下来是方块的移动了,但是在移动前我们需要判断方块是否能够移动,所以我们写一个判断的方法:
#region 判断能否移动或变形
/// <summary>
/// 能否移动
/// </summary>
/// <param name="ps">方块</param>
/// <param name="offsetX">偏移x,左偏移为负数</param>
/// <param name="offsetY">偏移y,下偏移为正数</param>
/// <returns></returns>
private bool CanMove(Point[] ps, int offsetX, int offsetY)
{
for (int i = 0; i < 4; i++)
{
int x = (int)ps[i].X + offsetX; //横向移动后的新x轴坐标
int y = (int)ps[i].Y + offsetY; //纵向移动后的新y轴坐标
if (x < 0) return false; //超出左边界
if (x >= width) return false; //超出右边界
if (y < 0) return false; //超出上边界
if (y >= height) return false; //超出下边界
if (staticRect[x, y]) return false; //新坐标已经有方块
}
return true;
}
/// <summary>
/// 能否移动
/// </summary>
/// <param name="offsetX">偏移x,左偏移为负数</param>
/// <param name="offsetY">偏移y,下偏移为正数</param>
/// <returns></returns>
private bool CanMove(int offsetX, int offsetY)
{
for (int i = 0; i < 4; i++)
{
int x = (int)runBlock[i].X + offsetX;
int y = (int)runBlock[i].Y + offsetY;
if (x < 0) return false;
if (x >= width) return false;
if (y < 0) return false;
if (y >= height) return false;
if (staticRect[x, y]) return false;
}
return true;
}
#endregion
原理很简单,就是先取得方块移动或变形后新的坐标,然后通过一系列得判断,最后决定能否操作。关键是如何取得新坐标,方向移动很好判断,但是不同方块变形之后的坐标可能毫无规律,所以我们写一个重载方法,直接获得新坐标的数组。总之,如果传递了point[]参数(该数组大小一定为4,因为一开始我们就规定了所有的俄罗斯方块只可能由4个小方块组成)则根据传递进来的点数组判断,否则就按照runBlock的索引器来判断。
三.有了判断方法,接下来,左右和下移动以及变形方法就好办了(俄罗斯方块可没有上移操作哦):
/// <summary>
/// 左移
/// </summary>
public void MoveLeft()
{
if (status != GameStatus.Play) return;
if (CanMove(-1, 0))
{
runBlock.Hidden(board);
runBlock.posX--;
runBlock.Show(board);
}
}
/// <summary>
/// 右移
/// </summary>
public void MoveRight()
{
if (status != GameStatus.Play) return;
if (CanMove(1, 0))
{
runBlock.Hidden(board);
runBlock.posX++;
runBlock.Show(board);
}
}
/// <summary>
/// 下移
/// </summary>
/// <returns></returns>
public bool MoveDown()
{
if (status != GameStatus.Play) return false;
if (CanMove(0, 1))
{
runBlock.Hidden(board);
runBlock.posY++;
runBlock.Show(board);
return true;
}
return false;
}
/// <summary>
/// 变形
/// </summary>
public void Change()
{
if (status != GameStatus.Play) return;
Point[] vitualPoint = runBlock.GetChangedPoint();
if (CanMove(vitualPoint, 0, 0))
{
runBlock.Hidden(board);
runBlock.Change();
runBlock.Show(board);
}
}
有的俄罗斯方块还拥有Drop效果,即方块直接落到最下端,这个实现效果实现起来并不困难,先停止计时器,然后不断下移,知道不能移动为止,这也就是为什么前面MoveDown方法要返回bool值的原因了。代码如下:
public void Drop()
{
if (status != GameStatus.Play) return;
timer.Stop();
while (MoveDown()) ;
timer.Start();
}
好了,在这之前,我们再定义一个枚举变量,如果游戏并不是在进行中,那么所有的操作都是无效的:
public enum GameStatus
{
Ready,
Play,
Pause,
Over
}
共有4种状态:准备就绪,进行中,暂定,结束。
四.在俄罗斯方块游戏中,当方块落到最底部,或者碰到下方的方块时,这个方块就会停住并归为静态方块,如果有满行的情况,会消除这一行,然后准备方块变成了下一个移动方块,这是俄罗斯方块的游戏规则,基于这个逻辑,我们用下面的方法实现:
/// <summary>
/// 检查方块是否到底
/// </summary>
/// <param name="runBlock"></param>
public void CheckAndOverBlock()
{
if (status != GameStatus.Play) return;
bool over = false;
for (int i = 0; i < 4; i++)
{
int x = (int)runBlock[i].X;
int y = (int)runBlock[i].Y;
if (y >= height - 1)//是否超出下边界,已经到达最低端
{
over = true;
break;
}
if (staticRect[x, y + 1])//方块下面是否已存在别的方块
{
over = true;
break;
}
}
if (over)//如果确定当前方块已经结束
{
for (int i = 0; i < 4; i++)//把当前砖块归入静态方块类
{
staticRect[(int)runBlock[i].X, (int)runBlock[i].Y] = true;
}
//检查是否有满行的情况,如果有则删除满行
CheckFullAndDelRow();
//重新绘制背景
this.PaintBack();
//产生新方块
runBlock = readyBlock;
runBlock.posX = 4;
runBlock.posY = 0;
for (int i = 0; i < 4; i++)//游戏结束(当产生的新方块的位置已经包含有静态方块)
{
if (staticRect[(int)runBlock[i].X, (int)runBlock[i].Y])
{
this.OnGameOver(null);
return;
}
}
runBlock.Show(board);
//绘制准备方块背景
this.PaintReadyBack();
readyBlock = GetBlock(1, 1);
readyBlock.Show(readyBoard);
}
}
在这个方法中,我们有使用了如下几个私有的方法,其中最重要的是消除满行的方法,代码如下:
/// <summary>
/// 检查是否满行,如果满行则重画
/// </summary>
/// <param name="runBlock"></param>
private void CheckFullAndDelRow()
{
int low = (int)runBlock[0].Y;
int high = (int)runBlock[0].Y;
for (int i = 0; i < 4; i++)//获得该方块在画板中的最大和最小纵坐标
{
int y = (int)runBlock[i].Y;
if (y < low)
low = y;
if (y > high)
high = y;
}
for (int j = low; j <= high; j++)
{
bool rowfull = true;//判断是否为满行
for (int i = 0; i < width; i++)
{
if (staticRect[i, j] == false)
{
rowfull = false;
break;
}
}
if (rowfull)
{
this.score.Text = (Int32.Parse(this.score.Text) + 1).ToString();
for (int k = j; k > 0; k--)
{
for (int i = 0; i < 10; i++)
{
staticRect[i, k] = staticRect[i, k - 1];
}
}
for (int i = 0; i < width; i++)//清除第0行
{
staticRect[i, 0] = false;
}
}
}
}
以及重新绘制背景与重新绘制准备方块背景的方法,之所以要重新绘制是为了避免新方块与旧方块产生重叠效果,具体代码如下:
/// <summary>
/// 刷新背景
/// </summary>
private void PaintBack()
{
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
if (staticRect[i, j])
{
board[i, j].Color = color;
}
else board[i, j].Color = null;
}
}
}
private void PaintReadyBack()
{
foreach (Rect r in readyBoard)
{
r.Color = null;
}
}
五.那么我们前面提到的定时器效果就是不断检查方块是否结束,并且不断让方块下移:
void timer_Tick(object sender, EventArgs e)
{
CheckAndOverBlock();
MoveDown();
}
到此为止,我们已经完成了大部分逻辑,在下一节中,我将介绍逻辑层与UI层的交互,敬请关注。

浙公网安备 33010602011771号