平时玩的游戏都是人家制作的,今天我们自已实现一款从小玩到大的游戏,俄罗斯方块,感受下自己完成的不一样的乐趣,游戏截图:
先构思下制作该款游戏需要解决的问题:
1、如何创建各种形状的方块。
2、方块在游戏区域的定位。
4、方块在游戏中形态的改变。
3、方块在游戏区域的移动。
5、方块填满整行后消除以及未消除的方块下移。
带着这些问题来制作我们的游戏。
首先解决方块的创建问题。
游戏中存在各种形态的方块(T、直角、直线、Z形、正方形或者其它任意形状),无论它的最终形态是什么,它都是由一个个的小正方形组成,因此,可以先对这个小正方形进行设计。
在Silverlight中创建一个控件,只包含一个矩形,命名为“SingleBlock”作为基准图形:
<Rectangle x:Name="rectBlock" Stroke="Black" Width="20" Height="20" RadiusX="2" RadiusY="2"></Rectangle>
之后为控件定义大小,这里是:20 * 20。有了它,以T形为例创建出一个方块图形,取出4个基准图形,将每个方块放入不同的位置,这样就形成了T形。
修改基准图形,为其增加位置信息(平面坐标系):
public static DependencyProperty AbsolutePositionProperty = DependencyProperty.Register("AbsolutePosition", typeof(Point), typeof(SingleBlock), new PropertyMetadata(new Point(0, 0)));
/// <summary>
/// 方块绝对位置
/// </summary>
public Point AbsolutePosition
{
get
{
return (Point)GetValue(AbsolutePositionProperty);
}
set
{
SetValue(AbsolutePositionProperty, value);
}
}
创建一个T形方块对象,取出4个基准图形:
public class TBlock : DependencyObject
{
public TBlock()
{
//初始化图形
SingleBlock blocks = new SingleBlock[4];
//设置元方块坐标
blocks[0].AbsolutePosition = new Point(0, 0);
blocks[1].AbsolutePosition = new Point(-1, 0);
blocks[2].AbsolutePosition = new Point(0, -1);
blocks[3].AbsolutePosition = new Point(1, 0);
//设置元方块显示位置
for (int i = 0; i < blocks.Length; i++)
{
blocks[i].SetValue(Canvas.LeftProperty, blocks[i].AbsolutePosition.X * blocks[i].Width);
blocks[i].SetValue(Canvas.TopProperty, blocks[i].AbsolutePosition.Y * blocks[i].Height);
}
}
}
这样,一个T形方块就生成了,调用后会发现它出现在画面中左上角的位置。如果要让它出现在画面中间,这时就该解决第二个问题了。
先了解一个概念,游戏区域就是一个坐标系,它是以左上角为原点(0,0),方块在游戏中出现的位置指的就是相对于原点出现所处的位置。
这里出现了另一个坐标信息,那就是方块的相对位置,它与绝对位置不同,绝对位置是相对于基准图形而言其原始坐标,而相对坐标是针对于T形方块的。
修改TBlock对象,为其增加相对坐标信息:
public static DependencyProperty RelativelyPositionProperty = DependencyProperty.Register("RelativelyPosition", typeof(Point), typeof(BlockGeometry), new PropertyMetadata(new Point(0, 0)));
/// <summary>
/// 图形相对位置
/// </summary>
public Point RelativelyPosition
{
get
{
return (Point)GetValue(RelativelyPositionProperty);
}
set
{
SetValue(RelativelyPositionProperty, value);
}
}
改变下相对位置,让T形出现在游戏区域的中间:
this.RelativelyPosition = new Point(4, 0);
/// <summary>
/// 设置图形出现位置
/// </summary>
public void SetPosition()
{
for (int i = 0; i < blocks.Length; i++)
{
blocks[i].SetValue(Canvas.LeftProperty, (blocks[i].AbsolutePosition.X + this.RelativelyPosition.X) * blocks[i].Width);
blocks[i].SetValue(Canvas.TopProperty, (blocks[i].AbsolutePosition.Y + this.RelativelyPosition.Y) * blocks[i].Height);
}
}
再来看看第三个问题,改变形态,就T形而言,它有4种形态,上、下、左、右。
根据前面的基础,实现它已经不是什么问题了,每一次形态改变都对基准图形的绝对坐标进行重新设置,绝对位置不同,形状也就不同。
但这里有一个问题,那就是如何知道这一次要改变成什么形态的图形,不同的图形每一次改变都不一样,这样就得寻找每种方块形态改变的规律。
就T形而言,它是沿着中心点顺时针运动,假定它不是由4个基准图形组成,而是由5个组成,这样每次变换就是中心点基准图形坐标不变,其它各个基准图形按顺序交换坐标,就能确定下来它每一次所要形成的形态,因此,对T形对象设置一个虚坐标,填补每5个基准图形的空白:
private Point virtualPoint = new Point(0, 1); //虚坐标,用于坐标互换
/// <summary>
/// T旋转
/// 规则:
/// 沿中心点旋转
/// </summary>
public override void Rotation()
{
Point[] points = new Point[4];
points[0] = new Point(0, 0);
for (int i = 1; i < 3; i++)
{
//前一个变成后一个
points[i] = new Point(this.Blocks[i + 1].AbsolutePosition.X, this.Blocks[i + 1].AbsolutePosition.Y);
}
//最后一个与虚坐标互换
points[3] = new Point(virtualPoint.X, virtualPoint.Y);
//虚坐标与第一个坐标互换
virtualPoint = new Point(this.Blocks[1].AbsolutePosition.X, this.Blocks[1].AbsolutePosition.Y);
//旋转
for (int i = 0; i < points.Length; i++)
{
this.Blocks[i].AbsolutePosition = points[i];
}
}
形态问题解决了,方块该如何移动,在该游戏中,方块只能向下、左、右移动,它是不能向上移动的,每一次移动实际上就是相对位置的改变,所以对左右移动来说就在原相对坐标下X轴进行加一或减一操作,而向下移动,就只对Y轴进行加一操作。
在方块的移动过程中,需要解决它的规则问题,当下次的移动被已存在的方块挡住时,这时该方向的移动应该被禁止,并且向下移动的过程,还得判断它已经不能再继续移动了,这时应该将该方块的移动停止掉。
这样,得定义游戏区域,每个基准图形的大小为20*20,定义游戏区域横向放10个基准图形,竖向放20个基准图形,游戏区域就10列20行,大小为200 * 400。
定义一个游戏区域变量,保存已结束移动的基准图形:
public static Dictionary<Point, SingleBlock> GameCanvas = new Dictionary<Point, SingleBlock>(); //定义游戏区域变量
//初始化游戏区域变量,每个区域开始时不存在任何基准图形
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 20; j++)
{
GameCanvas.Add(new Point(i, j), null);
}
}
判断方块能否移动需要逐个对基准图形与游戏区域中的坐标进行检测,比较下一次移动后的位置在游戏区中是否已存在基准图形,存在则不能移动,在这之前还要判断下一次的移动是否已超出游戏区域:
/// <summary>
/// 检验方块是否可移动(移动及旋转)
/// </summary>
private bool IsCanMove(Point newPoint)
{
foreach (SingleBlock block in Blocks)
{
//检验X轴是否超出画布区域
double x = block.AbsolutePosition.X + newPoint.X;
if (x < 0 || x > 9)
{
return false;
}
//检验Y轴是否超出画布区域
double y = block.AbsolutePosition.Y + newPoint.Y;
if (y > 19)
{
return false;
}
//检验当前位置是否已存在方块
if (y >= 0 && (!App.GameCanvas.ContainsKey(new Point(x, y)) || App.GameCanvas[new Point(x, y)] != null))
{
return false;
}
}
return true;
}
移动做了检测,相应的形态改变也需要做检验,它的原理与移动是一样的:
/// <summary>
/// 检验方块是否可旋转
/// </summary>
protected bool IsCanRotation(Point[] newPoints)
{
foreach (Point point in newPoints)
{
double x = this.RelativelyPosition.X + point.X;
if (x < 0 || x > 9)
{
return false;
}
//检验Y轴是否超出画布区域
double y = this.RelativelyPosition.Y + point.Y;
if (y > 19)
{
return false;
}
//检验当前位置是否已存在方块
if (!App.GameCanvas.ContainsKey(new Point(x, y)) || App.GameCanvas[new Point(x, y)] != null)
{
return false;
}
}
return true;
}
检测完成后可以进行移动了,为了使方块停止移动,加入方块当前的移动状态变量,停止时,将状态置为Ture:
/// <summary>
/// 图形是否已停止(到达指定位置)
/// </summary>
public bool IsStop
{
get;
set;
}:
/// <summary>
/// 方块移动方法
/// 参数:
/// newPoint:移动后的新坐标
/// isJudgeStop:是否判断移动结束
/// </summary>
public void Move(Point newPoint, bool isJudgeStop)
{
//判断是否可移动到新坐标
if (!IsCanMove(newPoint))
{
if (isJudgeStop)
{
this.IsStop = true;
}
return;
}
//移动
RelativelyPosition = newPoint;
SetPosition();
}
只剩下最后一个问题了,判断填满方块的行就是对游戏区域进行判定,每次方块移动结束后,遍历整个游戏区域,找出并记录填充完全的行序,然后从最下方开始进行遍历,当该行需消除时,清空游戏区当行记录,并为后面要下移的方块下移次数计数加一;不是需消除行则根据下移次数进行下移:
//判断游戏是否结束,顶层只要存在一个方块即结束
Point point;
//消除成功的行
List<int> lisSuccessRow = new List<int>();
for (int i = 19; i >= 0; i--)
{
bool isSuccess = true;
for (int j = 0; j < 10; j++)
{
point = new Point(j, i);
if (App.GameCanvas[point] == null)
{
isSuccess = false;
}
}
if (!isSuccess)
{
continue;
}
//记录将要移除的方块
lisSuccessRow.Add(i);
}
//移除方块
int downCount = 0; //未消除的行需要下移的次数
for (int i = 19; i >= 0; i--)
{
if (lisSuccessRow.Contains(i))
{
//判断并消除当前行
for (int j = 0; j < 10; j++)
{
point = new Point(j, i);
gameArea.Children.Remove(App.GameCanvas[point]);
App.GameCanvas[point] = null;
}
downCount += 1;
continue;
}
if (downCount == 0)
{
continue;
}
//未消除行下移
for (int j = 0; j < 10; j++)
{
point = new Point(j, i);
if (App.GameCanvas[point] == null)
{
continue;
}
double y = i * 1.0 + downCount;
App.GameCanvas[point].SetValue(Canvas.TopProperty, y * App.GameCanvas[point].Height);
App.GameCanvas[new Point(point.X, y)] = App.GameCanvas[point];
App.GameCanvas[point] = null;
}
}
所有的问题都解决了,最后为游戏添加定时器、侦听键盘事件,开始游戏、判断游戏结束、计算得分,整个游戏就完成了:
//添加定时器
private DispatcherTimer timer = new DispatcherTimer();
private double initFallStep = 0.5; //BLOCK下降步长(s)
timer.Tick += timer_Tick;
timer.Interval = TimeSpan.FromSeconds(curFallStep);
timer.Start();
this.KeyDown += OnKeyDown; //侦听键盘事件
private void timer_Tick(object sender, EventArgs e)
{
//SETP1:创建方块并显示
//SETP2:改变相对位置Y
//SETP3:判断结束并计算得分
}
private void OnKeyDown(object sender, KeyEventArgs e)
{
if (btStart.IsEnabled)
{
return;
}
if (e.Key == Key.Up)
{
//上方向键旋转
curGeometry.Rotation();
}
if (e.Key == Key.Left)
{
//左方向键左移
curGeometry.Move(new Point(curGeometry.RelativelyPosition.X - 1, curGeometry.RelativelyPosition.Y), false);
}
if (e.Key == Key.Right)
{
//右方向键右移
curGeometry.Move(new Point(curGeometry.RelativelyPosition.X + 1, curGeometry.RelativelyPosition.Y), false);
}
if (e.Key == Key.Down)
{
//下方向键加速,连续移动两次
for (int i = 0; i < 2; i++)
{
curGeometry.Move(new Point(curGeometry.RelativelyPosition.X, curGeometry.RelativelyPosition.Y + 1), true);
}
}
if (e.Key == Key.Space)
{
//空格键,直接结束移动
for (int i = 0; i < 20; i++)
{
curGeometry.Move(new Point(curGeometry.RelativelyPosition.X, curGeometry.RelativelyPosition.Y + 1), true);
}
}
}
//判断游戏是否结束,顶层只要存在一个方块即结束
Point point;
if (curGeometry.IsStop)
{
for (int i = 0; i < 10; i++)
{
point = new Point(i, 0);
if (App.GameCanvas[point] != null)
{
GameOver();
return;
}
}
}
到此为止,整个游戏设计完成了,接下来可以运行游戏,体验自己完成的游戏。