Excel+VBA制作小游戏:贪吃蛇
很长时间未曾更新,先前承诺要制作的贪吃蛇游戏还没有完成。今天在某个话题中讨论到贪吃蛇,下午便利用空闲时间着手开发。这次依然采用类模块的方式来进行构建。
一、准备界面
我给工作表取名为Game,采用30行20列的布局作为游戏场景,为了模仿像素风格开yun体育app入口登录,将单元格宽度设定为2,为了使轮廓更加分明,我从B2单元格起开始构建这个界面。

请允许我这种有洁癖的人,在四周都留有空白,选用加粗的边框这个设计就完成了。图片里的“蛇”和“豆”都是后来添加的。
我们在此明确,蛇的躯体呈现为墨色,而豆子则为朱红。为了实现直观展示,我们设定蛇身对应数字1,豆子对应数字2,并借助条件格式来调控单元格的色调。这一做法与我们在玩扫雷时的策略如出一辙,无需在程序中单独设定样式,预计也能提升运行效率。
二、蛇对象
1、初始化
我们创建一个名为Snake的新类别,在顶部需要定义两个协议,一个是用于计时功能的开元棋官方正版下载,另一个是用于检测按键情况的,具体实现如下:
#If VBA7 And Win64 Then
声明一个指针安全类型的函数GetTickCount,在库kernel32中调用,返回值为长整型数据
声明一个指针安全类型的函数GetKeyboardState,从动态链接库user32中获取,参数为字节型数组pbKeyState,返回值为长整型数据
#Else
声明一个函数GetTickCount, 来自库kernel32, 返回一个长整型值
声明一个函数GetKeyboardState, 通过调用user32动态链接库, 参数为字节型数组pbKeyState, 返回值为长整型数据
#End If
这两种方法在以往的俄罗斯方块游戏中都应用过,因此无需详细解释,此外,部分对象的特性如下所示。
游戏界面横向尺寸为整数
游戏界面高度为整数值
整型变量firstPointX代表初始位置的横坐标值
公共首点纵坐标值作为整数类型
头横坐标被声明为整型变量,命名为headX
声明一个名为头纵坐标的变量,其类型为整数
声明一个整型变量名为尾坐标,用于存储横向位置值
定义一个名为尾纵坐标的变量,其类型为整数,用于存储尾部垂直位置的具体数值
蛇身当前长度设定为整数值,具体数值需要根据实际情形确定。
全局变量 移动方位, 其值介于零至三之间, 其中零象征向上, 一象征向右, 二象征向下, 三象征向左
全局变量 移动点X , 类型为 整数 , 代表 物体 移动后的 横向位置
定义变量存储位移后的垂直位置,该值为整数类型,名为纵坐标偏移量
移动模式被定义为整数值,当其值为1时代表正在移动,当其值为2时则表示正在进食
游戏模式状态为布尔值,用以判定能否跨越界限
Public gameArr '游戏区域
Public snakeBodyArr '蛇身区域
其实前四个公共成员也可以用const声明为固定值,不过这个不必太在意了。至于那些能够跨越边界的变量,只是暂时放在这里,咳咳……实际上是因为下午时间不够,所以没有完成这一部分内容。
初始化时我们做了以下几件事:
公共成员设定首尾坐标,确定初始长度,描绘蛇形,将游戏空间与蛇形区域纳入列表,放置一颗豆子
私有子程序类初始化时
Me.gameModel = False
Me.interfaceWidth = 30
Me.interfaceHeight = 20
Me.firstPointX = 2
Me.firstPointY = 2
With Worksheets("Game")
'清空界面
单元格区域从坐标 Me.firstPointX, Me.firstPointY 开始,横向扩展 Me.interfaceWidth 个单位,纵向扩展 Me.interfaceHeight 个单位,该范围内的所有内容都被清空了
'设置头尾初始坐标
我的tailX值等于界面高度与第一个点的Y坐标之和的一半
Me.tailY = Me.tailX
Me.headX = Me.tailX
Me.headY = Me.tailY + 3
'初始长度为4,画出蛇身
Me.length = 4
单元格区域从尾X到尾Y,宽度为1,高度为长度,其值设置为包含1, 1, 1, 1的数组
'将游戏区域和蛇身区域放入数组
gameArr等于从指定起始坐标扩展的单元格区域,该区域的高度为界面高度,宽度为界面宽度,起始坐标位于第一点横纵坐标位置
重新定义蛇身数组长度为当前长度减一
Dim i%
For i = 0 To Me.length - 1
snakeBodyArr第i项等于一个数组,包含头部横坐标,头部纵坐标减去i
Next i
'生成一个豆子
Me.New_Bean
'默认方向为右
Me.moveDir = 1
End With
End Sub
2、生成豆子
生成豆子的函数属于初始化过程的一部分,该函数会随机确定游戏版图中的一个位置,接着在那个位置标记数值为2,实现方式如下:
Public Sub New_Bean()
Dim beanX%, beanY%
Do
Randomize (Timer)
beanX等于界面高度乘以随机数再加上初始点X坐标
beanY等于界面宽度乘以随机数的结果,再加上第一个点的纵坐标
不断循环,直到beanX和beanY能够互相作用,否则继续尝试
工作表游戏单元格区域位于坐标beanX,beanY的值被设置为2
End Sub
制作豆子时需关注一个要点,制作出的豆子不能出现在蛇的身体上,因此必须设计一个判断是否可放置豆子的函数,该函数会输出真或假的结果,这个函数只需检查蛇身体占据的坐标区域,看其是否与随机生成的坐标相同即可,如果将蛇身体占据的区域存储在字典中,就无需编写这样的函数了
公共函数 Bean可行性测试(参数x,参数y)返回布尔值
Bean_Is_Possible = True
Dim i%
For i = 0 To Me.length - 1
若蛇身数组第i个元素的第一个值等于x,且第二个值等于y,则豆子不可能被吃,函数立即终止
Next i
End Function
3、游戏开始
这个游戏是一个状态机,蛇会持续不断地前进,一旦无法继续移动,游戏状态就会转变为结束。
Public Sub Game_Start()
Dim direaction As Variant
方向数组包含四组数值,第一组为负一零,第二组为零一,第三组为一零,第四组为零负一
Do
DoEvents
'计算移动后头坐标
Dim movePointX%, movePointY%
移动点的横坐标等于头部横坐标加上移动方向向量的第一个分量
移动点的垂直坐标等于头部垂直坐标值加上移动方向函数返回的第一参数值
'判断是否可以移动
If Move_Possible Then
If Me.moveModel = 1 Then
Call Me.Move
ElseIf Me.moveModel = 2 Then
Call Me.Eat
End If
Me.Sleep (100)
Else
Exit Do
End If
Loop While (True)
MsgBox "GAME OVER"
End Sub
依据移动方向moveDir属性,推算出头部坐标的变化,接着确认能否实现移动。倘若在禁止穿越边界的情形下超出范围,又或者与自身相撞,便判定为无法移动并终止循环。倘若能够移动,就考察新位置是否含有豆子,若有则移动状态转为“进食”,若无则维持“行进”。
此函数能够判断移动是否可行,返回一个布尔值结果
Move_Possible = False
Me.moveModel = 1
If Me.movePointX < Me.firstPointX Or Me.movePointY < Me.firstPointY Or Me.movePointX >当我的第一个点的X坐标加上我的界面高度,或者我的移动点的Y坐标大于等于我的第一个点的Y坐标加上我的界面宽度时,就退出这个函数
gameArr等于工作表Game中从坐标Me.firstPointX, Me.firstPointY开始,宽度和Me.interfaceWidth相等,高度和Me.interfaceHeight相等的一个单元格区域
当游戏数组中位于 Me.movePointX 减去 firstPointX 加一 的横坐标,和 Me.movePointY 减去 firstPointY 加一 的纵坐标位置上的值为 1 时,程序将终止当前函数的执行
当游戏数组中坐标为移动点X减去第一个点X加一,移动点Y减去第一个点Y加一的元素值为二时,移动模型设置为二
Move_Possible = True
End Function
4、移动
实际上位移非常容易,表面上看整条蛇都前进了一个位置,实际上仅需在蛇体上添加一个头部,同时移除一个尾部元素。这也是为什么在对象特征里要设定头部和尾部标记。当然,也可以借助蛇身构成的数组区域来达成,这样操作会显得更加清晰明了。
Public Sub Move()
Me.headX = Me.movePointX
Me.headY = Me.movePointY
With Worksheets("Game")
当前单元格的值被设置为空字符串,该单元格位于指定的行和列位置
当前单元格的值设为1,位于指定行列位置
End With
Me.Snake_Body_Update
End Sub
这里有一个更新蛇身的方法,它的作用是调整蛇身区域这个数组。这个数组里记录的是蛇身所有节点的位置信息,我们需要把每个节点的位置依次往后挪,这样原先最后的节点位置也就是蛇尾就会被移除开yun体育官网入口登录app,然后再把新的蛇头位置信息放在数组的最前面。
公共子程序蛇身更新,当蛇头移动时,需要同步调整身体各部分的位置,首先确定头部的新坐标,然后依次更新每个身体节点的位置,确保它们跟随头部移动,更新过程要考虑蛇身的长度和移动方向,避免出现错位或重叠的情况,最后将所有节点的新位置应用到游戏中,以实现流畅的蛇身跟随效果
从Me的长度减一的位置开始,往前往后遍历,每次递减一个单位
snakeBodyArr的第i个元素等于snakeBodyArr的第i-1个元素
Next i
snakeBodyArr(0)(0) = Me.headX
snakeBodyArr(0)(1) = Me.headY
当前蛇尾的坐标等于蛇身数组中最后一个元素的第一个元素值
尾部Y坐标等于蛇身数组最后一个元素的第二个值
工作表游戏, 单元格区域AF22, 予以选中
End Sub
最终选定位置,目的是使该选定单元格保持稳定,不会发生位移。
5、吃
遇到豆子时,行动状态转为“进食”。首先,将豆子所在格子标记为1,蛇的体长加一,然后把蛇身所有位置依次右移,使豆子位置变为蛇头。接着,在游戏画面中随机生成新的豆子。
Public Sub Eat()
Me.headX = Me.movePointX
Me.headY = Me.movePointY
工作表游戏单元格移动点X移动点Y赋值为一
Me.length = Me.length + 1
重新调整数组大小为当前长度减一,并保留原有元素
Me.Snake_Body_Update
Me.New_Bean
End Sub
6、延时+键盘输入
整个流程已经大体结束,唯独状态机里的移动后延时措施Sleep缺少解释说明,延时的环节与先前在迷宫、俄罗斯方块等场合应用的机制一致,唯一变化是将通过Worksheet_SelectionChange获取坐标的操作移到了这个延时环节里面。
子程序歇息,参数名为数值
'延时方法
Dim num1 As Double
Dim num2 As Double
Dim numb As Double
numb = 0
num1 = GetTickCount
Do While numa - numb > 0
num2 = GetTickCount
numb = num2 - num1
DoEvents
Dim keycode(0 To 255) As Byte
GetKeyboardState keycode(0)
如果按键代码大于127,那么清除按键代码,当前界面移动方向设为0,这代表向上移动
当39键的键值超过127时清除键值,我的移动方向设为1向右
当按键代码值大于127时清除按键代码,当前界面移动方向设为2,代表向下移动
当键码值大于127时清除键码,我的移动方向设为3向左
Loop
End Sub
先前已经解释过缘由,贪吃蛇的运作机制是持续朝一个方向行进,因此与俄罗斯方块相较,针对键盘指令只需变更行进路线即可,无需启动特定的移动程序。
三、对象的使用
在模块里增加一个功能,通过设立变量,来执行Game_Start功能让游戏启动。在表格界面放置一个控制键,按下它就能运行Game_Start功能。
Public Sub Game_Start()
Dim s As New Snake
s.Game_Start
End Sub
这个办法相对原始,还有待进一步优化,另外边界穿越和积分提速等功能尚未开发,积分提速可以借鉴俄罗斯方块中的机制,而边界穿越则可以通过取模运算来达成。
谢谢阅读。