贪吃蛇游戏相信很多朋友都听说或者玩过,特别是以前使用过诺基亚手机的朋友,这在当时就是诺基亚手机的专配游戏。
本篇文章讲述如何在vb.net中编写此游戏代码。
一种方法是可以使用控件数组,在用户界面上使用如多个图片框构成整个背景,将相关图片框设置为蛇身来绘制出整条蛇。这种方法比较简单,处理速度也快。
另外一种方法是通过GDI+在用户界面上绘制此游戏,本文将采用此方法,
一、需要解决的问题
1、绘制蛇身。
2、蛇的移动
3、产生食物
4、当吃到食物(与食物发生碰撞时),使蛇增加一段长度。
5、当碰到边界或者自身时,游戏结束。
二、涉及到的类
1、单个蛇身(蛇头):clsSnakeSingleBody
2、整条蛇:clsSnakeBody
3、食物:clsFood
三、如何解决问题
1、不论单个的蛇身还是蛇头,都是一个clsSnakeSingleBody类,该类中有个nextBody 属性,这是clsSnakeSingleBody类型,指向了下一个段蛇身。
在clsSnakeBody类中,有一个snakehead(蛇头)属性,这是clsSnakeSingleBody类型。
当得到了snakehead(蛇头)后,通过它的nextBody属性不断地遍历,可以获得每一段蛇的坐标、大小(实际就是一个矩形),结合Graphics的FillRectangle方法,就可以绘制出整条蛇。
2、蛇(代码中实例为snake)的移动,实际最重要的是蛇头的移动。通过在窗口界面设置计时器组件(代码中为Timer1),每隔一段时间就按照clsSnakeBody类的direction(方向)属性计算蛇头坐标位置。如下图:
重新绘制蛇头,通过nextBody属性不断地遍历蛇身,获得的每一段蛇身的坐标都是前一段蛇身(头)的坐标,遍历的时候完成对蛇身的绘制。
3、设置食物坐标位置,通过Random类得到食物坐标,将得到的坐标与每一段蛇身(头)坐标对比,如果重合,那么重新获得食物坐标。
4、当蛇吃到食物(蛇头与食物碰撞)时,新建一个clsSnakeSingleBody实例(代码中为newsnakehead),坐标同食物坐标,使snake的snakehead属性指向newsnakehead,而newsnakehead的nextBody指向原来的蛇头,这样就顺利生成了新的一段。
5、通过Timer1的Tick事件移动蛇时,先预先计算蛇头下一步的位置,与边界位置比对,如果超出了边界,视为死亡,触发snake的snakeCrossWall事件。同样,预先计算蛇头下一步的位置,与蛇身各段坐标比对,如果坐标相同,视为发生了碰撞,触发snakeCrossBody事件。
四、扩展内容
1、在代码中设置了A按键和S按键来实现暂停和继续操作,实际对应Timer1的Stop和Start方法。
2、当蛇吃到食物时触发snakeEat事件,除了重新得到一个食物外,还可以获得积分。当吃的食物越多,获得的积分越高。
五、设计和代码
窗体界面如下:
1、clsSnakeSingleBody类
'一段蛇身/蛇头
Public Class clsSnakeSingleBody'宽度Property width As Integer'高度Property height As Integer'横坐标Property x As Integer'纵坐标Property y As Integer'颜色Property foreColor As Color'指向下一段蛇身Property nextBody As clsSnakeSingleBodySub New()width = 20height = 20End Sub
End Class
2、clsSnakeBody类
'整条蛇
Public Class clsSnakeBody'移动步长Private Const moveStep As Integer = 20'场地宽度Property bgWidth As Integer'场地高度Property bgHeight As Integer'运动方向Property direction As String'蛇头Property snakehead As clsSnakeSingleBody'事件:吃到食物Event snakeEat()'事件:蛇头和蛇身发生碰撞Event snakeCrossBody()'事件:撞墙Event snakeCrossWall()'构造函数Sub New(ByVal g As Graphics, ByVal bgwidth As Integer, ByVal bgheight As Integer)Me.bgWidth = bgwidthMe.bgHeight = bgheight'初始化的整条蛇是1个蛇头加上两个蛇身'这里蛇各段坐标Dim snakeStartHead As New clsSnakeSingleBodysnakeStartHead.x = 40snakeStartHead.y = 0snakeStartHead.foreColor = Color.RedDim snakeStartBody1 As New clsSnakeSingleBodysnakeStartBody1.x = 20snakeStartBody1.y = 0snakeStartBody1.foreColor = Color.BlueDim snakeStartBody2 As New clsSnakeSingleBodysnakeStartBody2.x = 0snakeStartBody2.y = 0snakeStartBody2.foreColor = Color.BluesnakeStartHead.nextBody = snakeStartBody1snakeStartBody1.nextBody = snakeStartBody2snakehead = snakeStartHeaddirection = "right"Call DrawSnake(g)End SubPublic Sub move(ByVal g As Graphics, ByVal food As clsFood)'判断是否死亡Dim isDead As Boolean = False'状态Dim skState As String = "normal"'判断是否吃到食物Dim isEat As Boolean = False'保存头部坐标Dim previewX, previewY As IntegerpreviewX = snakehead.xpreviewY = snakehead.y'变化后的头部坐标Dim nextX, nextY As IntegernextX = snakehead.xnextY = snakehead.y'获得头部坐标,并判断头部是否碰撞,是否碰到身体Select Case directionCase "up"'是否碰到四周If (snakehead.y - moveStep >= 0) ThennextY = snakehead.y - moveStepElseskState = "wall"Exit SelectEnd If'是否碰到自己If checkCross(snakehead.x, snakehead.y - moveStep) = False ThennextY = snakehead.y - moveStepElseskState = "body"Exit SelectEnd If'是否碰到食物If nextX = food.x AndAlso nextY = food.y ThenskState = "food"Exit SelectEnd IfCase "down"'是否碰到四周If snakehead.y + moveStep + snakehead.height < bgHeight ThennextY = snakehead.y + moveStepElseskState = "wall"Exit SelectEnd If'是否碰到自己If checkCross(snakehead.x, snakehead.y + moveStep) = False ThennextY = snakehead.y + moveStepElseskState = "body"Exit SelectEnd If'是否碰到食物If nextX = food.x AndAlso nextY = food.y ThenskState = "food"Exit SelectEnd IfCase "left"'是否碰到四周If snakehead.x - moveStep >= 0 ThennextX = snakehead.x - moveStepElseskState = "wall"Exit SelectEnd If'是否碰到自己If checkCross(snakehead.x - moveStep, snakehead.y) = False ThennextX = snakehead.x - moveStepElseskState = "body"Exit SelectEnd If'是否碰到食物If nextX = food.x AndAlso nextY = food.y ThenskState = "food"Exit SelectEnd IfCase "right"'是否碰到四周If snakehead.x + moveStep + snakehead.width < bgWidth ThennextX = snakehead.x + moveStepElseskState = "wall"Exit SelectEnd If'是否碰到自己If checkCross(snakehead.x + moveStep, snakehead.y) = False ThennextX = snakehead.x + moveStepElseskState = "body"Exit SelectEnd If'是否碰到食物If nextX = food.x AndAlso nextY = food.y ThenskState = "food"Exit SelectEnd IfEnd SelectSelect Case skStateCase "wall"'如果碰到墙壁,那么触发snakeCrossWall事件food.drawFood(g)Call DrawSnake(g)RaiseEvent snakeCrossWall()Case "body"'如果碰到自身,那么触发snakeCrossBody事件food.drawFood(g)Call DrawSnake(g)RaiseEvent snakeCrossBody()Case "food"'如果吃到食物,那么触发snakeEat事件Dim newsnakehead As New clsSnakeSingleBody()newsnakehead.x = food.xnewsnakehead.y = food.ynewsnakehead.foreColor = Color.Redsnakehead.foreColor = Color.Bluenewsnakehead.nextBody = snakeheadMe.snakehead = newsnakehead'身体其他部分不动Call DrawSnake(g)'触发snakeEat事件RaiseEvent snakeEat()Case "normal"'向前移动snakehead.x = nextXsnakehead.y = nextYDim currentX, currentY As Integer'移动身体各部分Dim snakesinglebody As clsSnakeSingleBody = snakehead.nextBodyDo While IsNothing(snakesinglebody) = FalsecurrentX = snakesinglebody.xcurrentY = snakesinglebody.ysnakesinglebody.x = previewXsnakesinglebody.y = previewYpreviewX = currentXpreviewY = currentYsnakesinglebody = snakesinglebody.nextBodyLoopfood.drawFood(g)Call DrawSnake(g)End SelectEnd Sub'判断头部是否和身体碰撞Private Function checkCross(ByVal x As Integer, ByVal y As Integer) As Boolean'Dim snakesinglebody As clsSnakeSingleBody = snakehead.nextBodyDo While IsNothing(snakesinglebody) = FalseIf x = snakesinglebody.x AndAlso y = snakesinglebody.y ThenConsole.WriteLine("碰撞:" & x & " " & y)Console.WriteLine(snakesinglebody.x & " " & snakesinglebody.y)Return TrueEnd Ifsnakesinglebody = snakesinglebody.nextBodyLoopReturn FalseEnd FunctionPrivate Sub DrawSnake(ByVal g As Graphics)'绘制头部Dim fillBrush As New SolidBrush(snakehead.foreColor)g.FillRectangle(fillBrush, New Rectangle(snakehead.x, snakehead.y, snakehead.width, snakehead.height))'绘制身体Dim snakesinglebody As clsSnakeSingleBody = snakehead.nextBodyDo While IsNothing(snakesinglebody) = FalsefillBrush.Color = snakesinglebody.foreColorg.FillRectangle(fillBrush, New Rectangle(snakesinglebody.x, snakesinglebody.y, snakesinglebody.width, snakesinglebody.height))snakesinglebody = snakesinglebody.nextBodyLoopEnd Sub
End Class
3、clsFood类
Public Class clsFoodConst width As Integer = 20Const height As Integer = 20'食物颜色Property foreColor As Color'食物横坐标Property x As Integer'食物纵坐标Property y As IntegerSub New(ByVal g As Graphics, ByVal snake As clsSnakeBody, ByVal bgwidth As Integer, ByVal bgheight As Integer)'设置颜色foreColor = Color.Green'设置坐标Call getPos(snake, bgwidth, bgheight)'绘制食物Call drawFood(g)End Sub''' <summary>''' 得到食物的坐标位置''' </summary>Private Sub getPos(ByVal snake As clsSnakeBody, ByVal bgwidth As Integer, ByVal bgheight As Integer)Dim widthsection As Integer = (bgwidth \ 20)Dim heightsection As Integer = (bgheight \ 20)Dim rnd As New RandomDim posx, posy As IntegerDim hasCross As BooleanDohasCross = Falseposx = 20 * rnd.Next(widthsection)posy = 20 * rnd.Next(heightsection)If (posx = snake.snakehead.x) AndAlso (posy = snake.snakehead.y) ThenhasCross = TrueElseDim snakesinglebody As clsSnakeSingleBody = snake.snakehead.nextBodyDo While IsNothing(snakesinglebody) = FalseIf (posx = snakesinglebody.x) AndAlso (posy = snakesinglebody.y) ThenhasCross = TrueExit DoEnd Ifsnakesinglebody = snakesinglebody.nextBodyLoopEnd IfLoop While hasCross = Truex = posxy = posyConsole.WriteLine(posx & ":" & posy)End SubPublic Sub drawFood(ByVal g As Graphics)'绘制食物Dim fillBrush As New SolidBrush(foreColor)g.FillRectangle(fillBrush, New Rectangle(x, y, width, height))End SubEnd Class
4、窗体代码
Public Class Form1Const boxWidth As Integer = 20Const boxHeight As Integer = 20Dim WithEvents snake As clsSnakeBodyDim food As clsFoodDim snakeCount As IntegerPrivate Sub DrawLines(ByVal g As Graphics)Dim penGrid As New Pen(New SolidBrush(Color.Black), 1.0)For i As Integer = 0 To picBackGround.Width Step boxWidthg.DrawLine(penGrid, New Point(i, 0), New Point(i, picBackGround.Height))NextFor j As Integer = 0 To picBackGround.Height Step boxHeightg.DrawLine(penGrid, New Point(0, j), New Point(picBackGround.Width, j))NextEnd SubPrivate Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.ClickDim g As Graphics = picBackGround.CreateGraphicsg.Clear(Color.White)snake = New clsSnakeBody(g, picBackGround.Width, picBackGround.Height)food = New clsFood(g, snake, picBackGround.Width, picBackGround.Height)snakeCount = 0Timer1.Start()End SubPrivate Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.TickDim g As Graphics = picBackGround.CreateGraphicsg.Clear(Color.White)snake.move(g, food)End Sub'这个对方向按键无效Private Sub Form1_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDownEnd SubProtected Overrides Function ProcessDialogKey(keyData As Keys) As BooleanSelect Case keyDataCase Keys.UpIf snake.direction = "up" Or snake.direction = "down" Then Exit Selectsnake.direction = "up"Case Keys.DownIf snake.direction = "up" Or snake.direction = "down" Then Exit Selectsnake.direction = "down"Case Keys.LeftIf snake.direction = "left" Or snake.direction = "right" Then Exit Selectsnake.direction = "left"Case Keys.RightIf snake.direction = "left" Or snake.direction = "right" Then Exit Selectsnake.direction = "right"Case Keys.ATimer1.Stop()Case Keys.STimer1.Start()Case ElseReturn MyBase.ProcessDialogKey(keyData)End SelectEnd Function'碰到墙壁Private Sub snake_snakeDead() Handles snake.snakeCrossWallTimer1.Stop()MessageBox.Show("碰到墙壁")Call gameover()End Sub'吃到食物Private Sub snake_snakeEat() Handles snake.snakeEatDim g As Graphics = picBackGround.CreateGraphicsfood = New clsFood(g, snake, picBackGround.Width, picBackGround.Height)snakeCount += 10lblCount.Text = snakeCountEnd Sub'碰到自身Private Sub snake_snakeCrossBody() Handles snake.snakeCrossBodyTimer1.Stop()MessageBox.Show("碰到自身")Call gameover()End Sub'结束本局Private Sub gameover()End SubEnd Class
六、运行结果
七、遗留的问题
1、蛇死亡(超过边界或者碰到自身)时,按下“S”按键,蛇就复活了。
解决办法:在按下S按键时检查snake是否已经挂了,如果挂了,S按键就不再发挥作用。
2、在检测蛇头和蛇身碰撞时,实际上由于代码中我们仅仅预测了蛇头的下一步坐标位置,而没有预测蛇身每一段的下一个坐标位置,所以代码中如果蛇头恰好碰撞蛇尾(最后一段蛇身),在实际上蛇头和蛇尾同时前进,是碰不到的。
解决方法:在蛇头和蛇身碰撞时,将蛇身每一段的坐标变化也考虑进去,在检测是否碰撞。
3、当蛇占据整个游戏区域时,并没有结束游戏,因为没有相关代码。
要给比较简单的解决方法:获得游戏区域的大小,计算可以分隔的方块(蛇身)数量,如果蛇身长度等于方块总数,那么表示已经占据整个游戏区域。
相信各位粉丝看了以上内容,可以熟练地做出属于自己的贪吃蛇程序。
由于.net平台下C#和vb.NET很相似,本文也可以为C#爱好者提供参考。
学习更多vb.net知识,请参看vb.net 教程 目录
目前我已经将VB.Net的教程汇集到《Visual Basic.Net 循序渐进》,喜欢编程的朋友不要错过。