如何制作俄罗斯方块克隆?

Posted

技术标签:

【中文标题】如何制作俄罗斯方块克隆?【英文标题】:How To Make a Tetris Clone? 【发布时间】:2010-10-08 15:30:19 【问题描述】:

我正在使用 XNA C# 编写俄罗斯方块克隆,我不确定从高层次上处理游戏数据结构方面的最佳方法。

我对碰撞检测、旋转、动画等非常满意。我需要知道存储“掉落块”的最佳方法——即不再受玩家控制的块。

我认为每个 Tetromino 块都应该存储在它自己的类中,该类由一个 4x4 数组组成,以便块可以轻松旋转。然后问题是我如何将 tetromino 的最终位置存储到游戏网格中,然后将 tetromino 切割成单独的块(对于每个单元格),然后设置主游戏网格的相应位置来保存这些相同的块,然后消失一次 tetromino它已到达最终位置。也许我的方法有一些缺点。

我应该为主要的游戏网格创建一个 10x20 的矩阵,然后可以存储吗?或者我应该使用堆栈或队列以某种方式存储丢弃的块。或者也许有一些更好的方法/数据结构来存储东西?

我确信我的方法会奏效,但我想看看是否有人知道更好的方法,或者我的方法是否足够好?

附:不是家庭作业,这将是我投资组合的一个项目。谢谢。

【问题讨论】:

【参考方案1】:

在我的示例(Java)中 - 所有图形都有块列表 - 可以在需要时删除。同样在我的 Board 类中,我有一个数字列表和一个由用户控制的字段变量数字。当图形“着陆”时 - 它进入其他图形列表,并且用户可以控制一个新图形。 这里有一个更好的解释:http://bordiani.wordpress.com/2014/10/20/tetris-in-java-part-i-overview/

【讨论】:

【参考方案2】:

使用 Simon Peverett 逻辑,这是我在 c# 中得到的结果

public class Tetromino 

    // Block is composed of a Point called Position and the color
    public Block[] Blocks  get; protected internal set; 

    // Constructors, etc.

    // Rotate the tetromino by 90 degrees, clock-wise
    public void Rotate() 
    
        Point middle = Blocks[0].Position;
        List<Point> rel = new List<Point>();
        foreach (Block b in Blocks)
            rel.Add(new Point(b.Position.x - middle.x, b.Position.y - middle.y));

        List<Block> shape = new List<Block>();
        foreach (Point p in rel)
            shape.Add(new Block(middle.x - p.y, middle.y + p.x));

        Blocks = shape.ToArray();
    

    public void Translate(Point p)
    
        // Block Translation: Position+= p; 
        foreach (Block b in Blocks)
            b.Translate(p);
    

注意:使用 XNA,Point 结构可以交换为 Vector2D

【讨论】:

【参考方案3】:

在我看来,不让块看起来像自治块是许多俄罗斯方块克隆的一大失败。我特别努力确保my clone 总是看起来正确,无论该块仍然“在游戏中”还是被丢弃。这意味着要稍微超越简单的矩阵数据结构,并提出一些支持块部分之间“连接”概念的东西。

我有一个名为BlockGrid 的类,它用作BlockBoard 的基类。 BlockGrid 有一个名为 AreBlockPartsSameBlock 的抽象(C++ 中的纯虚拟)方法,子类必须覆盖该方法以确定两个不同的块部分是否属于同一个块。对于Block 中的实现,如果两个位置都有块部分,它只返回true。对于Board 中的实现,如果两个位置包含相同的Block,则返回true

BlockGrid 类使用此信息“填充”渲染块中的​​细节,使它们看起来像块。

【讨论】:

