R语言游戏之旅 贪食蛇入门

R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大。

R语言作为统计学一门语言,一直在小众领域闪耀着光芒。直到大数据的爆发,R语言变成了一门炙手可热的数据分析的利器。随着越来越多的工程背景的人的加入,R语言的社区在迅速扩大成长。现在已不仅仅是统计领域,教育,银行,电商,互联网….都在使用R语言。

要成为有理想的极客,我们不能停留在语法上,要掌握牢固的数学,概率,统计知识,同时还要有创新精神,把R语言发挥到各个领域。让我们一起动起来吧,开始R的极客理想。

关于作者:

  • 张丹(Conan), 程序员Java,R,PHP,Javascript
  • weibo:@Conan_Z
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/r-game-snake/

snake-title

前言

用R语言进行统计分析不神奇,用R语言做分类算法不神奇,用R语言做可视也不神奇,你见过用R语言做的游戏吗?

本文将带你进入R语言的游戏开发,用R语言实现贪食蛇游戏。

目录

  1. 贪食蛇游戏介绍
  2. 场景设计
  3. 程序设计
  4. R语言实现

1. 贪食蛇游戏介绍

贪食蛇是一个产生于1970年代中后期的计算机游戏。此类游戏在1990年代由于一些小屏幕设备引入而再度流行起來,在现在的手机上基本都可安装此小游戏。

在游戏中,玩家操控一条细长的直线蛇,它会不停前进,玩家只能操控蛇的头部朝向(上下左右),一路拾起触碰到之物(水果),并要避免触碰到自身或者其他障碍物。每次贪食蛇吃掉一件食物,它的身体便增长一些。吃掉一些食物后会使蛇的移動速度逐漸加快,让游戏的难度渐渐变大。游戏设置分为四面都有墙,并且不可以穿越,蛇头碰到墙或障碍物时,游戏结束。以游戏过程吃到的水果,得分。 贪食蛇游戏,在各种设备上都有实现,已经有很多种版本。

snake0

2. 场景设计

要开发这款游戏,我们应该如何动手呢?首先,我们需要从软件开发的角度,对这款游戏进行需求分析,列出游戏的规则,并设计业务流程,给出游戏的原型,验证是否可行。

2.1 需求分析

贪食蛇游戏,应该有3个场景:开机场景,游戏场景,结束场景。

  • 开机场景:运行程序,在游戏前,给用户做准备,并提示如何操作游戏。
  • 游戏场景:游戏运行中的场景。
  • 结束场景:当用户胜利、失败或退出时的场景,并提示用户在游戏中的得分。

开机场景和结束场景比较简单,不再解释。游戏场景,包括一块画布,一条蛇,一个蛇头和一个不定长的蛇尾,一个水果,边界和障碍物。

2.2 游戏规则

游戏进行时的规则:

  • 1. 开始游戏后,用户可以通过上(up)下(down)左(left)右(right)键,来操作蛇头,控制蛇的前进方向,还可以按q键直接游戏失败,其他的键盘操作无效。
  • 2. 蛇头用蓝色标识,蛇尾用灰色标识,水果用红色标识,障碍物用黑色标识。
  • 3. 当蛇头移动到水果的位置后,表示蛇吃到了水果,蛇尾的长度加1。水果会在下一次蛇头移动后,在空路径上自动生成。
  • 4. 游戏画布的外围是枪,当蛇头移动到画布看不到的位置,则表示蛇头撞到枪,游戏失败。
  • 5. 游戏画面中,有一些黑色障碍物,当蛇头碰到障碍,游戏失败。
  • 6. 当蛇头碰到蛇尾时,游戏失败。

2.3 业务流程

场景切换的流程:

  • 打开程序时,用户首先看到开机场景,按任意键后进入游戏场景。
  • 在游戏场景,当游戏失败,进入结束场景;按q键,则直接游戏失败。
  • 在结束场景,按空格回到开机场景;按q键,则直接能出软件。

snake-process

2.4 游戏原型

我们画出3个场景的界面。左边为开机场景,中间是游戏场景,右边是结束场景。

