简介
扫雷这个游戏没有什么好说的,就是排布雷场场加判定,很经典的游戏,于是我尝试用golang自己写一个,选用了fyne框架作为UI。个人认为fyne灵活程度不是很高,自定义有些难度,拿来做一些简单的ui可以,但是想要深度定制需要下一番功夫去学习。
效果展示
- 游戏菜单——选择游戏难度
- 点击进入游戏主界面
- 游戏过程
两个难点
懂得使用fyne进行简单的元素排布其实整个界面就能写出来了,无非是根据数组循环生成按钮和标签。不过生成雷场是一个难点,另一个我认为是做到点击空地后一点打开一大片的效果
一、生成雷场
这个项目中我使用了自定义的Vector3结构去记录每个单元的X,Y位置信息,以及单元格代表的属性(0表示空地,-1表示地雷,其余数字表示该方格周围存在的地雷数)
type Vector3 struct {
X int
Y int
Num int
}
这样,如果我们想要布雷,只需要根据两个随机数生成器生成x,y上的随机数就可以了,然后准备一个Vector3的数组来装载这些单元。不过记得除重以及判断是否超界,否则就会出现报错和莫名其妙的BUG。
那么,如何去求那些烦人的数字呢?现在我的手里有一个数组的代表单元格的结构体,但是我怎么去求这些单元格之间互相纠葛的数字呢?这个事情让我纠结了好久。后来我想到了一个方法:既然这个数字代表的是附近地雷的数量,那么反过来想,当你凝视深渊时,深渊也在凝视着你。每个雷单元都会处于这个数字的单元格周围范围内,我只需要去遍历数组中存在的雷单元格,然后再遍历每个雷单元格周围的单元格,将遍历到的代表数字的单元格数字+1就可以了。为什么要说“代表数字”的单元格,因为有些雷单元可能相邻,千万别忘了处理这些格子,不然你的雷单元就会消失。另外也不要忘了判断是否出界,不然会出错的。
以下是处理这个功能的代码:
func (mf *MineField) getAroundMinesNum() {
//这里拿到了Vector3数组
mineList := mf.mapData
for i := 0; i < len(mineList); i++ {
//遍历周边单元
for x := -1; x <= 1; x++ {
for y := -1; y <= 1; y++ {
aroundPoint := &Vector3{
X: mineList[i].X + x,
Y: mineList[i].Y + y,
}
//如果是本身,则跳过
if aroundPoint.isEqual(mineList[i]) {
continue
}
//如果数组中存在
//1. 判断是否是雷,如果是则跳过
//2. 如果不是雷,则Num加一
index := mf.getPointIndex(*aroundPoint)
if index != -1 {
if mf.mapData[index].Num == -1 {
continue
} else {
mf.mapData[index].Num++
}
} else {
//如果不存在,则添加;如果位置非法则放弃添加
mf.addNums(*aroundPoint)
}
}
}
}
}
二、点击空地开一片的效果
到这里得说明一下用户界面的结构。大家都知道,每一个元素的单元格其实都是被按钮遮盖的,点击之后让单元格上的按钮消失就会显示你扫到的到底是个啥,那么底下的元素怎么显示呢?我的方案是使用Label作为底层显示的元素,去表示空地,数字,或者地雷——这里我偷了个懒取了个巧 ,所有图标都是依靠emoji显示的,包括地雷和旗子,省去了很多麻烦。
为了让每个单元格由两个组件表示,我选择了StackContainer,它的特性是只会显示最上层组件,因此只要上层按钮隐藏,自然会露出下面藏着的label。
根据Vector3数组转换的整型二维数组表示的地图数据,只需要遍历各单元,label根据每个单元的类型表示相应元素就成了。
不过如何做到点击空地时隐藏一片按钮直到一圈数字呢?这个问题好比于在一片空间内找边界,也许可以用到一个广度优先算法。如果知道了我们点击的表示空地的按钮位置,就寻找它周围的数字、空地元素并隐藏他们的按钮,如果是数字就停止延申,如果是空地就继续这个过程。 进行一个递归调用,最终掀开所有合法单元的脑壳。
func (gv *GameViewer) openField(positon []int) {
//获得本按钮的位置
posX := positon[1]
posY := positon[0]
//遍历周围单元
for x := -1; x <= 1; x++ {
for y := -1; y <= 1; y++ {
if x == 0 && y == 0 {
continue
}
pointX := posX + x
pointY := posY + y
//地址合法
if pointX >= 0 && pointX < gv.mf.length && pointY >= 0 && pointY < gv.mf.width {
//获得该点元素类型
pointNum := gv.mapData[pointY][pointX]
//获得按钮,以进行操作
btn := gv.btnArray[pointY*gv.mf.length+pointX]
//确保朝一个方向蔓延,防止重复访问导致死循环
if btn.Hidden || btn.Text == "🚩" {
continue
}
btn.Hide()
//是空地,则递归调用
if pointNum == 0 {
newPos := make([]int, 0)
newPos = append(newPos, pointY)
newPos = append(newPos, pointX)
gv.openField(newPos)
}
}
}
}
}
结语
对于Fyne我还是不熟练,有很多界面设置都是直接摆烂的,实在是还没学会怎么调整它的界面,现在觉得前端html组合还是灵活的多。不过这个小项目倒是让我对golang和fyne更了解了一些,希望能给大家带来一些思考启发和学习。
源码在此:https://github.com/OneWalkerCN/Minesweeper