让这些片段看起来像这样“连接”起来纯粹是一种视觉选择。最初的 NES 俄罗斯方块并没有这样做,每个块都是独立的,但它的颜色由它最初来自的块类型设置。总体而言,我认为这对于尝试编写基本克隆的人来说会增加很多复杂性。 IMO 它看起来比单独的正方形更难看,但如果你真的喜欢这种外观,那么你的方式就是要走的路。 是的,Kent,我同意你所说的通过使用轮廓或外发光或其他东西使游戏中的活动块在视觉上有所不同。你能解释一下你在 Daniel Lew 的回答中不同意的地方吗? 不明白为什么我不能使用矩阵并使活动块在视觉上有所不同 你说得对,Brock,如果块的每个单元格都有与哪些边相连的信息,或者与此相关的信息,你仍然可以让它在视觉上有所不同【参考方案4】:

我的解决方案(设计),以 Python 中的示例作为伪代码的良好替代品。

使用 20 x 10 的网格,四联骨牌会倒下。

Tetromino 由块组成,块具有坐标 (x,y) 和颜色属性。

所以,例如,T 形四肢骨看起来是这样的......

     . 4 5 6 7 8 .
  .
  19     # # #
  20       #
  .   

因此,T 形是坐标为 (5,19)、(6,19)、(7,19)、(6,20) 的块的集合。

移动形状只需对组中的所有坐标应用简单的变换即可。例如要向下移动形状,请将 (0,1)、左 (-1,0) 或右 (1,0) 添加到集合中构成该形状的所有坐标中。

这还允许您使用一些简单的三角函数将形状旋转 90 度。规则是,当相对于原点旋转 90 度时,(x,y) 等于 (-y,x)。

这是一个解释它的例子。从上方取T形,以(6,19)为中心块旋转。为简单起见,将其设为集合中的第一个坐标,所以...

 t_shape = [ [6,19], [5,19], [7,19], [6,20] ]

然后,这是一个将坐标集合旋转 90 度的简单函数

def rotate( shape ):
    X=0      # for selecting the X and Y coords
    Y=1

    # get the middle block
    middle = shape[0]   

    # work out the coordinates of the other blocks relative to the
    # middle block
    rel = []
    for coords in shape:
        rel.append( [ coords[X]-middle[X], coords[Y]-middle[Y] ] )

    # now rotate 90-degrees; x,y = -y, x
    new_shape = []
    for coords in rel:
        new_shape.append( [ middle[X]-coords[Y], middle[Y]+coords[X] ] )

    return new_shape

现在,如果您将此函数应用于我们的 T 形坐标集合...

    new_t_shape = rotate( t_shape )

    new_t_shape
    [[6, 19], [6, 18], [6, 20], [5, 19]]

在坐标系中绘制出来,它看起来像这样......

     . 4 5 6 7 8 .
  .
  18       #
  19     # #
  20       #
  .   

这对我来说是最难的一点,希望这对某人有所帮助。

【讨论】:

使用了你的逻辑并在c#中改变了它【参考方案5】:

实际上我几天前才这样做,除了在 WPF 而不是 XNA 中。这是我所做的:

编辑: 似乎我对“阻止”的定义与其他人不同。我定义的块是构成 Tetromino 的 4 个单元之一,而实际的 Tetromino 本身就是一块。

将块作为具有 X、Y 坐标和颜色的结构。 (后来我加了一个 bool IsSet 来表示它是在一个浮动块中还是在实际板上,但这只是因为我想在视觉上区分它们)

作为 Block 上的方法,我有 Left、Right、Down 和 Rotate(Block center),它们返回一个新的移位块。这使我可以在不知道该块的形状或方向的情况下旋转或移动任何块。

我有一个通用的 Piece 对象,它包含它包含的所有块的列表以及作为中心的块的索引,它用作旋转中心。

然后我创建了一个 PieceFactory 可以生产所有不同的碎片,并且不需要知道它是什么类型的碎片,我可以(并且确实)轻松添加由多于或少于 4 个块组成的碎片的变体无需创建任何新类

Board 由一个 Dictionary 组成,其中包含当前在板上的所有块,以及可配置的板尺寸。您也可以使用矩阵,但使用字典我只需要遍历没有空格的块。

【讨论】:

【参考方案6】:

使用数组是处理俄罗斯方块的最简单方法。您在屏幕上看到的内容与内存中使用的结构之间存在直接关联。使用堆栈/队列将是一种矫枉过正和不必要的复杂。