snake-stage

我们根据游戏原型的图,用程序画出游戏的场景。

3. 程序设计

通过上面的功能需求分析,我们已经非常清楚地了解贪食游戏的各种规则和功能。接下来,我们要把需求分析中的业务语言,通过技术语言重新描述,并考虑非功能需求,以及R语言相关的技术细节。

3.1 游戏场景

我们让每个场景对应于一块画布,及每个场景对应一个内存结构。

  • 开机场景,是静态的,我们可以提前生成好这块画布存储起来,也可以当用户切换时再临时生成,性能开销不大。
  • 游戏场景,是动态的,每进行一次用户的交互行为或按时间刷新时,都需要求重新绘制画布,让游戏场景通过绑定事件来生成画布。由于用户会频繁操作,因此性能开销比较大。
  • 结束场景,是动态的,在结束场景会显示当次游戏的得分,需要在切换时临时生成。

3.2 游戏对象

在游戏进行中,会产生很多的对象,如上文中提到的。这些对象都需要在内存中进行定义,匹配到对应程序语言的数据类型。

画布对象:

  • 画布:用矩阵来描述,画布中每个小方块对应到矩阵中一个数据。
  • 画布大小:画布的长和宽,分别用对应两个数字变量。
  • 画布坐标:用于画布内小格子的定位,从左到右横坐标是1到20,从底到顶纵坐标为1到20。
  • 画布索引:用于画布内小格式的定位,按从左到右,从底到顶的顺序,为1到400。
  • 方格:在画布里最小的单位是方格,按照画面的比例,设置方格的大小。

matrix

蛇对象:

  • 蛇头:用一个向量来描述,只有一个方格。游戏开始时,起点位置为坐标(2,2),默认蛇头向上移动,用户打开界面显示位置为(2,3)。
  • 蛇尾:用数据框来描述,存储不定长度的坐标向量。游戏开始时,蛇尾长度是0。

水果对象:

  • 水果:用一个向量来描述,只有一个方格。游戏开始时,随机在空格式上,选一个坐标为水果位置。

边界和障碍物:

  • 边界:无内存描述,通过计算判断。当蛇头坐标超过矩阵坐标时,触发边界。
  • 障碍物:用数据框来描述,存储不定长度的坐标向量。

3.3 游戏事件
游戏过程中,会有3种事件,键盘事件、时间事件和碰撞事件。

  • 键盘事件:全局事件,用户通过键盘输入,而触发的事件,比如,上下左右控制蛇的移动方向。
  • 时间事件:全局事件,系统计时以每0.2秒触发一个时间事件,比如,蛇头每0.2秒的移动一格。
  • 碰撞事件:当蛇头移动时,与非空和格式碰撞除法的事情,比如,吃到水果,蛇头撞到蛇尾。

通常情况,上面3种事件分别有3个线程来控制。但由于R语言本身是单线程的设计,而且不支持异步调用,因此我们无法同时实现上面的3个事件监听。取一种折中方案为,全局监听键盘事件,通过键盘事件触发碰撞事件的进行检查。

3.4 游戏控制

在游戏进行中,每个状态我们都需要进行控制的。比如,什么时候生成新的水果,什么时候增加一节尾巴,什么游戏结束等。通过定义控制函数,可以方便我们管理游戏运行中的各种游戏状态。

program

上图中每个方块代表一个R语言函数定义:

  • run():启动程序。
  • keydown():监听键盘事件,锁定线程。
  • stage0():创建开机场景,可视化输出。
  • stage1():创建游戏场景,可视化输出。
  • stage2():创建结束场景,可视化输出。
  • init():打开游戏场景时,初始化游戏变量。
  • furit():判断并生成水果坐标。
  • head():生成蛇头移动坐标。
  • fail():失败查询,判断蛇头是否撞墙或蛇尾,如果失败则跳过画图,进入结束场景。
  • body():生成蛇尾移动坐标。
  • drawTable():绘制游戏背景。
  • drawMatrix():绘制游戏矩阵。

通过程序设计过程,我们就把需求分析中的业务语言描述,变成了程序开发中的技术语言描述。经过完整的设计后,最后就剩下写代码了。

