查看原文
其他

R语言游戏框架设计

2017-03-29 张丹 R语言中文社区

前言

本文继续上一篇文章 R语言游戏之旅 贪食蛇入门。当我们完成了贪食蛇游戏之后,我们应该把代码进一步整理,抽象出游戏中通用的部分,分离游戏框架代码和贪食蛇游戏代码。我们就可以提取出一个R语言的游戏开发引擎,当再开发新的游戏时,只要关心游戏本身的程序设计就行了。


目录

  1. 贪食蛇的面向对象改造

  2. 游戏框架定义

  3. 在框架中重新实现贪食蛇游戏


1. 贪食蛇的面向对象改造

我们可以利用面向对象的方法论,对贪食蛇游戏进行抽象整理,并实现代码的面向对象的改造。R语言支持3种面向对象的编程实现方式,我选择基于RC的面向对象编程,关于RC详细使用请参考文章:R语言基于RC的面向对象编程。


由于我们之前的代码都是通过函数来封装的,所以代码重构还是比较简单的,只要把Snake对象的属性和方法定义清楚就行了。


1.1 定义Snake类

定义Snake类,画出类图,包括属性和方法。



属性解释:

  • name:游戏的名字

  • stage:当前的游戏场景

  • e:游戏中的变量,environment类型

  • m:游戏地图矩阵

  • width:矩阵的宽

  • height:矩阵的高


方法解释:

  • initialize():构建函数,用于RC类的初始化

  • init():给stage1场景初始化游戏变量

  • fail():失败查询

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

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

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

  • drawTable():绘制游戏背景。

  • drawMatrix():绘制游戏矩阵。

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

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

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

  • keydown():监听键盘事件。

  • run():启动函数


1.2 全局函数调用顺序图

接下来,根据UML规范画出顺序图,主要包括全局函数调用和stage1场景游戏环境调用。

全局函数调用关系。

  1. 通过run()函数启动游戏,进入stage0场景,注册键盘事件。

  2. 在stage0场景按任意键切换到stage1场景。

  3. init()出始化stage1场景的游戏变量。

  4. stage1()运行游戏

  5. 当游戏失败fail()或按q键

  6. 游戏进行stage2场景,显示游戏结束画面,

  7. 按空格键回到stage0重新开始,按q键退出程序。




1.3 stage1场景游戏环境函数调用顺序图

stage1场景游戏环境函数调用关系。

  1. 游戏进入stage1场景,按上下左右(up,down,left,right)方向键操作蛇头的前进路线。

  2. furit()函数检查,如果地图上水果被吃掉,生成一个新水果,记录到矩阵中。

  3. head()函数,通过上下左边键的操作,进行蛇头的移动,记录到矩阵中。

  4. fail()函数失败检查,no未失败继续,yes失败进行stage2场景。

  5. body()函数,蛇身体移动,记录到矩阵中。

  6. drawTable()函数,画出游戏背景画布。

  7. drawMatrix()函数,画出游戏矩阵。


利用UML的方法,通过类图和顺序图的描述,我们就把贪食蛇的游戏程序进行了面向对象的设计改造。不用着急去写代码,我们再想想如何进行游戏框架的提取。


2. 游戏框架定义

要设计一个完整、易用、有良好扩展的游戏框架是比难的,但我们可以基于贪食蛇的游戏,一步一步来做抽象。抽象过程就是把程序对象化,上面的我们已经做了;第二步再把公用的属性和方法提取封装,可以统一把公用的部分提取到一个Game的父类里面,让Snake类继承Game类,从而实现游戏框架定义。我们画出Game类和Snake类的类图。


  • Game类公共属性,包括了所有的Snake类的属性,这是因为这些属性都是全局的,其他的游戏也会用到,而且每个游戏中的属性,可以在e中进行定义。

  • Game类公共方法,包括了游戏全局调用的方法,但不包括Snake游戏stage1场景中运行的方法。在Game类的方法中,我们主要实现的都是开发的辅助功能。

  • Snake类方法,同样还有Game类的方法,这是用到方法的重写技术。子类的方法,先调用父类的同名方法,然后再执行子类方法里的程序。

