VB.NET 2005 俄罗斯方块游戏基础






4.88/5 (34投票s)
用现代语言重制经典下落方块游戏。

引言
深受喜爱的俄罗斯方块游戏曾经非常流行,但现在它已被层出不穷的新游戏所超越。我去年开始编写这个程序,当时我参加的高级Visual Basic编程课程要求我们重新创建这个经典游戏。花了好几个小时寻找代码示例和头脑风暴程序的各个方面后,大家一致认为这个任务对整个班级来说过于困难。网上有很多游戏示例,但没有一个是开源的,也没有解释程序是如何构建的。然而,我不能就这样放弃这个伟大的程序。结果就是您现在看到的:一个基础的双人俄罗斯方块游戏,包含了经典游戏的所有基本功能。本教程将从头到尾指导您完成这个程序的创建,并尽可能详细地解释每一个细节。
概念
您可能会认为俄罗斯方块的概念非常简单。然而,有很多方面需要涵盖。需要考虑的一些概念包括:
- 俄罗斯方块游戏板应采用什么尺寸?
- 程序是否混合了逻辑和图形方面?
- 如何旋转方块?
- 如何将方块存储在游戏板上?
- 如何将不同的方块绘制到游戏板上?
- 如何为方块设置动画?
- 如何管理已完成的行?
这仅仅是开始俄罗斯方块制作过程之前必须提出的所有问题中的一小部分。有些问题可以通过简单的Google搜索得到答案。例如,俄罗斯方块游戏板的标准尺寸是10x20。要存储方块,我只能想到一种存储所有东西的方法……数组……或者为了更灵活的方法,可以使用集合。在这个程序中,我使用了System.Collections.ObjectModel
命名空间中的Collection(Of type)
类。
现在可以回答的最后一个问题是关于程序的逻辑和图形方面的混合。当我的编程班最初选择俄罗斯方块时,目标之一是将程序移植到XNA,以便游戏可以转移到XBOX 360。当然,这不会发生,因为班级已经转移到另一个程序了,但为了方便这一点,我编写这个程序时将逻辑和图形方面分开。这样,在程序移植到XNA时,只需要更改图形层。
逻辑层
为了开始逻辑层,我们现在必须回答这个问题:“如何将方块存储在游戏板上?”作为班级,我们开始时是一次一个点地添加到游戏板上,然后操作四个点来移动方块。这几乎立即导致了笨拙的代码,最终会产生冗长且令人困惑的代码语句。为了纠正这个问题,我们决定创建一个名为Shape
的类。这个类基本上是逻辑层的心脏。这是Shape
类的基本大纲:
Points
as Collection(Of Point) - 此属性存储当前形状所有点的逻辑网格。每个形状总共有四个点,每个点都存储在此属性中。由于这是程序中的逻辑部分,因此这些点不需要遵守游戏板上的像素坐标。逻辑网格是一个10x20的网格,游戏板的顶部坐标为20,游戏板的最右边坐标为10。
IsFrozen
as Boolean - 此属性用于阻止方块在着陆后不再移动。
Color
as Color - 每个方块都有不同的颜色,颜色就存储在这里。即使此属性分配了一种颜色,图形层也可以操纵此颜色的使用。此应用程序通过ControlPaint.Light
(c
as Color) as Color 方法使用此颜色和颜色变体来创建LinearGradientBrush
。
GetNewPoints
(direction
as NewDirection) as Collection(Of Point) - 如果形状要沿NewDirection
枚举中的任何方向移动,此函数将返回一个新点集,表示形状将要移动到的位置。NewDirection
枚举包含以下方向:left
、right
、down
、current
、toggleOrientation
。稍后将在文章中讨论此方法中的移动。
MoveLeft
()
- 此方法调用GetNewPoints
并用GetNewPoints
返回的点替换当前存储在形状对象中的点。
MoveRight
()
- 此方法调用GetNewPoints
并用GetNewPoints
返回的点替换当前存储在形状对象中的点。
ToggleOrientation
()
- 此方法调用GetNewPoints
并用GetNewPoints
返回的点替换当前存储在形状对象中的点。
WillCollide
(shp
as Shape, direction
as NewDirection) as Boolean - 我从AP编程课的海洋生物学案例研究中获得了此函数的灵感。此方法将检查两个形状的逻辑网格并查找重叠。如果发生点重叠,则函数返回false
。否则,将返回true
。
WillCollide
(direction
as NewDirection) as Boolean - 此函数是第一个WillCollide
函数的重载。此重载仅处理方块在逻辑网格(10x20)上的放置。如果新方向导致方块水平或垂直超出游戏板,此函数将返回false
。否则,将返回true
。
形状对象的移动
如上面的大纲所述,GetNewPoints
函数是Shape
类的一个主要部分。GetNewPoints
函数基本上有助于回答“如何旋转方块?”这个问题。当实例化Shape
类时,会通过构造函数传递一个ShapeType
。ShapeType
枚举包含了俄罗斯方块中的所有形状。包含的形状有:square
、pyramid
、staircaseLeft
、staircaseRight
、lshapeLeft
、lshapeRight
、line
和null
。所有ShapeType
基本上都是不言自明的,但null
是一个例外,将在本文后面解释。GetNewPoints
基本上是一个巨大的Select Case
语句,用于确定如何移动方块。GetNewPoints
的基本部分是左移、右移和下移。
Dim rval As New Collection(Of Point)
Select Case direction
Case NewDirection.current
Return Me.Points
Case NewDirection.down
For Each pnt As Point In Me.Points
rval.Add(New Point(pnt.X, pnt.Y - 1))
Next
Case NewDirection.left
For Each pnt As Point In Me.Points
rval.Add(New Point(pnt.X - 1, pnt.Y))
Next
Case NewDirection.right
For Each pnt As Point In Me.Points
rval.Add(New Point(pnt.X + 1, pnt.Y))
Next
...
正如您所见,代码相当直接。如果形状向左移动,则从每个点的x值中减去1。如果形状向右移动,则在每个点的x值上加1。最后,如果形状向下移动,则从每个点的y值中减去1。请记住,此代码仍在逻辑层中,其中x是0到10之间的任何值,y值是0到20之间的任何值。对于ToggleOrientation
方向,代码会变得更复杂一些。
Select Case direction
...
Case NewDirection.toggleOrientation
Select Case shapeType
...
Case falling blocks.ShapeType.lshapeLeft
'toggle point is 3rd cube
Dim x, y As Integer
x = Points(2).X
y = Points(2).Y
'find the farrest point from center
If (Points(0).X = Points(3).X - 2) Then 'pointing left
'switch to up
rval.Add(New Point(x, y + 2))
rval.Add(New Point(x, y + 1))
rval.Add(New Point(x, y))
rval.Add(New Point(x - 1, y))
ElseIf (Points(0).Y = Points(3).Y + 2) Then 'pointing up
'switch to right
rval.Add(New Point(x + 2, y))
rval.Add(New Point(x + 1, y))
rval.Add(New Point(x, y))
rval.Add(New Point(x, y + 1))
ElseIf (Points(0).X = Points(3).X + 2) Then 'pointing
'right switch to down
rval.Add(New Point(x, y - 2))
rval.Add(New Point(x, y - 1))
rval.Add(New Point(x, y))
rval.Add(New Point(x + 1, y))
ElseIf (Points(0).Y = Points(3).Y - 2) Then 'pointing down
'switch to left
rval.Add(New Point(x - 2, y))
rval.Add(New Point(x - 1, y))
rval.Add(New Point(x, y))
rval.Add(New Point(x, y - 1))
End If
...
Case falling blocks.ShapeType.pyramid
'toggle point is 3rd cube
Dim x, y As Integer
x = Points(2).X
y = Points(2).Y
If (Points(0).X = x - 1) Then 'pointing left switch to up
rval.Add(New Point(x, y + 1))
rval.Add(New Point(x - 1, y))
rval.Add(New Point(x, y))
rval.Add(New Point(x + 1, y))
ElseIf (Points(0).Y = y + 1) Then 'pointing up switch to right
rval.Add(New Point(x + 1, y))
rval.Add(New Point(x, y + 1))
rval.Add(New Point(x, y))
rval.Add(New Point(x, y - 1))
ElseIf (Points(0).X = x + 1) Then 'pointing right switch to down
rval.Add(New Point(x, y - 1))
rval.Add(New Point(x - 1, y))
rval.Add(New Point(x, y))
rval.Add(New Point(x + 1, y))
ElseIf (Points(0).Y = y - 1) Then 'pointing down switch to left
rval.Add(New Point(x - 1, y))
rval.Add(New Point(x, y - 1))
rval.Add(New Point(x, y))
rval.Add(New Point(x, y + 1))
End If
...
正如您从此代码语句中可以看出,它确实变得更加复杂。我选择了两个代码片段来旋转pyramid
和lshapeleft
。对于这两者,每个点都基于一个焦点进行操作,该焦点(至少根据代码的编写方式)始终是Points
集合中的第三个点。我通过反复试验编译了这段代码,所以可能有更有效的方法来完成相同的代码。总而言之,您找到焦点、形状当前指向的方向,然后操作其他点来更改形状指向的方向。
碰撞检测
形状类的碰撞系统一次仅处理两个形状控件之间的碰撞。这是通过重载的WillCollide
(...)函数实现的。很简单,这两个函数只是嵌套的for循环,用于检查两个控件之间的相似点或超出逻辑网格的点。这两个函数利用GetNewPoints
(...)方法在形状控件实际移动之前检查其移动。在实际移动方块之前必须调用这些碰撞检测方法,以确保移动是合法的。
Public Function WillCollide(ByVal shape As Shape, ByVal direction As NewDirection)_
As Boolean
Dim pntsToCheck As Collection(Of Point) = GetNewPoints(direction)
For Each pnt As Point In shape.Points
For Each pnts As Point In pntsToCheck
If (pnt.Equals(pnts)) Then
Return True
End If
Next
Next
End Function
Public Function WillCollide(ByVal direction As NewDirection) As Boolean
Dim pntsToCheck As Collection(Of Point) = GetNewPoints(direction)
For Each pnt As Point In pntsToCheck
If (pnt.X < 0 Or pnt.X > 9 Or pnt.Y > 19 Or pnt.Y < 0) Then
Return True
End If
Next
End Function
游戏棋盘
到目前为止,我们可以回答文章开头提出的7个问题中的3个。通过Board
用户控件,我们现在可以回答最后四个问题:“如何将方块存储在游戏板上?”,“如何将不同的方块绘制到游戏板上?”,“如何为方块设置动画?”和“如何管理已完成的行?”
首先,“如何将方块存储在游戏板上?”这个问题与如何存储Shape
类的逻辑点非常相似。主要的思路应该是数组,但Collection(Of type)
在添加/删除/操作其中存储的对象方面提供了更好的功能。对于Board
控件,我添加了一个恰当命名的私有变量boardPieces
,其类型为Collection(Of Shape)
。
第二个问题,“如何将不同的方块绘制到游戏板上?”基本上是整个图形层。对于这个Board
对象,paint事件已通过以下代码处理:
Private Sub Board_Paint(ByVal sender As Object,_
ByVal e As System.Windows.Forms.PaintEventArgs)_
Handles Me.Paint
For x As Integer = 0 To 9
For y As Integer = 0 To 19
e.Graphics.DrawRectangle(Pens.Black, x * 20, y * 20, 20, 20)
Next
Next
For Each shp As Shape In boardPieces
For Each pnt As Point In shp.Points
e.Graphics.FillRectangle(New LinearGradientBrush(_
New Rectangle(0, 0, 19, 19),_
shp.Color,_ControlPaint.Light(shp.Color), _
LinearGradientMode.BackwardDiagonal), _
(9 - (9 - pnt.X)) * 20 + 1, (19 - pnt.Y) * 20 + 1, 19, 19)
Next
Next
End Sub
这段代码非常简单地遍历每个形状的每个点,并根据形状点在逻辑网格上的位置在Board
控件上绘制一个矩形。这是这个俄罗斯方块程序中唯一处理像素坐标的代码段。如果将此程序移植到其他图形框架,只需替换这一段代码即可。
游戏板方块的动画
倒数第二个问题是一个非常简单的问题,“如何为方块设置动画?”目前,此程序不使用.NET 3.0框架的Windows Presentation Foundation,因此实现此类动画的少数方法之一是使用计时器。对于这个游戏板,添加了两个计时器:tmrMove
和tmrIncreaseTmr
。tmrMove
,正如您可能猜到的,是让所有方块从俄罗斯方块游戏板顶部下降的计时器。tmrIncreaseTmr
仅用于每个级别,以增加tmrMove
计时器的速度。tmrMove_Tick
是Board
控件中唯一创建新形状并管理向下移动碰撞检测的方法。
Private Sub tmrMove_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs)_
Handles tmrMove.Tick
If (paused) Then
Return
End If
If (currentPieceIndex = -1) Then
'initially create a piece
Dim randVal As Integer = randPieceGen.Next(0, 7)
Dim shp As Shape
Select Case randVal
Case 0
shp = New Shape(ShapeType.line)
Case ...
End Select
'set the nextPiece property
randVal = randPieceGen.Next(0, 7)
Select Case randVal
Case 0
_nextPiece = ShapeType.line
Case ...
End Select
boardPieces.Add(shp)
currentPieceIndex = boardPieces.IndexOf(shp)
ElseIf (boardPieces(currentPieceIndex).IsFrozen) Then
'if frozen then create a new piece
Dim randVal As Integer = randPieceGen.Next(0, 7)
Dim shp As Shape = New Shape(_nextPiece)
randVal = randPieceGen.Next(0, 7)
Select Case randVal
Case 0
_nextPiece = ShapeType.line
Case ...
End Select
For Each shap As Shape In boardPieces
If (shp.WillCollide(shap, NewDirection.down)) Then
Me.tmrMove.Enabled = False
RaiseEvent GameOver(Me, New EventArgs())
Return
End If
Next
boardPieces.Add(shp)
currentPieceIndex = boardPieces.IndexOf(shp)
Else
'this is the overall movement and scoring along with the arrow key methods
Dim boolCollision As Boolean = False
boolCollision = boardPieces(currentPieceIndex).WillCollide(NewDirection.down)
For Each shp As Shape In boardPieces
If (Not shp Is boardPieces(currentPieceIndex)) Then
If (Not boolCollision) Then
boolCollision = boardPieces(currentPieceIndex).WillCollide(shp,_
NewDirection.down)
End If
End If
Next
If (Not boolCollision) Then
boardPieces(currentPieceIndex).MoveDown()
score = score + 5 * level
RaiseEvent ScoreChange(Me, New EventArgs())
Else
boardPieces(currentPieceIndex).IsFrozen = True
ManageCompleteLines()
End If
End If
Me.Invalidate()
End Sub
正如您所见,tmrMove_Tick
事件分为三个部分。第一个部分是游戏刚开始时:该方法将一个新的形状控件添加到boardPieces
集合中。第二部分处理初始游戏板设置之外的所有其他形状创建。它与第一部分非常相似,但它还处理GameOver
事件,以防创建的形状在创建时碰到另一个方块。
第三部分处理更多内容。首先,该部分检查当前方块是否能够向下移动,或者是否会发生碰撞。如果发生碰撞,则调用名为ManageCompleteLines
(...)的方法,并将方块冻结。如果不会发生碰撞,则方块只会向下移动一个位置。此方法中的最后一行将导致游戏板完全重绘。重绘将反映刚刚对Board
控件的boardPieces
所做的任何更改。还创建了与此代码的第三部分结构相似的方法来处理用户输入:MoveLeft
、MoveRight
、MoveDown
和ToggleOrientation
。
管理已完成的行
到目前为止,我们已经涵盖了创建一个功能齐全的俄罗斯方块游戏的所有代码,但我们最后一个问题呢?在俄罗斯方块开发概念阶段提出的最后一个问题是:“如何管理已完成的行?”这个方法是程序最重要的部分之一。没有这个方法,程序就没有得分或目标。
Private Sub ManageCompleteLines()
Dim counter(20) As Integer
'instantiate everything
For i As Integer = 0 To 19
counter(i) = 0
Next
'get lines completed
For Each shp As Shape In boardPieces
For Each pnt As Point In shp.Points
counter(pnt.Y) = counter(pnt.Y) + 1
Next
Next
'score for every completed line
Dim lineCount As Integer = 0
For Each i As Integer In counter
If (counter(i) = 10) Then
lineCount += 1
End If
Next
score = score + (20 * level * lineCount)
RaiseEvent ScoreChange(Me, New EventArgs())
'for all completed lines remove points
For i As Integer = 0 To 19
If (counter(i) = 10) Then
Dim shapes As New Collection(Of Shape)
For Each shp As Shape In boardPieces
If (shp.IsFrozen) Then
'collect all points then remove them
Dim pntsToRemove As New Collection(Of Point)
For Ipnt As Integer = 0 To shp.Points.Count - 1
If (shp.Points(Ipnt).Y = i) Then
pntsToRemove.Add(shp.Points(Ipnt))
End If
Next
For Each pnt As Point In pntsToRemove
shp.RemovePoint(pnt)
Next
End If
Next
End If
Next
'move lines down by moving only the points of the objects
For i As Integer = 19 To 0 Step -1
If (counter(i) = 10) Then
For Each shp As Shape In boardPieces
Dim moveDown As Boolean = False
Dim pointsToMove As New Collection(Of Point)
For Each pnt As Point In shp.Points
If (pnt.Y > i) Then
pointsToMove.Add(pnt)
End If
Next
For Each pnt As Point In pointsToMove
shp.MovePointDown(pnt)
Next
'If (moveDown And shp.WillCollide(NewDirection.down) = False) Then
' shp.MoveDown()
'End If
shp.IsFrozen = True
Next
End If
Next
'if the board is completely cleared add bonus points
If (boardPieces.Count = 0) Then
score = score + 1000 * level
End If
End Sub
这段代码基本上会搜索boardPieces
中每个形状包含的每个点,并累积每行中的点数。如果一行包含10个点,则该行已完成,应将其删除。形状在无法移动时冻结的一个优点是,您可以随后操作形状中的点,而不必担心更多的移动或旋转。
最后一个for...next
方法充分利用了这一功能,通过遍历每个形状的点来删除一行中的每个点。如果该点位于指定的完成行上,则删除该点并向下移动形状。已完成行的得分也在此方法中计算。得分(至少对于这个俄罗斯方块游戏)乘以一次完成的行数以及完成这些行的级别。
使用此控件
Board
控件是一个标准的自定义控件,您可以将其拖放到任何您选择的窗体上。该控件公开三个事件——ScoreChange
、LevelChange
和GameOver
——并公开方法,以允许在其父控件中进行专门的输入。在此项目中,我在mainWindow
上有两个Board
控件。为了管理每个控件的用户输入,我创建了一个单一的mainForm_KeyUp
方法,它处理通过键盘接收的所有输入。输入根据按键方案W、A、S、D以及上/下/左/右箭头键以及映射到两个玩家的暂停“P”按钮分配给每个控件。然后,这些按键被映射到相应的方法:ToggleOrientation
、MoveDown
、MoveLeft
和MoveRight
。
关注点
这个程序还有一些我没有涵盖的零碎部分,但这应该足以让其他人开始创建自己的俄罗斯方块变体。未包含但会是很棒的功能的方面包括高分以及游戏的加载/保存。
历史
- 2008年3月12日 - 下载和图像已更新
- 2008年3月10日 - 文章已编辑并移至Code Project主文章库
- 2008年2月8日 - 修正了在单人游戏中按W、A、S、D键时出现的问题,修复了在用箭头键玩游戏时主窗体上的按钮切换焦点的问题
- 2008年2月7日 - 首次公开发布
- 2008年1月12日 - 程序开始