4. R语言实现

用R语言写代码,其实没有几行就可以搞定,按照上面的函数定义,我们把代码像填空一样地写进去就行了。当然,在写代码的过程中,我们需要掌握一些R语言特性,让代码更健壮。

run()函数,启动程序。


run<-function(){
  # 设置全局画布无边
  par(mai=rep(0,4),oma=rep(0,4))

  # 定义全局环境空间,用于封装变量
  e<<-new.env()

  # 启动开机场景
  stage0()
  
  # 注册键盘事件
  getGraphicsEvent(prompt="Snake Game",onKeybd=keydown)
}

上面代码中,通过定义环境空间e来存储变量,可以有效的解决变量名冲突,和变量污染的问题,关于环境空间的介绍,请参考文章:揭开R语言中环境空间的神秘面纱解密R语言函数的环境空间

keydown函数,监听键盘事件。


keydown<-function(K){
  print(paste("keydown:",K,",stage:",e$stage));
  
  if(e$stage==0){ #开机画面
    init()
    stage1()
    return(NULL)
  }  
  
  if(e$stage==2){ #结束画面
    if(K=="q") q()
    else if(K==' ') stage0()  
    return(NULL)
  } 
  
  if(e$stage==1){ #游戏中
    if(K == "q") {
      stage2()
    } else {
      if(tolower(K) %in% c("up","down","left","right")){
        e$lastd<-e$dir
        e$dir<-tolower(K)
        stage1()  
      }
    }
  }
  return(NULL)
}

代码中,参数K为键盘输入。通过对当前所在场景,与键盘输入的条件判断,来确定键盘事件的响应。在游戏中,键盘只响应5个键 "up","down","left","right","q"。

stage0():创建开机场景,可视化输出。


# 开机画图
stage0<-function(){
  e$stage<-0
  plot(0,0,xlim=c(0,1),ylim=c(0,1),type='n',xaxs="i", yaxs="i")
  text(0.5,0.7,label="Snake Game",cex=5)
  text(0.5,0.4,label="Any keyboard to start",cex=2,col=4)
  text(0.5,0.3,label="Up,Down,Left,Rigth to control direction",cex=2,col=2)
  text(0.2,0.05,label="Author:DanZhang",cex=1)
  text(0.5,0.05,label="http://blog.fens.me",cex=1)
}

stage2():创建结束场景,可视化输出。


# 结束画图
stage2<-function(){
  e$stage<-2
  plot(0,0,xlim=c(0,1),ylim=c(0,1),type='n',xaxs="i", yaxs="i")
  text(0.5,0.7,label="Game Over",cex=5)
  text(0.5,0.4,label="Space to restart, q to quit.",cex=2,col=4)
  text(0.5,0.3,label=paste("Congratulations! You have eat",nrow(e$tail),"fruits!"),cex=2,col=2)
  text(0.2,0.05,label="Author:DanZhang",cex=1)
  text(0.5,0.05,label="http://blog.fens.me",cex=1)
}

init():打开游戏场景时,初始化游戏变量。


# 初始化环境变量
init<-function(){
  e<<-new.env()
  e$stage<-0 #场景
  e$width<-e$height<-20  #切分格子
  e$step<-1/e$width #步长
  e$m<-matrix(rep(0,e$width*e$height),nrow=e$width)  #点矩阵
  e$dir<-e$lastd<-'up' # 移动方向
  e$head<-c(2,2) #初始蛇头
  e$lastx<-e$lasty<-2 # 初始化蛇头上一个点
  e$tail<-data.frame(x=c(),y=c())#初始蛇尾
  
  e$col_furit<-2 #水果颜色
  e$col_head<-4 #蛇头颜色
  e$col_tail<-8 #蛇尾颜色
  e$col_path<-0 #路颜色
}

代码中,初始化全局的环境空间e,然后将所有需要的变量,定义在e中。

