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层的交互,敬请关注。