俄罗斯方块游戏






4.44/5 (10投票s)
创建游戏
引言
我一直在 DoFactory 上研究一些设计模式,作为一种良好的实践,我决定编写一个使用工厂模式的俄罗斯方块游戏。
我不会深入探讨模式的细节,但我建议你阅读一下。
“抽象工厂设计模式。
定义
提供一个接口,用于创建相关或依赖对象的族,而无需指定它们的具体类。”
你可以在 这里 阅读有关该模式的信息。
我假设你这辈子都玩过最著名的下落方块游戏“俄罗斯方块”,所以没有必要解释游戏规则。
开始
当我开始编写游戏时,我实际上不知道从哪里开始。作为目标,我决定不看任何开源的下落方块代码或任何关于该主题的教程。我想自己摸索。
所以我想,一个好的起点应该是最简单的形状——立方体——一个四乘四的正方形。但在那之前,我们必须创建一个名为 `Shape` 的抽象
类,所有实际的形状都将继承它。
只是一个形状
让我们快速看一下 `shape` 的代码。
abstract class Shape
{
// holds the current shape turn state shapes can rotate up to 4 times.
protected int turnState;
public int TurnState { get {return turnState;} set {turnState = value;}}
/*
* Returns the new cords of the shape after rotation.
*/
public abstract Point[] Turn(int top, int left);
/*
* Returns the current shape cords depends on the top left position
* of the shape and its turnState.
*/
public abstract Point[] GetCoordinates(int top, int left);
}
正如你所见,`shape` 可以旋转(每次 90 度),并且我们可以通过获取其坐标来移动 `shape`,以获得所需的左上角位置。我们将跟踪 `shape` 的左上角坐标,这将使事情保持通用。
因此,在 `Shape` 类到位并且我们创建的每个 `shape` 都将继承它的想法下,让我们继续我们的 `square` 类。
别做一个方块
class Square : Shape
{
public Square()
{
}
public override Point[] Turn(int top, int left)
{
return GetCoordinates(top, left);
}
public override Point[] GetCoordinates(int top, int left)
{
Point[] cords = new Point[4];
cords[0] = new Point(left, top);
cords[1] = new Point(left + 1, top);
cords[2] = new Point(left, top + 1);
cords[3] = new Point(left + 1, top + 1);
return cords;
}
}
很简单吧。`square` 不需要旋转,所以它的左上角坐标总是在同一个位置。
形状及其坐标
标记为 1 的 `square` 是我们跟踪的左上角位置。

让我来解释一下 `GetCoordinates` 方法。
假设我们要将正方形向下移动一行。我们将调用 `GetCoordinates`,Y 增加 1,X 记住我们跟踪的是左上角坐标。因此,通过将 Y 增加 1,我们将 `square` 向下移动了一行。
cords[0] = new Point(left, top) //is our new top left position
基于这一点,我们构建了 `square` 的其余部分。
我们仍然需要检查刚刚进行的移动是否合法,但我们将在稍后解决这个问题。
一旦我们理解了 `shape` 表示和移动的关键概念,创建新的 `shape` 就很容易了。
旋转 `shape` 只是一个如何确定 `shape` 旋转后的布局以及我们的左上角坐标应该去哪里的问题,然后剩下要做的就是根据这个新的左上角坐标重新构建 `shape`。