furit():判断并生成水果坐标。


 # 随机的水果点
  furit<-function(){
    if(length(index(e$col_furit))<=0){ #不存在水果
      idx<-sample(index(e$col_path),1)
      
      fx<-ifelse(idx%%e$width==0,10,idx%%e$width)
      fy<-ceiling(idx/e$height)
      e$m[fx,fy]<-e$col_furit
      
      print(paste("furit idx",idx))
      print(paste("furit axis:",fx,fy))
    }
  }

fail():失败查询,判断蛇头是否撞墙或蛇尾,如果失败则跳过画图,进入结束场景。


 # 检查失败
  fail<-function(){
    # head出边界
    if(length(which(e$head<1))>0 | length(which(e$head>e$width))>0){
      print("game over: Out of ledge.")
      keydown('q')
      return(TRUE)
    }
    
    # head碰到tail
    if(e$m[e$head[1],e$head[2]]==e$col_tail){
      print("game over: head hit tail")
      keydown('q')
      return(TRUE)
    }
    
    return(FALSE)
  }

head():生成蛇头移动坐标。


  # snake head
  head<-function(){
    e$lastx<-e$head[1]
    e$lasty<-e$head[2]
    
    # 方向操作
    if(e$dir=='up') e$head[2]<-e$head[2]+1
    if(e$dir=='down') e$head[2]<-e$head[2]-1
    if(e$dir=='left') e$head[1]<-e$head[1]-1
    if(e$dir=='right') e$head[1]<-e$head[1]+1
    
  }

body():生成蛇尾移动坐标。


  # snake body
  body<-function(){
    e$m[e$lastx,e$lasty]<-0
    e$m[e$head[1],e$head[2]]<-e$col_head #snake
    if(length(index(e$col_furit))<=0){ #不存在水果
      e$tail<-rbind(e$tail,data.frame(x=e$lastx,y=e$lasty))
    }
    
    if(nrow(e$tail)>0) { #如果有尾巴
      e$tail<-rbind(e$tail,data.frame(x=e$lastx,y=e$lasty))
      e$m[e$tail[1,]$x,e$tail[1,]$y]<-e$col_path
      e$tail<-e$tail[-1,]
      e$m[e$lastx,e$lasty]<-e$col_tail
    }
    
    print(paste("snake idx",index(e$col_head)))
    print(paste("snake axis:",e$head[1],e$head[2]))
  }

drawTable():绘制游戏背景。


 # 画布背景
  drawTable<-function(){
    plot(0,0,xlim=c(0,1),ylim=c(0,1),type='n',xaxs="i", yaxs="i")
    
    # 显示背景表格
    abline(h=seq(0,1,e$step),col="gray60") # 水平线
    abline(v=seq(0,1,e$step),col="gray60") # 垂直线
    # 显示矩阵
    df<-data.frame(x=rep(seq(0,0.95,e$step),e$width),y=rep(seq(0,0.95,e$step),each=e$height),lab=seq(1,e$width*e$height))
    text(df$x+e$step/2,df$y+e$step/2,label=df$lab)
  }

drawMatrix():绘制游戏矩阵。


  # 根据矩阵画数据
  drawMatrix<-function(){
    idx<-which(e$m>0)
    px<- (ifelse(idx%%e$width==0,e$width,idx%%e$width)-1)/e$width+e$step/2
    py<- (ceiling(idx/e$height)-1)/e$height+e$step/2
    pxy<-data.frame(x=px,y=py,col=e$m[idx])
    points(pxy$x,pxy$y,col=pxy$col,pch=15,cex=4.4)
  }

stage1():创建游戏场景,stage1()函数内部,封装了游戏场景运行时的函数,并进行调用。


# 游戏中
stage1<-function(){
  e$stage<-1
  furit<-function(){...} //见furit
  fail<-function(){...} //见fail
  head<-function(){...} //见head
  body<-function(){...}//见body
  drawTable<-function(){...} //见drawTable
  drawMatrix<-function(){...} //见drawMatrix

  # 运行函数
  furit()
  head()
  if(!fail()){ #失败检查
    body()
    drawTable()
    drawMatrix()  
  }
}

注:此处代码为伪代码。

最后,是完整的程序代码。


