65.9K
CodeProject 正在变化。 阅读更多。
Home

俄罗斯方块游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.44/5 (10投票s)

2008年7月6日

CPOL

6分钟阅读

viewsIcon

52029

downloadIcon

1995

创建游戏

引言

我一直在 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` 是我们跟踪的左上角位置。

shapes.jpg

让我来解释一下 `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`。

turn.jpg

我不确定如何正确地旋转每个 `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 日:初始帖子
© . All rights reserved.