这样我们就简单地分离了游戏框架Game类和游戏实现Snake类,下面我们要做的就是把代码按照设计进行实现,看看我们的设计是否是合理的。


3. 在框架中重新实现贪食蛇游戏

Game类的代码实现,用R语言的RC面向对象编程进行代码编写。

Game<-setRefClass('Game',
                 
   fields=list(
     # 系统变量
     name="character", # 名字
     debug='logical',  # 调试状态
     width='numeric',  # 矩阵宽
     height='numeric', # 矩阵高
     
     # 应用变量
     stage='numeric',  # 场景
     e='environment',  # 环境空间变量
     m='matrix',       # 数据矩阵
     isFail='logical'  # 游戏失败
   ),
                 
   methods=list(
     
     # 构造函数
     initialize = function(name,width,height,debug) {
       name<<-"R Game Framework"
       debug<<-FALSE
       width<<-height<<-20   #矩阵宽高
     },
     
     # 初始化变量
     init = function(){
       e<<-new.env()   #环境空间
       m<<-matrix(rep(0,width*height),nrow=width)  #数据矩阵
       isFail<<-FALSE
     },
     
     # 开机画图
     stage0=function(){
       stage<<-0
       init()
     },
     
     # 结束画图
     stage2=function(){
       stage<<-2
     },
     
     # 游戏中
     stage1=function(default=FALSE){
       stage<<-1
       if(FALSE){  # 默认游戏中界面
         plot(0,0,xlim=c(0,1),ylim=c(0,1),type='n',xaxs="i", yaxs="i")
         text(0.5,0.7,label="Playing",cex=5)  
       }
     },
     
     # 矩阵工具
     index = function(col) {
       return(which(m==col))
     },
     
     # 失败操作
     fail=function(msg){
       print(paste("Game Over",msg))
       isFail<<-TRUE
       keydown('q')
       return(NULL)
     },
     
     # 键盘事件,控制场景切换
     keydown=function(K){
       if(stage==0){ #开机画面
         stage1()
         return(NULL)
       }  
       
       if(stage==2){ #结束画面
         if(K=="q") q()
         else if(K==' ') stage0()  
         return(NULL)
       }
     },
     
     # 启动程序
     run=function(){
       par(mai=rep(0,4),oma=rep(0,4))
       stage0()
       getGraphicsEvent(prompt="Snake Game",onKeybd=function(K){
         if(debug) print(paste("keydown",K))  
         return(keydown(K))
       })
     }
   )                  
)

Snake类的代码实现,继承Game类,并实现贪食蛇游戏的私有方法。


# 引用game.r文件
source(file="game.r")