# 初始化环境变量
init<-function(){
  e<<-new.env()
  e$stage<-0 #场景
  e$width<-e$height<-20  #切分格子
  e$step<-1/e$width #步长
  e$m<-matrix(rep(0,e$width*e$height),nrow=e$width)  #点矩阵
  e$dir<-e$lastd<-'up' # 移动方向
  e$head<-c(2,2) #初始蛇头
  e$lastx<-e$lasty<-2 # 初始化蛇头上一个点
  e$tail<-data.frame(x=c(),y=c())#初始蛇尾
  
  e$col_furit<-2 #水果颜色
  e$col_head<-4 #蛇头颜色
  e$col_tail<-8 #蛇尾颜色
  e$col_path<-0 #路颜色
}


# 获得矩阵的索引值
index<-function(col) which(e$m==col)

# 游戏中
stage1<-function(){
  e$stage<-1
  
  # 随机的水果点
  furit<-function(){
    if(length(index(e$col_furit))<=0){ #不存在水果
      idx<-sample(index(e$col_path),1)
      
      fx<-ifelse(idx%%e$width==0,10,idx%%e$width)
      fy<-ceiling(idx/e$height)
      e$m[fx,fy]<-e$col_furit
      
      print(paste("furit idx",idx))
      print(paste("furit axis:",fx,fy))
    }
  }
  
  
  # 检查失败
  fail<-function(){
    # head出边界
    if(length(which(e$head<1))>0 | length(which(e$head>e$width))>0){
      print("game over: Out of ledge.")
      keydown('q')
      return(TRUE)
    }
    
    # head碰到tail
    if(e$m[e$head[1],e$head[2]]==e$col_tail){
      print("game over: head hit tail")
      keydown('q')
      return(TRUE)
    }
    
    return(FALSE)
  }
  
  
  # snake head
  head<-function(){
    e$lastx<-e$head[1]
    e$lasty<-e$head[2]
    
    # 方向操作
    if(e$dir=='up') e$head[2]<-e$head[2]+1
    if(e$dir=='down') e$head[2]<-e$head[2]-1
    if(e$dir=='left') e$head[1]<-e$head[1]-1
    if(e$dir=='right') e$head[1]<-e$head[1]+1
    
  }
  
  # snake body
  body<-function(){
    e$m[e$lastx,e$lasty]<-0
    e$m[e$head[1],e$head[2]]<-e$col_head #snake
    if(length(index(e$col_furit))<=0){ #不存在水果
      e$tail<-rbind(e$tail,data.frame(x=e$lastx,y=e$lasty))
    }
    
    if(nrow(e$tail)>0) { #如果有尾巴
      e$tail<-rbind(e$tail,data.frame(x=e$lastx,y=e$lasty))
      e$m[e$tail[1,]$x,e$tail[1,]$y]<-e$col_path
      e$tail<-e$tail[-1,]
      e$m[e$lastx,e$lasty]<-e$col_tail
    }
    
    print(paste("snake idx",index(e$col_head)))
    print(paste("snake axis:",e$head[1],e$head[2]))
  }
  
  # 画布背景
  drawTable<-function(){
    plot(0,0,xlim=c(0,1),ylim=c(0,1),type='n',xaxs="i", yaxs="i")
  }
  
  # 根据矩阵画数据
  drawMatrix<-function(){
    idx<-which(e$m>0)
    px<- (ifelse(idx%%e$width==0,e$width,idx%%e$width)-1)/e$width+e$step/2
    py<- (ceiling(idx/e$height)-1)/e$height+e$step/2
    pxy<-data.frame(x=px,y=py,col=e$m[idx])
    points(pxy$x,pxy$y,col=pxy$col,pch=15,cex=4.4)
  }
  
  furit()
  head()
  if(!fail()){
    body()
    drawTable()
    drawMatrix()  
  }
}