您可以拥有 2 个下落块的副本。一个用于展示(Alpha),另一个用于移动(Beta)。

你需要一个类似的结构

class FallingBlock int pos_grid_x; int pos_grid_y; int blocks_alpha[4][4]; int blocks_beta[4][4]; function movedDown(); function rotate(int direction(); function checkCollision(); function revertToAlpha(); function copyToBeta() ;

_beta 数组将被移动或旋转,并检查板是否发生碰撞。如果发生冲突,将其恢复为 _alpha,如果没有,则将 _beta 复制到 _alpha。

如果在movedDown() 上发生碰撞,方块的生命就结束了,_alpha 网格将不得不复制到游戏板上并删除 FallingBlock 对象。

董事会当然必须是另一种结构,例如:

class Board int gameBoard[10][20]; //some functions go here

我用 int 来表示一个块,每个值(如 1、2、3)表示不同的纹理或颜色(0 表示一个空白点)。

一旦块成为游戏板的一部分,它只需要显示纹理/颜色标识符。

【讨论】:

为什么他得到了否定..只是好奇? +1 来自我,可能不是我要走的路,但我仍然很感激您的意见【参考方案7】:

这闻起来像家庭作业,但我对俄罗斯方块的面向对象方法的看法是让每个单独的正方形都是一个对象,“块”(tetrominos)和网格本身都是相同正方形对象的集合.

块对象管理下落方块的旋转和位置,网格处理显示它们和销毁已完成的行。每个方块都有与之相关的颜色或纹理,由它来自的原始方块对象提供,但网格底部的方块将没有其他迹象表明它们曾经是同一原始方块的一部分。

详细地说,当您创建一个新的块对象时,它会在网格上创建一组具有相同颜色/纹理的 4 个正方形。网格管理它们的显示。所以当方块触到底部时,你就忘记了方块,方块仍然被网格引用。

旋转和放下是只有一个方块需要处理的操作,并且只有它的四个方块之一(尽管它需要能够查询网格以确保旋转适合)。

【讨论】:

【参考方案8】:

我绝不是俄罗斯方块专家,但正如你所描述的,10x20 矩阵对我来说似乎是一个自然的选择。

当需要检查你是否完成了一行并处理它时,它会变得非常容易。只需遍历 2d 数组,查看每个位置的布尔值,看看它们是否加起来为 10 个块位置。

但是,如果有一条完整的线路,您将需要进行一些手动清理。不得不把一切都放下。尽管归根结底,这并不是什么大不了的事。

【讨论】:

【参考方案9】:

一旦一个块是不动的,就没有什么可以将它与任何其他现在不动的块区分开来了。在这方面,我认为将整个网格存储为矩阵是最有意义的,其中每个正方形要么被填充,要么被填充(如果是块的颜色,还有块的颜色)。

我觉得矩阵有很多优点。它将使碰撞检测变得简单(无需与多个对象进行比较,只需与矩阵上的位置进行比较)。将其存储为矩阵还可以更容易地确定何时创建了整行。最重要的是,当一条线消失时,您不必担心拼接不动的 Tetromino。当你这样做时,你可以一举将整个矩阵向下移动。

【讨论】:

我非常不同意 - 请参阅我的回答。 另外,你不能做甜美的动画或高级重力。我的董事会是一堆对这些作品的引用。当一条线被清空时,每个块单独落下,如果它们被分割或导致挂起的位被移除,碎片将按照应有的方式落下。 @toast:说他的回答不好,这一切都很好。我绝对可以在那里看到你的观点。也许您会提供一个答案来解释您将如何做到这一点。 您仍然可以使整条线消失而其余的块慢慢下降,您只需要在视图模型中添加一些额外的状态。我一直都是这样做的,它使游戏代码变得非常漂亮和简单。 toast 描述的不是俄罗斯方块,而是别的东西。 我没有说他的回答不好,我更同意肯特的说法。而且我觉得现有的答案涵盖了我无论如何都会说的内容。如果我觉得我只是在重复别人很糟糕,我不想回答。【参考方案10】:

请记住,之前的混淆 C 代码竞赛的获胜者用不到 512 字节的混淆 C 实现了一个相当不错的俄罗斯方块游戏(适用于 BSD unix 上的 VT100 终端):

long h[4];t()h[3]-=h[3]/3000;setitimer(0,h,0);c,d,l,v[]=(int)t,0,2,w,s,I,K
=0,i=276,j,k,q[276],Q[276],*n=q,*m,x=17,f[]=7,-13,-12,1,8,-11,-12,-1,9,-1,1,
12,3,-13,-12,-1,12,-1,11,1,15,-1,13,1,18,-1,1,2,0,-12,-1,11,1,-12,1,13,10,-12,
1,12,11,-12,-1,1,2,-12,-1,12,13,-12,12,13,14,-11,-1,1,4,-13,-12,12,16,-11,-12,
12,17,-13,1,-1,5,-12,12,11,6,-12,12,24;u()for(i=11;++i<264;)if((k=q[i])-Q[i]
)Q[i]=k;if(i-++I||i%12<1)printf("\033[%d;%dH",(I=i)/12,i%12*2+28);printf(
"\033[%dm  "+(K-k?0:5),k);K=k;Q[263]=c=getchar();G(b)for(i=4;i--;)if(q[i?b+
n[i]:b])return 0;return 1;g(b)for(i=4;i--;q[i?x+n[i]:x]=b);main(C,V,a)char*
*V,*a;h[3]=1000000/(l=C>1?atoi(V[1]):2);for(a=C>2?V[2]:"jkl pq";i;i--)*n++=i<
25||i%12<2?7:0;srand(getpid());system("stty cbreak -echo stop u");sigvec(14,v,
0);t();puts("\033[H\033[J");for(n=f+rand()%7*4;;g(7),u(),g(0))if(c<0)if(G(x+
12))x+=12;elseg(7);++w;for(j=0;j<252;j=12*(j/12+1))for(;q[++j];)if(j%12==10)
for(;j%12;q[j--]=0);u();for(;--j;q[j+12]=q[j]);u();n=f+rand()%7*4;G(x=17)||(c
=a[5]);if(c==*a)G(--x)||++x;if(c==a[1])n=f+4**(m=n),G(x)||(n=m);if(c==a[2])G
(++x)||--x;if(c==a[3])for(;G(x+12);++w)x+=12;if(c==a[4]||c==a[5])s=sigblock(
8192);printf("\033[H\033[J\033[0m%d\n",w);if(c==a[5])break;for(j=264;j--;Q[j]=
0);while(getchar()-a[4]);puts("\033[H\033[J\033[7m");sigsetmask(s);d=popen(
"stty -cbreak echo stop \023;cat - HI|sort -rn|head -20>/tmp/$$;mv /tmp/$$ HI\
;cat HI","w");fprintf(d,"%4d on level %1d by %s\n",w,l,getlogin());pclose(d);

http://www.ioccc.org/1989/tromp.hint

【讨论】:

是的,我知道我可以蛮力解决问题。那不是我感兴趣的事情,否则我会继续我的理论。我问了这个问题,看看是否有人对我计划进行的方式有一个优雅/更好的解决方案。 编写结构合理的代码很重要。这里不需要黑客攻击 我的观点是,对于一个非常简单的问题来说,花哨的数据结构和类层次结构太过分了。 @Paul Tomblin,这是一个很弱的例子。他不是在制作基于文本的俄罗斯方块。即使您只需要为片段添加颜色,为每个片段添加一个片段/块对象也会开始有意义。 +1 表示有趣,+1 表示提出合理的观点,-1 表示没有真正解决问题。

以上是关于如何制作俄罗斯方块克隆?的主要内容,如果未能解决你的问题,请参考以下文章

俄罗斯方块游戏

[从零开始]使用Vue.js制作俄罗斯方块小游戏ui实现

只会C语言编程还要学哪些才能做俄罗斯方块这样的小游戏?

俄罗斯方块游戏开发系列教程2:随机生成形状

俄罗斯方块

程序员技术宅万圣节自制“南瓜俄罗斯方块”