我不确定如何正确地旋转每个 `shape`,所以我自己想了一个办法。
我一直在谈论坐标。让我们看看它们实际上去了哪里。
游戏棋盘类
将游戏想象成一个二维布尔网格,具有宽度和高度,已填充的空间标记为 `true`,自由空间标记为 `false`。
棋盘类管理游戏棋盘,通过
- 检查是否可以重新定位给定的形状
- 将形状重新绘制到屏幕上
- 更新游戏棋盘矩阵的布尔值
- 检查是否有行已满
将所有这些责任放在一个地方似乎是合乎逻辑的。这个类有很多内容,所以我只指出一些我认为有趣的事情。
一个出现的问题是我们如何知道某个形状的移动(右、左、下、旋转)是否可能?
当然,我们可以为每个形状实现一个复杂的检查,但这将花费太长时间。幸运的是,有一个更快捷的方法可以做到这一点。
让我们看一下棋盘类的 `Move` 方法。
public bool Move(Point[] currentPos, Point[] desiredPos)
{
if (!LegitMove(currentPos, desiredPos))
return false;
// Remove shape from the board
Pen pen = new Pen(backgroundColor, 3);
DrawShape(currentPos, pen);
// Redraw
RePosition(desiredPos);
pen = new Pen(Color.Blue, 3);
DrawShape(desiredPos, pen);
return true;
}
该方法获取当前形状的位置坐标和期望的坐标(形状想要移动到的位置)。
在 `LegitMove` 方法中,我们创建游戏棋盘的一个副本,并从中“剪掉”当前形状,使其不占用任何空间,然后我们尝试将形状粘贴到新的位置。如果成功,则该移动合法,我们将游戏棋盘与我们制作的副本覆盖。否则,我们无法将形状移动到其新位置,并返回 `false`,表示原始游戏棋盘未发生任何更改。
这为我们提供了一个简单的机制来检查所有可想象的 `shape` 移动,只要我们能够获取当前 `shape` 的位置(在棋盘上的坐标)及其新的期望位置。
整合
那么事情是如何实际工作的呢?让我们快速回顾一下游戏的“流程”。
游戏向 `shape` 工厂请求一个 `shape`。有关此类的更多信息将在下一节中介绍。
就我们而言,我们不关心实际的 `shape` 是什么。我们所知道的是,我们得到了一个 `shape`,并且我们可以与之交互。
接下来,我们尝试将 `shape` 定位在棋盘的顶部中间。如果我们未能做到这一点,我们假设棋盘已填满到顶部,这意味着游戏结束。
否则,我们设置一个计时器,该计时器将在每个滴答声将我们的 `shape` 向下移动一行。
如果 `shape` 成功向下移动了一行,我们什么也不做。
否则,如果 `shape` 无法向下移动一行,那是因为它碰到了另一个 `shape` 或到达了游戏底部棋盘。我们将不得不检查玩家是否设法填满了一整行(或多行),所以我们执行检查并根据需要更新棋盘(清除已满的行,将上面的行重新定位到已清除的行之上)。
关于当前 `shape` 就到这里。所以从我们的工厂获取一个新的 `shape` 并重复。
在整个过程中,用户可以通过旋转、向左、向右和向下移动来操纵当前的 `shape`,并且对于每一次“重新定位”,我们都会检查移动是否可能。
工厂的辛苦一天
class ShapesFactory
{
Random rand;
enum shapes { Square, Stick, L, MirroredL, Plus, Z, MirroredZ };
public ShapesFactory()
{
rand = new Random();
}
public Shape GetShape()
{
int shape = rand.Next(7);
switch (shape)
{
case (int)shapes.Square:
return new Square();
break;
case (int)shapes.Stick:
return new Stick();
break;
.
.
.
case (int)shapes.MirroredZ:
return new MirroredZ();
break;
default:
return new Square();
}
}
此类负责根据随机数创建新的 `shape`。
工厂有其产品(L、立方体、Z 等),当游戏需要一个 `shape` 时,工厂就会提供。
最后的话
游戏中缺少一些“关键”功能,如棋盘边界、让用户知道下一个 `shape` 将是什么、显示和计分。
但游戏的核心部分已经到位,这实际上是我的目标。而且图形不是很好,但我不是设计师。
我喜欢写这个游戏,我花了一段时间才弄清楚事情应该如何运作,但一旦我有了检查机制和 `Shape` 类的抽象
方法,添加新的 `shape` 就非常迅速。
到这里就差不多了。希望我指出了游戏如何运作的一些关键观点。如果您有任何意见或问题,请随时写信给我。
历史
- 2008 年 7 月 6 日:初始帖子