# 开机画图
stage0<-function(){
  e$stage<-0
  plot(0,0,xlim=c(0,1),ylim=c(0,1),type='n',xaxs="i", yaxs="i")
  text(0.5,0.7,label="Snake Game",cex=5)
  text(0.5,0.4,label="Any keyboard to start",cex=2,col=4)
  text(0.5,0.3,label="Up,Down,Left,Rigth to control direction",cex=2,col=2)
  text(0.2,0.05,label="Author:DanZhang",cex=1)
  text(0.5,0.05,label="http://blog.fens.me",cex=1)
}

# 结束画图
stage2<-function(){
  e$stage<-2
  plot(0,0,xlim=c(0,1),ylim=c(0,1),type='n',xaxs="i", yaxs="i")
  text(0.5,0.7,label="Game Over",cex=5)
  text(0.5,0.4,label="Space to restart, q to quit.",cex=2,col=4)
  text(0.5,0.3,label=paste("Congratulations! You have eat",nrow(e$tail),"fruits!"),cex=2,col=2)
  text(0.2,0.05,label="Author:DanZhang",cex=1)
  text(0.5,0.05,label="http://blog.fens.me",cex=1)
}

# 键盘事件
keydown<-function(K){
  print(paste("keydown:",K,",stage:",e$stage));
  
  if(e$stage==0){ #开机画面
    init()
    stage1()
    return(NULL)
  }  
  
  if(e$stage==2){ #结束画面
    if(K=="q") q()
    else if(K==' ') stage0()  
    return(NULL)
  } 
  
  if(e$stage==1){ #游戏中
    if(K == "q") {
      stage2()
    } else {
      if(tolower(K) %in% c("up","down","left","right")){
        e$lastd<-e$dir
        e$dir<-tolower(K)
        stage1()  
      }
    }
  }
  return(NULL)
}

#######################################
# RUN  
#######################################  

run<-function(){
  par(mai=rep(0,4),oma=rep(0,4))
  e<<-new.env()
  stage0()
  
  # 注册事件
  getGraphicsEvent(prompt="Snake Game",onKeybd=keydown)
}

run()

游戏截图:

snake

全部代码仅仅190行,有效代码行只有100行左右,我们就实现了贪食蛇游戏。当然,时间事件我们没有实现,只因为R语言本身的单线程机制,而且不支持异步调用。正因为R语言强大的数据处理能力和可视化能力,让我们的程序写起来非常简单。我想如果让R来实现策略类游戏的矩阵部分的计算,一定会非常顺手的。

有了贪食蛇游戏的雏形,再通过面向对象的封装,能不能归纳出一个基于R语言游戏的开发框架呢?下一篇文章将继续R语言游戏之旅,R语言游戏框架设计

转载请注明出处:
http://blog.fens.me/r-game-snake/

打赏作者

This entry was posted in R语言实践, 游戏

0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

12 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
acheng99

这是要出书的节奏。。。啥时候出版呢?看了你很多的博客,值得期待哈

Conan Zhang

呵呵,这么晚都没有睡啊,是不是看球呢!!

Kelvin Jiang

牛人阿

Conan Zhang

过奖。

[…] R语言游戏之旅 贪食蛇入门 […]

[…] R语言游戏之旅 贪食蛇入门, R语言游戏框架设计, R语言游戏之旅 […]

kelly

试了您的代码显示“图形设备不支持事件处理”,所以mac的R不可以?

Conan Zhang

目前应该只支持win下运行,Linux有字体的错误,Mac没有测试过。

Win下,可以从github安装这个包。
https://github.com/bsspirit/gridgame

library(devtools)
install_github(“gridgame”,”bsspirit”)
snake() # start snake game.
g2048() # start 2048 game.

Chen Liu

你好,在win下尝试了您的代码,同样显示:

Error in setGraphicsEventEnv(which, as.environment(list(…))) :
this graphics device does not support event handling

请问是什么原因呢?

Chen Liu

你好,我先前在Rstudio里面运行不支持,但是今天在R里面尝试已经成功了:-),应该是Rstudio的问题。

Conan Zhang

Rstudio有一个GUI的设置,他重置了grDevices库。

Conan Zhang

R的3.2.2版本以后,R就就可以了,等Rstudio更新那个配置就行了。

12
0
Would love your thoughts, please comment.x
()
x