# Snake类,继承Game类
Snake<-setRefClass("Snake",contains="Game",
                 
  methods=list(
   
    # 构造函数
    initialize = function(name,width,height,debug) {
      callSuper(name,width,height,debug) # 调父类
     
      name<<-"Snake Game"
    },
   
    # 初始化变量
    init = function(){
      callSuper()  # 调父类
     
      e$step<<-1/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$col_barrier<<-1 #障碍颜色
    },
   
    # 失败检查
    lose=function(){
      # head出边界
      if(length(which(e$head<1))>0 | length(which(e$head>width))>0){
        fail("Out of ledge.")
        return(NULL)
      }
     
      # head碰到tail
      if(m[e$head[1],e$head[2]]==e$col_tail){
        fail("head hit tail.")
        return(NULL)
      }
    },
   
    # 随机的水果点
    furit=function(){
      if(length(index(e$col_furit))<=0){ #不存在水果
        idx<-sample(index(e$col_path),1)
       
        fx<-ifelse(idx%%width==0,10,idx%%width)
        fy<-ceiling(idx/height)
        m[fx,fy]<<-e$col_furit
       
        if(debug){
          print(paste("furit idx",idx))
          print(paste("furit axis:",fx,fy))
        }
      }
    },
   
    # 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(){
      if(isFail) return(NULL)
     
      m[e$lastx,e$lasty]<<-e$col_path
      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))
        m[e$tail[1,]$x,e$tail[1,]$y]<<-e$col_path
        e$tail<<-e$tail[-1,]
        m[e$lastx,e$lasty]<<-e$col_tail
      }
     
      if(debug){
        print(paste("snake idx",index(e$col_head)))
        print(paste("snake axis:",e$head[1],e$head[2]))  
      }
    },
   
    # 画布背景
    drawTable=function(){
      if(isFail) return(NULL)
     
      plot(0,0,xlim=c(0,1),ylim=c(0,1),type='n',xaxs="i", yaxs="i")
     
      if(debug){
        # 显示背景表格
        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),width),y=rep(seq(0,0.95,e$step),each=height),lab=seq(1,width*height))
        text(df$x+e$step/2,df$y+e$step/2,label=df$lab)
      }
    },
   
    # 根据矩阵画数据
    drawMatrix=function(){
      if(isFail) return(NULL)
     
      idx<-which(m>0)
      px<- (ifelse(idx%%width==0,width,idx%%width)-1)/width+e$step/2
      py<- (ceiling(idx/height)-1)/height+e$step/2
      pxy<-data.frame(x=px,y=py,col=m[idx])
      points(pxy$x,pxy$y,col=pxy$col,pch=15,cex=4.4)
    },
   
    # 游戏场景
    stage1=function(){
      callSuper()
     
      furit()
      head()
      lose()
      body()
      drawTable()
      drawMatrix()  
    },
   
    # 开机画图
    stage0=function(){
      callSuper()
      plot(0,0,xlim=c(0,1),ylim=c(0,1),type='n',xaxs="i", yaxs="i")
      text(0.5,0.7,label=name,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(){
      callSuper()
      info<-paste("Congratulations! You have eat",nrow(e$tail),"fruits!")
      print(info)
     
      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=info,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){
      callSuper(K)
     
      if(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)
      }
      return(NULL)
    }
  )                  
)

snake<-function(){
 game<-Snake$new()
 game$initFields(debug=TRUE)
 game$run()
}

snake()

最后,我们运行snake.r的程序,就完成了整个贪食蛇游戏。游戏运行效果,可以查看上一篇文章,R语言游戏之旅 贪食蛇入门 的游戏截图和动画。

对于贪食蛇游戏的开发,我们已经完成了,虽然界面还是比较土,而且没有时间维度的操作。那么我们换一个角度思考,如果不太要求画面效果,而且不需要时间维度的游戏,是不是R语言会表现的更好呢?当然,这类游戏也有很多,比如最近流行的2048。那么接下来,就用我们的游戏框架,再来做一个2048的游戏吧,试试在100行之内实现。


作者介绍:
张丹,R语言中文社区专栏特邀作者,《R的极客理想》系列图书作者,民生银行大数据中心数据分析师,前况客创始人兼CTO。
10年IT编程背景,精通R ,Java, Nodejs 编程,获得10项SUN及IBM技术认证。丰富的互联网应用开发架构经验,金融大数据专家。个人博客 http://fens.me, Alexa全球排名70k。
著有《R的极客理想-工具篇》、《R的极客理想-高级开发篇》,合著《数据实践之美》,新书《R的极客理想-量化投资篇》(即将出版)。


作者书籍推荐:(阅读原文即可购买)

R的极客理想-工具篇京东购买快速通道:

    https://item.jd.com/11524750.html

R的极客理想-高级开发篇京东购买快速通道:

    https://item.jd.com/11731967.html

数据实践之美京东购买快速通道:    

    https://item.jd.com/12106224.html

R的极客理想-量化投资篇》(即将出版)

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存