效果图
缘起
刚装了Ubuntu系统,发现里面有自带的扫雷等小游戏。最近又疯狂使用R,忽然有一个念头,R做游戏如何呢?是的,刚开始我想做的是扫雷,后面发现这里面存在一个很大的问题,故放弃了,具体原因我后面会讲。我便在网上查了一下,发现还真有人用R写 贪吃蛇游戏,便花费了一晚上也简单写了个游戏试试。
R的图形API
只有4个:
- setGraphicsEventHandlers, 注册图形事件,包括键盘事件和鼠标事件(按下,释放,移动)。
- getGraphicsEvent, 启动图形事件监听器,开始监听。
- setGraphicsEventEnv, 设置图形设备和环境空间,默认为当前图形设备和当前环境空间。
- getGraphicsEventEnv, 获取图形设备,默认为当前图形设备。
我只用到了第二个,这方面的知识可参考这篇 文章,很有用!
DFS函数生成迷宫
学过数据结构的小伙伴一定很熟悉 dfs(): 广度优先搜索算法
这里生成迷宫的思想是这样的,首先看如下图,黄色的地方代表 通路,灰色代表 围墙,最外侧的四面围墙是不可拆卸的,而相邻黄色格子间的通路是可以 打通的(对应代码里的connect()函数),然后利用 DFS()实现图的遍历,把一个个黄点看成独立的结点,进行搜索,生成连接路径,把 连接路径上的墙推倒(由灰色转为黄色)。是的,你可能意识到的,用DFS生成这样的迷宫会有如下两点:
- 每两个初始的黄色结点必有通路,因为是遍历
- 这样生成的迷宫岔路较少(相比其他生成方法)
数据结构
前面也说到,视初始的黄色结点为 图的节点,我们用变量 block_map 进行保存,大小的变量为 size
最终生成图形黄色的部分实际上有两种:一是 初始结点 ,二是dfs()运行中 推倒的墙 (墙倒了形成了路)。我们的 block_map 只会记录初始结点,我们需要另外任命一个变量 maze_map 来记录究竟哪里是路哪里是墙,它的大小记为 size2,可以简单知道 size2 = 2 * size - 3
代码解析
输入参数为:size, cex_set(后面解释cex_set)
切记:0表示路,1表示墙
- 初始化数据结构
size2 = 2*size -3
block_map = matrix(0, size, size)
maze_map = matrix(1, size2, size2)
for(i in 1:(size-2)){
for(j in 1:(size-2)){
maze_map[2*i, 2*j]=0
}
}
# 四面的围墙
block_map[1, ] = 1
block_map[size, ] = 1
block_map[, 1] = 1
block_map[, size] = 1
- 使用move简便 “上下左右”的操作
move=list(c(-1, 0), c(1, 0), c(0, -1), c(0, 1))
使用时应用 move[[i]][j] 这样的形式,list数据类型就是会麻烦点
- dfs() 前的准备
in_map() 用于判断操作是否越界
connect() 用于拆墙
neighbor_count() 用于计算该节点周围有几面墙
in_map <- function(x, y){
return((1<=x) && (x<=size) && (1<=y) && (y<=size))
}
connect <- function(x, y, xx, yy){
maze_map[(x+xx)-2, (y+yy)-2] <<- 0
}
neighbor_count <- function(x, y){
temp = 0
for(i in 1:4){
if(in_map(x + move[[i]][1], y + move[[i]][2])){
if(block_map[x + move[[i]][1], y + move[[i]][2]] == 1){
temp = temp + 1
}
}
}
return(temp)
}
- 实施 dfs()
看不懂的话直接上网上搜,网上有些说得很清楚。DFS()算法展开来讲会很多,这里略过。但我提一个很容易犯的错误作用域,如果这里把 block_map[xx, yy] <<- 1 错写成 block_map[xx, yy] <- 1 和 block_map[xx, yy] = 1,代码都会失效,问题在哪呢?—— 问题在于在 函数体 内你对 block_map 所作的修改并 没法传出去 !!具体可见这篇 文章
dfs <- function(x, y){
print(c(x, y, neighbor_count(x, y)))
if(neighbor_count(x, y) == 4){return}
direction = c(FALSE, FALSE, FALSE, FALSE)
while(neighbor_count(x, y) < 4){
temp = -1
while(temp == -1 || direction[temp] == TRUE ){
temp = sample(1:4, 1)
}
xx=x+move[[temp]][1]
yy=y+move[[temp]][2]
if(in_map(xx, yy) && block_map[xx, yy] == 0){
block_map[xx, yy] <<- 1
connect(x, y, xx, yy)
dfs(xx, yy)
direction[temp]=TRUE
if(neighbor_count(x, y) == 4){
return
}
}
}
return
}
- 开始画图,这里的配色你可以自己后期改,配色的话可以参考这个
windows()
plot(0,0,xlim=c(0, 10),ylim=c(0, 10),type='n',xaxs="i", yaxs="i")
for(i in 1:10){
points(i - 0.5, i - 0.5, col = i, pch = 15, cex = 2.5)
}
正式画图
now.x() 和 now.y() 记录现在的位置,dest.x() 和 dest.y() 记录终点位置
windows()
plot(0,0,xlim=c(0, size2),ylim=c(0, size2),type='n',xaxs="i", yaxs="i")
for(i in 1:(size2 -1)){
abline(h=i,col="gray60") # 水平线
abline(v=i,col="gray60")
}
abline(h=size2)
abline(v=size2)
for(i in 1:size2){
for(j in 1:size2){
if(maze_map[i, j]==1){
points(i-0.5, j-0.5, col = 8, pch = 15, cex = cex_set)
}
else{
points(i-0.5, j-0.5, col = 7, pch = 15, cex = cex_set)
}
}
}
now.x = 2
now.y = 2
dest.x = size2 - 1
dest.y = size2 - 1
points(now.x-0.5, now.y-0.5, col = 2, pch = 15, cex = cex_set)
points(dest.x-0.5, dest.y-0.5, col = 6, pch = 15, cex = cex_set)
- 写键盘事件
判断是否越界 及 是否为路
特别注意:now.y <<- now.y - 1
if(K == "down"){
if(now.y > 2 && maze_map[now.x, now.y-1] == 0){
points(now.x-0.5, now.y-0.5, col = 7, pch = 15, cex = cex_set)
now.y <<- now.y - 1
points(now.x-0.5, now.y-0.5, col = 2, pch = 15, cex = cex_set)
}
}
- 绑定键盘事件
getGraphicsEvent(onKeybd = keydown)
- cex_set
因为生成不同规模大小的地图,颜色填充的可能不好,要么没有填充完区域的大部份,要么超过区域到影响正常游戏操作,所以留了个cex_set设置点的大小,让使用者自己调节,实话实说,这是一处败笔,但我也懒得改了。
(2022/7/28: 实际上可以用函数polygon,它可以很好地填充颜色,但我懒)
后话
为啥我没法做扫雷呢?因为获取到的鼠标位置和图像的位置对不上,这个问题我至今未解决。
附录:完整代码
maze <- function(size, cex_set = 2.5){
# 1是墙体,0是通路
size2 = 2*size -3
block_map = matrix(0, size, size)
maze_map = matrix(1, size2, size2)
for(i in 1:(size-2)){
for(j in 1:(size-2)){
maze_map[2*i, 2*j]=0
}
}
block_map[1, ] = 1
block_map[size, ] = 1
block_map[, 1] = 1
block_map[, size] = 1
move=list(c(-1, 0), c(1, 0), c(0, -1), c(0, 1))
in_map <- function(x, y){
return((1<=x) && (x<=size) && (1<=y) && (y<=size))
}
connect <- function(x, y, xx, yy){
maze_map[(x+xx)-2, (y+yy)-2] <<- 0
}
neighbor_count <- function(x, y){
temp = 0
for(i in 1:4){
if(in_map(x + move[[i]][1], y + move[[i]][2])){
if(block_map[x + move[[i]][1], y + move[[i]][2]] == 1){
temp = temp + 1
}
}
}
return(temp)
}
dfs <- function(x, y){
print(c(x, y, neighbor_count(x, y)))
if(neighbor_count(x, y) == 4){return}
direction = c(FALSE, FALSE, FALSE, FALSE)
while(neighbor_count(x, y) < 4){
temp = -1
while(temp == -1 || direction[temp] == TRUE ){
temp = sample(1:4, 1)
}
xx=x+move[[temp]][1]
yy=y+move[[temp]][2]
if(in_map(xx, yy) && block_map[xx, yy] == 0){
block_map[xx, yy] <<- 1
connect(x, y, xx, yy)
dfs(xx, yy)
direction[temp]=TRUE
if(neighbor_count(x, y) == 4){
return
}
}
}
return
}
# 设起点,并开始生成地图矩阵
block_map[2, 2] = 1
dfs(2, 2)
# 生成地图
windows()
plot(0,0,xlim=c(0, size2),ylim=c(0, size2),type='n',xaxs="i", yaxs="i")
for(i in 1:(size2 -1)){
abline(h=i,col="gray60") # 水平线
abline(v=i,col="gray60")
}
abline(h=size2)
abline(v=size2)
for(i in 1:size2){
for(j in 1:size2){
if(maze_map[i, j]==1){
points(i-0.5, j-0.5, col = 8, pch = 15, cex = cex_set)
}
else{
points(i-0.5, j-0.5, col = 7, pch = 15, cex = cex_set)
}
}
}
now.x = 2
now.y = 2
dest.x = size2 - 1
dest.y = size2 - 1
points(now.x-0.5, now.y-0.5, col = 2, pch = 15, cex = cex_set)
points(dest.x-0.5, dest.y-0.5, col = 6, pch = 15, cex = cex_set)
keydown<-function(K){
K = tolower(K)
print(K)
if(K == "down"){
if(now.y > 2 && maze_map[now.x, now.y-1] == 0){
points(now.x-0.5, now.y-0.5, col = 7, pch = 15, cex = cex_set)
now.y <<- now.y - 1
points(now.x-0.5, now.y-0.5, col = 2, pch = 15, cex = cex_set)
}
}
if(K == "up"){
if(now.y < size2 - 1 && maze_map[now.x, now.y+1] == 0){
points(now.x-0.5, now.y-0.5, col = 7, pch = 15, cex = cex_set)
now.y <<- now.y + 1
points(now.x-0.5, now.y-0.5, col = 2, pch = 15, cex = cex_set)
}
}
if(K == "left"){
if(now.x > 2 && maze_map[now.x - 1, now.y] == 0){
points(now.x-0.5, now.y-0.5, col = 7, pch = 15, cex = cex_set)
now.x <<- now.x - 1
points(now.x-0.5, now.y-0.5, col = 2, pch = 15, cex = cex_set)
}
}
if(K == "right"){
if(now.x < size2 - 1 && maze_map[now.x + 1, now.y] == 0){
points(now.x-0.5, now.y-0.5, col = 7, pch = 15, cex = cex_set)
now.x <<- now.x + 1
points(now.x-0.5, now.y-0.5, col = 2, pch = 15, cex = cex_set)
}
}
if(now.x == dest.x && now.y == dest.y){
text(2, 2, label="You Win", cex = 3)
getGraphicsEvent(onKeybd = NULL)
}
}
getGraphicsEvent(onKeybd = keydown)
}
maze(16)