分享 Ruby + GTK 3 实现经典小游戏,坦克大战

ysjrdfaps · 2017年06月14日 · 最后由 pynix 回复于 2018年04月16日 · 9752 次阅读
本帖已被管理员设置为精华贴

Ruby 学习经历:业余学习编程,挺喜欢 Ruby 的语法,学习了一段时间

1、听说在 Windows 上 Ruby on Rails 非常不好整,尝试了安装 Ruby on Rails,也能跑起来,小问题 baidu 一下还是能解决的,个人认为应该 Ruby2.3+ 版本应该对 Windows 不是哪么排斥了吧,毕竟看到说建议不要在 Windows 上玩 Ruby on Rails 的贴子也是几年前了吧。

2、也关注了 Ruby 的另一个轻量框架 Sinatra,在 Windows 上玩了一把,好像也没什么大问题,简单的连接 MySql,启动服务器,都是可以的,就是有一点迷惑,启动服务器要在 Windows 的 cmd 下启动,cmd 窗口不能关闭,看着哪个 cmd 窗口,总有一种不踏实的感觉,觉得服务器进程会不会宕掉啊😂 。关于 Ruby 的服务器,例如:puma,thin 一类的服务器,如果只单开一个进程,到底能支持多大的并发数呢?还有这些服务器都说最好用 Nginx 反向代理,不用代理可不可以跑啊?这些个迷惑一直在找答案,听说 Ruby China 社区很多热心的前辈,希望有经验的前辈能给个明示,先谢谢!

3、进入主题,Ruby 的 gui 界面开发,没看到说 Ruby 在 Windows 上难整,高兴一个😁,一直关注 Ruby China 社区,但在社区没有发现有使用 Ruby 进行界面开发的好例子,因为工作一直在 Windows 上,就试着在 Windows 上玩。也装过双系统 Windows7+Ubuntu Gnome16.04 玩过,不适应啊😥,还有一个最重要的问题就是这个坦克大战在 ubuntu 上跑居然会卡死,一直没想通,在 Windows 上啥事没有😇 和儿子玩得挺欢,这是什么情况啊?.

游戏是参照马士兵的 Java 坦克大战的思想,图片来自韩顺平的坦克大战。Ruby 的实现,废话不多说,以下目录结构及图片代码。

先上小游戏的目录结构:

  • tank(主目录)
    • img(图片目录)
    • tank_game.rb(游戏入口,主类)
    • tank.rb(坦克类)
    • missile.rb(子弹类)
    • wall.rb(墙类)
    • bomb.rb(爆炸类)
    • source.rb(图片资源类)
    • rectangle.rb(检测碰撞类)
    • menu.rb(菜单类)
    • menu.xml(菜单的名称及响应的方法名都记录在此文件中)
    • map.xml(地图)
  • img(图片目录下的目录结构)
    • 0P(敌人坦克目录,目录里有图片:Up.png ,Down.png ,Left.png ,Right.png )
    • 1P(1P 坦克目录,目录里有图片:Up.png ,Down.png ,Left.png ,Right.png )
    • 2P(2P 坦克目录,目录里有图片:Up.png ,Down.png ,Left.png ,Right.png )
    • bomb_0.gif(爆炸图片)
    • bomb_1.gif(爆炸图片)
    • bomb_2.gif(爆炸图片)
    • wall_0.png(土墙图片)
    • wall_1.png(草墙图片)
    • wall_2.png(水墙图片)
    • wall_3.png(钢墙图片)
    • wall_4.png(总部图片)

以下是代码

1、tank_game.rb(游戏入口,主类)

# -*- coding: UTF-8 -*-

require 'gtk3'
require 'thread'
require 'rexml/document'
Dir["./*.rb"].each{|f| require f if f!="./tank_game.rb"}

module TankGame

  # 坦克大战主类
  class TankGame < Gtk::Window

    # 常量
    WIDTH, HEIGHT = 800, 600
    MENU_HEIGHT = 25 
    TANK_TYPE = {enemy: 0, my1p: 1, my2p: 2}

    # setter and getter
    attr_reader :missiles, :tanks, :bombs, :walls, :tank_image, :bomb_image, :wall_image
    attr_accessor :run_flag

    # 构造器方法
    def initialize
      super      
      source      = Source.new
      @tank_image = source.tank_image # 获取坦克图片资源
      @bomb_image = source.bomb_image # 获取爆炸图片资源
      @wall_image = source.wall_image # 获取墙类图片资源
      @run_flag   = true              # 线程启动标记
      # xml解析地图
      @maps = REXML::Document.new(File.new "map.xml").root
      init        # 初始化变量
      init_ui     # def初始化ui界面
      listeners   # def事件监听    
      show_all    # 显示所有部件
      run         # def启动线程
    end
    # 初始化变量
    def init
      @my1p_num = @my2p_num = 3 # 我方坦克数量 
      @tanks    = Array.new     # 初始化敌人坦克数组
      @missiles = Array.new     # 初始化子弹数组
      @bombs    = Array.new     # 初始化爆炸数组
      @walls    = Array.new     # 初始化墙类数组
      @tank_1p = Tank.new self, 325, 570, TANK_TYPE[:my1p], true, Tank::DIR[:stop]  # 生成1P坦克
      @tank_2p = Tank.new self, 445, 570, TANK_TYPE[:my2p], true, Tank::DIR[:stop]  # 生成2P坦克
      init_tanks    # def初始化创建坦克
      init_walls 1  # def初始化创建墙,并指定关数
    end
    # 重置1P坦克
    def reset_my1p_tank
      @my1p_num -= 1
      if @my1p_num > 0 && @tank_1p != nil
        @tank_1p.x, @tank_1p.y = 325, 570
        @tank_1p.dir, @tank_1p.ptdir = Tank::DIR[:stop], Tank::DIR[:up]        
      else
        @tank_1p = nil
      end
    end
    # 重置2P坦克
    def reset_my2p_tank
      @my2p_num -= 1
      if @my2p_num > 0 && @tank_2p != nil
        @tank_2p.x, @tank_2p.y = 445, 570
        @tank_2p.dir, @tank_2p.ptdir = Tank::DIR[:stop], Tank::DIR[:up]        
      else
        @tank_2p = nil
      end
    end
    # 初始化ui界面
    def init_ui
      set_title "坦克世界"                                                          # 设置窗口标题
      set_resizable false                                                           # 禁止改变窗口大小,ubuntu下要注释掉,不明原因        
      set_icon @tank_image[TANK_TYPE[:my2p]][Tank::DIR[:up]]                        # 设置任务栏窗口图标,使用2p的坦克作为图标
      # set_icon_from_file "./img/1P/Up.png"                                        # 设置任务栏窗口图标
      override_background_color :normal, Gdk::RGBA::new(0.6, 0.6, 0.6, 1)           # 设置窗口背景色
      set_default_size WIDTH, HEIGHT + MENU_HEIGHT                                  # 设置窗口宽高
      set_window_position :center                                                   # 设置窗口位置                         

      @darea = Gtk::DrawingArea.new                                                 # 创建画布
      @box = Gtk::Box.new :vertical, 0                                              # 创建盒子 :vertical垂直, :horizontal水平

      @menu = Menu.new(self)
      @menubar = @menu.menubar
      @box.pack_start @menubar, :expand => false, :fill => false, :padding => 0     # 添加菜单到box盒子
      @box.pack_start @darea, :expand => true, :fill => true, :padding => 0         # 添加画布到box盒子

      # widget:@darea对象, cr: 是@darea.window.create_cairo_context创建的对象
      @darea.signal_connect "draw" do |widget, cr|                                  # 监听draw动作
        draw cr
      end
      add @box                                                                      # 画面box盒子到窗口
      # add @darea                                                                  # 画面添加到窗口
    end

    # 事件监听
    def listeners
      # 窗口销毁事件
      signal_connect "destroy" do                          
        Gtk.main_quit 
      end
      # 键盘按下事件
      signal_connect "key_press_event" do |widget, event|      
        @tank_1p.key_press event unless @tank_1p == nil
        @tank_2p.key_press event unless @tank_2p == nil
      end
      # 键盘松开事件
      signal_connect "key_release_event" do |widget, event|
        @tank_1p.key_release event if @tank_1p != nil
        @tank_2p.key_release event if @tank_2p != nil
      end
    end

    # 画出所有东西
    def draw cr
      # 写字
      cr.save do # 等同于cr.save和cr.restore同时使用
        cr.set_source_rgb 1, 1, 1           
        cr.select_font_face "宋体", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL
        cr.set_font_size 13      
        cr.move_to 20, 15
        cr.show_text "子弹数量: %d" % @missiles.size
        cr.move_to 20, 30
        cr.show_text "坦克数量: %d" % @tanks.size
        cr.move_to 120, 15
        cr.show_text "炸弹数量: %d" % @bombs.size
        cr.move_to 120, 30
        cr.show_text "墙的数量: %d" % @walls.size
        cr.move_to 220, 15
        cr.show_text "1P的数量: %d" % @my1p_num
        cr.move_to 220, 30
        cr.show_text "2P的数量: %d" % @my2p_num
        cr.stroke # 写字也要结尾
      end
      # 画草墙以外所有墙
      @walls.each {|w| w.draw cr unless w.type == 1}
      # 画所有子弹
      @missiles.each do |m|
        m.hit_tanks @tanks
        m.hit_walls @walls
        m.hit_tank @tank_1p if @tank_1p != nil
        m.hit_tank @tank_2p if @tank_2p != nil
        m.draw cr
      end
      # 画我方坦克
      if @tank_1p != nil
        @tank_1p.collide_walls @walls
        @tank_1p.collide_tank @tank_2p if @tank_2p != nil
        @tank_1p.collide_tanks @tanks   
        @tank_1p.draw cr
      end
      if @tank_2p != nil
        @tank_2p.collide_walls @walls
        @tank_2p.collide_tank @tank_1p if @tank_1p != nil
        @tank_2p.collide_tanks @tanks   
        @tank_2p.draw cr
      end
      # 画敌人坦克
      @tanks.each do |t|
        t.collide_tank @tank_1p if @tank_1p != nil
        t.collide_tank @tank_2p if @tank_2p != nil
        t.collide_tanks @tanks        
        t.collide_walls @walls
        t.draw cr   
      end
      # 画所有草墙
      @walls.each {|w| w.draw cr if w.type == 1}
      # 画所有爆炸
      @bombs.each {|b| b.draw cr}
      # 我方坦克数量为0,调用菜单的重新游戏
      if @my1p_num + @my2p_num == 0
        @menu.again
      end
    end 
    # 初始化坦克
    def init_tanks
      3.times do |i|        
        @tanks << Tank.new(self, i*385, 0, TANK_TYPE[:enemy], false, Tank::DIR[:down]) # 默认间隔385
      end
    end

    # 初始化墙
    def init_walls num
      map = @maps.elements[num].text.split ";"
      map.each do |m|
        n = m.split(",").map{|i| i.to_i}     
        @walls << Wall.new(self, n[0]*20, n[1]*20, n[2])
      end
      @walls << Wall.new(self, 380, 560, 4)  # 总部
    end

    # 重画函数
    def redraw
      @darea.queue_draw #部件重画函数,会自动调用draw事件
    end

    # 开启线程自动重画
    def run
      t = Thread.new {         # 线程创建
        while true             # 无限循环
          sleep(0.05)          # 线程阻塞
          redraw if @run_flag  # 调用重画        
        end
      }    
    end
  end
end

tank = TankGame::TankGame.new # 实例化窗口对象
Gtk.main                      # 启动窗口程序

2、tank.rb(坦克类)

# -*- coding: UTF-8 -*-

module TankGame

  # 坦克类
  class Tank
    # 常量
    SPEED = 3
    WIDTH = HEIGHT = 30
    DIR = {up: 0, down: 1, left: 2, right: 3, stop: 4}

    # setter and getter
    attr_reader :good, :type
    attr_accessor :x, :y, :dir, :ptdir
    # 构造器方法
    def initialize tg, x, y, type, good, dir
      @tg, @x, @y, @type, @good, @dir = tg, x, y, type, good, dir
      @image = @tg.tank_image
      @bU = @bD = @bL = @bR = false
      @ptdir = DIR[:up]
      @step = rand(4..15)
      @old_x, @old_y = @x, @y
      @missile_max_number = 5
      @interval = 0
    end

    # 坦克画自己
    def draw cr      
      cr.save do
        img = @image[@type][@ptdir].scale Tank::WIDTH, Tank::HEIGHT  # 获取图片并设置宽高
        # img = img.rotate 90                                        # 图片旋转
        cr.set_source_pixbuf(img, @x, @y).paint                      # 设置来源并画出图片
      end
      move  # 移动坦克
    end 

    # 生成碰撞检测类
    def get_rect
      Rectangle.new @x, @y, WIDTH, HEIGHT
    end

    # 坦克撞墙
    def collide_wall wall
      if wall.type != 1 && get_rect.intersects?(wall.get_rect)  
        stay #unless wall.type == 1
        return true 
      end
      false
    end

    # 坦克撞墙集合
    def collide_walls walls
      walls.each {|w| return true if collide_wall w}      
      false
    end

    # 坦克撞坦克
    def collide_tank tank      
      if get_rect.intersects?(tank.get_rect) && tank != self        
        stay
        return true 
      end
      false
    end

    # 坦克撞坦克集合
    def collide_tanks tanks
      tanks.each {|t| return true if collide_tank t}      
      false
    end

    # 键盘按下事件
    def key_press event
      key_event event, true
    end

    # 键盘松开事件
    def key_release event
      key_event event, false
    end

    # 以下是私有方法
    private
    # 键盘事件
    def key_event event, b
      key = event.keyval      
      if @type == 1
        case key
        when Gdk::Keyval::KEY_KP_4  then fire if !b
        when Gdk::Keyval::KEY_Up    then @bU = b
        when Gdk::Keyval::KEY_Down  then @bD = b
        when Gdk::Keyval::KEY_Left  then @bL = b
        when Gdk::Keyval::KEY_Right then @bR = b
        end
      else
        case key
        when Gdk::Keyval::KEY_j then fire if !b
        when Gdk::Keyval::KEY_w then @bU = b
        when Gdk::Keyval::KEY_s then @bD = b
        when Gdk::Keyval::KEY_a then @bL = b
        when Gdk::Keyval::KEY_d then @bR = b
        end
      end
      locate_direction 
    end

    # 根据键盘事件改变坦克方向
    def locate_direction
      @dir = DIR[:up]    if  @bU && !@bD && !@bL && !@bR
      @dir = DIR[:down]  if !@bU &&  @bD && !@bL && !@bR
      @dir = DIR[:left]  if !@bU && !@bD &&  @bL && !@bR
      @dir = DIR[:right] if !@bU && !@bD && !@bL &&  @bR
      @dir = DIR[:stop]  if !@bU && !@bD && !@bL && !@bR
    end

    # 我方坦克自动挂载子弹
    def load_missile
      @interval > 20 ? @interval = 0 : @interval += 1
      @missile_max_number += 1 if @missile_max_number < 5 && @interval == 20
    end

    # 坦克移动
    def move
      @old_x, @old_y = @x, @y
      case @dir
      when DIR[:up]    then @y -= SPEED        
      when DIR[:down]  then @y += SPEED
      when DIR[:left]  then @x -= SPEED
      when DIR[:right] then @x += SPEED
      when DIR[:stop]
      end
      @ptdir = @dir unless @dir == DIR[:stop] # 根据坦克方向改变炮桶方向,运用Ruby的unless判断
      out
      turn_dir
      load_missile
    end

    # 敌人坦克自动改变方向,行走,发炮弹
    def turn_dir
      if !@good
        if @step == 0
          @step = rand(4..15)
          @dir = rand(DIR.size)
        end
        @step -= 1
        fire if rand(40) > 38
      end
    end

    # 判断坦克出界
    def out
      if @x > TankGame::WIDTH - WIDTH
        @x = TankGame::WIDTH - WIDTH
      elsif @x < 0
        @x = 0
      end
      if @y > TankGame::HEIGHT - HEIGHT
        @y = TankGame::HEIGHT - HEIGHT
      elsif @y < 0
        @y = 0
      end      
    end

    # 发射子弹
    def fire
      x = @x + WIDTH/2 - Missile::WIDTH/2
      y = @y + HEIGHT/2 - Missile::HEIGHT/2   
      if @missile_max_number > 0
        m = Missile.new @tg, x, y, @type, @good, @ptdir
        @tg.missiles << m
        @missile_max_number -= 1
      end
    end

    # 返回上一步
    def stay
      @x, @y = @old_x, @old_y
    end
  end
end

3、missile.rb(子弹类)

# -*- coding: UTF-8 -*-

module TankGame

  # 子弹类
  class Missile

    # 常量
    SPEED = 6           # 子弹速度
    WIDTH = HEIGHT = 3  # 子弹宽高

    # 构造器方法
    # @tg: TankGame主类实例
    # @x, @y: 子弹坐标
    # @type: 子弹类型,区分敌,我[1P,2P]
    # @good: 区分子弹好坏
    # @dir, @color: 子弹方向,颜色
    def initialize tg, x, y, type, good, dir
      @tg, @x, @y, @type, @good, @dir = tg, x, y, type, good, dir      
      @color = [[0,1,1],[1,1,0],[0,0.5,0]]
    end

    # 画出子弹
    def draw cr
      cr.save do
        cr.set_source_rgb @color[@type]
        cr.rectangle @x, @y, WIDTH, HEIGHT  # 设置坐标,宽高
        cr.fill      
      end
      move
    end

    # 生成碰撞检测类
    def get_rect
      Rectangle.new @x, @y, WIDTH, HEIGHT
    end

    # 子弹打中墙
    def hit_wall wall
      if get_rect.intersects?(wall.get_rect)
        @tg.missiles.delete self unless wall.type == 1 || wall.type == 2
        @tg.walls.delete wall if wall.type == 0 || wall.type == 4
        return true
      end
      false
    end

    # 子弹打中墙集合
    def hit_walls walls
      walls.each {|w| return true if hit_wall w}
      false
    end

    # 子弹打中坦克
    def hit_tank tank
      if get_rect.intersects?(tank.get_rect) && @good != tank.good
        @tg.bombs << Bomb.new(@tg, tank.x, tank.y)
        @tg.tanks.delete tank
        @tg.reset_my1p_tank if tank.type == TankGame::TANK_TYPE[:my1p]
        @tg.reset_my2p_tank if tank.type == TankGame::TANK_TYPE[:my2p]
        @tg.missiles.delete self
        return true
      end
      false
    end

    # 子弹打中全部坦克
    def hit_tanks tanks
      tanks.each {|t| return true if hit_tank t}      
      false
    end

    # private以下都属于私有方法
    private

    # 子弹移动
    def move
      case @dir
      when Tank::DIR[:up]    then @y -= SPEED                
      when Tank::DIR[:down]  then @y += SPEED        
      when Tank::DIR[:left]  then @x -= SPEED        
      when Tank::DIR[:right] then @x += SPEED              
      end
      if @y > TankGame::HEIGHT || @y < 0 || @x > TankGame::WIDTH || @x < 0
        @tg.missiles.delete self  # 子弹出界移除
      end
    end
  end
end

4、wall.rb(墙类)

# -*- coding: UTF-8 -*-

module TankGame

  # 墙类,包括总部
  class Wall

    # setter and getter
    attr_reader :type

    # 构造器方法    
    def initialize tg, x, y, type
      @tg, @x, @y, @type = tg, x, y, type
      @img = @tg.wall_image[@type]
      @w, @h = @img.width, @img.height 
    end

    # 画墙
    def draw cr      
      cr.set_source_pixbuf(@img, @x, @y).paint  # 设置来源,并画出图片      
    end

    # 生成碰撞检测类
    def get_rect
      Rectangle.new @x, @y, @w, @h
    end    
  end
end

5、bomb.rb(爆炸类)

# -*- coding: UTF-8 -*-

module TankGame

  # 爆炸类
  class Bomb

    # 构造器方法
    def initialize tg, x, y
      @tg, @x, @y = tg, x, y
      @step = 0      
    end

    # 画爆炸
    def draw cr
      img = @tg.bomb_image[@step/4].scale Tank::WIDTH, Tank::HEIGHT  # 设置图片宽高   
      cr.set_source_pixbuf(img, @x, @y).paint                        # 设置来源,并画出图片
      @step == 11 ? @tg.bombs.delete(self) : @step += 1              # 以动画形式切换图片
    end
  end  
end

6、source.rb(图片资源类)

# -*- coding: UTF-8 -*-

module TankGame

  # 资源类
  class Source

    # getter and setter
    attr_reader :tank_image, :bomb_image, :wall_image

    # 构造器方法
    def initialize
      load_img  # def引入图片
    end

    # 私有方法,load_img引入图片资源
    private
    # 引入图片
    def load_img    
      @tank_image = Array.new 3 do |i|
        ["Up", "Down", "Left", "Right"].map! do |n|
          # Cairo::ImageSurface.from_png "./img/%dp/%s.png" % [i, n]  # 引入图片资源
          GdkPixbuf::Pixbuf.new :file => "./img/%dp/%s.png" % [i, n]  # 引入图片资源
        end
      end
      @bomb_image = Array.new 3 do |i|
        GdkPixbuf::Pixbuf.new :file => "./img/bomb_%d.gif" % i        
      end
      @wall_image = Array.new 5 do |i|
        GdkPixbuf::Pixbuf.new :file => "./img/wall_%d.png" % i
      end     
    end    
  end
end

7、rectangle.rb(检测碰撞类)

# -*- coding: UTF-8 -*-

module TankGame
  # 碰撞检测类
  class Rectangle  
    # getter and setter
    attr_reader :x, :y, :w, :h

    # 构造器方法
    def initialize x, y, w, h
      @x, @y, @w, @h = x, y, w, h
    end

    # 矩形碰撞检测方法
    def intersects? other_rectangle
      r = other_rectangle
      # 根据两个矩形左上角x,y计算出中心点x,y
      x, y, x1, y1 = @x + @w/2, @y + @h/2, r.x + r.w/2, r.y + r.h/2
      # 当两个矩形横向中心点的距离小于两个矩形宽的和的一半
      # 且两个矩形纵向中心点的距离小于两个矩形高的和的一半,即碰上
      (x - x1).abs < (@w + r.w)/2 && (y - y1).abs < (@h + r.h)/2
    end
  end
end

8、menu.rb(菜单类)

# -*- coding: UTF-8 -*-

module TankGame

  # 菜单类
  class Menu

    # setter and getter
    attr_reader :menubar

    # 构造器方法    
    def initialize tg
      @tg = tg
      @menubar = Gtk::MenuBar.new                              # 创建根菜单      
      init_menu                                                # 初始化菜单
    end

    # 菜单初始化
    def init_menu
      menu = REXML::Document.new(File.new "menu.xml").root     # 解析菜单menu.xml文件
      menu.elements.each "Menu" do |m|                         # 遍历xml文件根目录下Menu一级目录
        menu_name = m.elements["Name"].text                    # 获取一级目录名称
        menu_obj = Gtk::Menu.new                               # 创建一级菜单容器
        menu_item_obj = Gtk::MenuItem.new :label => menu_name  # 创建一级菜单
        menu_item_obj.set_submenu menu_obj                     # 一级菜单绑定一级菜单容器
        @menubar.append menu_item_obj                          # 一级菜单添加到根菜单@menubar
        m.elements.each "MenuItem" do |mi|                     # 遍历一级目录下的MenuItem二级目录
          item_name = mi.elements["Name"].text                 # 获取二级目录的名称
          item_acti = mi.elements["Activate"].text             # 获取二级目录的事件绑定方法名称
          item_obj = Gtk::MenuItem.new :label => item_name     # 创建二级子菜单
          menu_obj.append item_obj                             # 二级子菜单添加到一级菜单容器
          item_obj.signal_connect "activate" do                # 创建二级子菜单激活事件
            send item_acti                                     # 利用方法名称绑定子菜单事件到指定方法
          end          
        end        
      end 
    end

    # 以下为菜单事件绑定方法,方法名与nemu.xml文件中定义一样
    def start
      p @tg.tanks
    end
    def again
      @tg.run_flag = false # 线程暂停
      md = Gtk::MessageDialog.new :parent => @tg, :flags => :destroy_with_parent, :type => :info, 
        :buttons_type => :ok, :message => "游戏结束!点击确定重新开始游戏"
      md.override_font Pango::FontDescription.new "DFKai-SB 12"  # 设置消息框字体
      md_msg = md.run
      md.destroy
      @tg.init if md_msg.name == "GTK_RESPONSE_OK" # 点击确定重新开始游戏,初始化所有变量
      @tg.run_flag = true  # 线程启动
    end
    def pause
      @tg.run_flag = false # 线程暂停
      md = Gtk::MessageDialog.new :parent => @tg, :flags => :destroy_with_parent, :type => :info, 
        :buttons_type => :ok, :message => "游戏已暂停!点击确定继续游戏"
      md.override_font Pango::FontDescription.new "DFKai-SB 12"  # 设置消息框字体
      md.run
      md.destroy
      @tg.run_flag = true  # 线程启动
    end
    def read
      p "read"
    end
    def save
      p "save"
    end
    def exit
      Gtk.main_quit
    end
    def empty
      p "empty"
    end
  end
end

9、menu.xml(菜单的名称及响应的方法名都记录在此文件中)

<?xml version="1.0" encoding="UTF-8"?> 
<MenuBar>
  <Menu>
    <Name>游戏</Name>
    <MenuItem><Name>开始游戏</Name><Activate>start</Activate></MenuItem>
    <MenuItem><Name>重新游戏</Name><Activate>again</Activate></MenuItem>
    <MenuItem><Name>暂停游戏</Name><Activate>pause</Activate></MenuItem>
    <MenuItem><Name>读取游戏</Name><Activate>read</Activate></MenuItem>
    <MenuItem><Name>保存游戏</Name><Activate>save</Activate></MenuItem>
    <MenuItem><Name>退出游戏</Name><Activate>exit</Activate></MenuItem>
  </Menu>
  <Menu>
    <Name>设置</Name>
    <MenuItem><Name>游戏人数</Name><Activate>empty</Activate></MenuItem>
    <MenuItem><Name>游戏难度</Name><Activate>empty</Activate></MenuItem>
    <MenuItem><Name>游戏设置</Name><Activate>empty</Activate></MenuItem>    
  </Menu>
  <Menu>
    <Name>地图</Name>
    <MenuItem><Name>创建地图</Name><Activate>empty</Activate></MenuItem>
    <MenuItem><Name>修改地图</Name><Activate>empty</Activate></MenuItem>
    <MenuItem><Name>删除地图</Name><Activate>empty</Activate></MenuItem>    
  </Menu>
  <Menu>
    <Name>关于</Name>
    <MenuItem><Name>游戏帮助</Name><Activate>empty</Activate></MenuItem>
    <MenuItem><Name>关于游戏</Name><Activate>empty</Activate></MenuItem>      
  </Menu>
</MenuBar>

10、map.xml(地图)

<?xml version="1.0" encoding="UTF-8"?>  
<Maps>
<Map id='1'>2,2,0;3,2,0;6,2,0;7,2,0;10,2,0;11,2,0;12,2,0;15,2,0;16,2,0;19,2,0;20,2,0;23,2,0;24,2,0;27,2,0;28,2,0;29,2,0;32,2,0;33,2,0;36,2,0;37,2,0;2,3,0;3,3,0;6,3,0;7,3,0;10,3,0;11,3,0;12,3,0;15,3,0;16,3,0;19,3,0;20,3,0;23,3,0;24,3,0;27,3,0;28,3,0;29,3,0;32,3,0;33,3,0;36,3,0;37,3,0;2,4,0;3,4,0;6,4,0;7,4,0;10,4,0;11,4,0;12,4,0;15,4,0;16,4,0;19,4,0;20,4,0;23,4,0;24,4,0;27,4,0;28,4,0;29,4,0;32,4,0;33,4,0;36,4,0;37,4,0;2,5,0;3,5,0;6,5,0;7,5,0;10,5,0;11,5,0;12,5,0;15,5,0;16,5,0;19,5,0;20,5,0;23,5,0;24,5,0;27,5,0;28,5,0;29,5,0;32,5,0;33,5,0;36,5,0;37,5,0;2,6,0;3,6,0;6,6,0;7,6,0;8,6,3;9,6,3;10,6,0;11,6,0;12,6,0;15,6,0;16,6,0;17,6,3;18,6,3;19,6,0;20,6,0;21,6,3;22,6,3;23,6,0;24,6,0;27,6,0;28,6,0;29,6,0;30,6,3;31,6,3;32,6,0;33,6,0;36,6,0;37,6,0;2,7,0;3,7,0;6,7,0;7,7,0;8,7,3;9,7,3;10,7,0;11,7,0;12,7,0;15,7,0;16,7,0;17,7,1;18,7,1;19,7,0;20,7,0;21,7,1;22,7,1;23,7,0;24,7,0;27,7,0;28,7,0;29,7,0;30,7,3;31,7,3;32,7,0;33,7,0;36,7,0;37,7,0;2,8,0;3,8,0;6,8,0;7,8,0;10,8,0;11,8,0;12,8,0;15,8,0;16,8,0;17,8,1;18,8,1;19,8,0;20,8,0;21,8,1;22,8,1;23,8,0;24,8,0;27,8,0;28,8,0;29,8,0;32,8,0;33,8,0;36,8,0;37,8,0;2,9,0;3,9,0;6,9,0;7,9,0;10,9,0;11,9,0;12,9,0;15,9,0;16,9,0;19,9,0;20,9,0;23,9,0;24,9,0;27,9,0;28,9,0;29,9,0;32,9,0;33,9,0;36,9,0;37,9,0;2,10,0;3,10,0;6,10,0;7,10,0;10,10,0;11,10,0;12,10,0;15,10,0;16,10,0;19,10,0;20,10,0;23,10,0;24,10,0;27,10,0;28,10,0;29,10,0;32,10,0;33,10,0;36,10,0;37,10,0;2,11,0;3,11,0;6,11,0;7,11,0;10,11,0;11,11,0;12,11,0;27,11,0;28,11,0;29,11,0;32,11,0;33,11,0;36,11,0;37,11,0;14,12,0;15,12,0;24,12,0;25,12,0;0,13,3;1,13,3;4,13,0;5,13,0;6,13,0;7,13,0;8,13,0;14,13,0;15,13,0;24,13,0;25,13,0;31,13,0;32,13,0;33,13,0;34,13,0;35,13,0;38,13,3;39,13,3;0,14,3;1,14,3;4,14,0;5,14,0;6,14,0;7,14,0;8,14,0;10,14,1;11,14,1;12,14,1;13,14,1;16,14,2;17,14,2;18,14,2;19,14,2;20,14,2;21,14,2;22,14,2;23,14,2;26,14,1;27,14,1;28,14,1;29,14,1;31,14,0;32,14,0;33,14,0;34,14,0;35,14,0;38,14,3;39,14,3;10,15,1;11,15,1;12,15,1;13,15,1;16,15,2;17,15,2;18,15,2;19,15,2;20,15,2;21,15,2;22,15,2;23,15,2;26,15,1;27,15,1;28,15,1;29,15,1;2,16,0;3,16,0;6,16,0;7,16,0;10,16,0;11,16,0;28,16,0;29,16,0;32,16,0;33,16,0;36,16,0;37,16,0;2,17,0;3,17,0;6,17,0;7,17,0;10,17,0;11,17,0;14,17,0;15,17,0;18,17,0;21,17,0;24,17,0;25,17,0;28,17,0;29,17,0;32,17,0;33,17,0;36,17,0;37,17,0;0,18,1;1,18,1;2,18,0;3,18,0;6,18,0;7,18,0;10,18,0;11,18,0;14,18,0;15,18,0;18,18,0;21,18,0;24,18,0;25,18,0;28,18,0;29,18,0;32,18,0;33,18,0;36,18,0;37,18,0;0,19,1;1,19,1;2,19,0;3,19,0;6,19,0;7,19,0;8,19,3;9,19,3;10,19,0;11,19,0;14,19,0;15,19,0;18,19,0;21,19,0;24,19,0;25,19,0;28,19,0;29,19,0;30,19,3;31,19,3;32,19,0;33,19,0;36,19,0;37,19,0;38,19,1;39,19,1;0,20,1;1,20,1;2,20,0;3,20,0;6,20,0;7,20,0;8,20,3;9,20,3;10,20,0;11,20,0;14,20,0;15,20,0;18,20,0;19,20,3;20,20,3;21,20,0;24,20,0;25,20,0;28,20,0;29,20,0;30,20,3;31,20,3;32,20,0;33,20,0;36,20,0;37,20,0;38,20,1;39,20,1;0,21,1;1,21,1;2,21,0;3,21,0;6,21,0;7,21,0;10,21,0;11,21,0;14,21,0;15,21,0;18,21,0;19,21,3;20,21,3;21,21,0;24,21,0;25,21,0;28,21,0;29,21,0;32,21,0;33,21,0;36,21,0;37,21,0;38,21,1;39,21,1;2,22,0;3,22,0;6,22,0;7,22,0;10,22,0;11,22,0;14,22,0;15,22,0;18,22,0;21,22,0;24,22,0;25,22,0;28,22,0;29,22,0;32,22,0;33,22,0;36,22,0;37,22,0;38,22,1;39,22,1;2,23,0;3,23,0;6,23,0;7,23,0;10,23,0;11,23,0;14,23,0;15,23,0;18,23,0;21,23,0;24,23,0;25,23,0;28,23,0;29,23,0;32,23,0;33,23,0;36,23,0;37,23,0;2,24,0;3,24,0;12,24,1;13,24,1;18,24,0;21,24,0;26,24,1;27,24,1;36,24,0;37,24,0;12,25,1;13,25,1;26,25,1;27,25,1;3,26,0;4,26,0;5,26,0;6,26,0;7,26,0;8,26,0;9,26,0;10,26,0;11,26,0;12,26,0;13,26,0;14,26,0;15,26,0;24,26,0;25,26,0;26,26,0;27,26,0;28,26,0;29,26,0;30,26,0;31,26,0;32,26,0;33,26,0;34,26,0;35,26,0;36,26,0;3,27,0;4,27,0;5,27,0;6,27,0;7,27,0;8,27,0;9,27,0;10,27,0;11,27,0;12,27,0;13,27,0;14,27,0;15,27,0;18,27,0;19,27,0;20,27,0;21,27,0;24,27,0;25,27,0;26,27,0;27,27,0;28,27,0;29,27,0;30,27,0;31,27,0;32,27,0;33,27,0;34,27,0;35,27,0;36,27,0;18,28,0;21,28,0;18,29,0;21,29,0;</Map>
</Maps>

游戏说明:1P 控制键:方向键 + 小键盘 4 发弹,2P 控制键:ASDW+J 键发弹,

游戏还有很多地方未完善,分享出来主要是互相学习,看看有没有高手实现网络对战版😍 ,并解决 ubuntu 下为什么卡死

补充一下:Ruby 版本 2.3.3,gem 版本 2.5.2,gtk3 版本 3.1.6

第一次发贴,不会排版,也不会直接上传文件,只好这样了。多多包含,谢谢!

最后,上一张游戏画面

👍 放到 Github 上更佳啊

huacnlee 将本帖设为了精华贴。 06月14日 07:21

👍👍👍

厉害啊,求 github

一脸的羡慕,我什么时候能有这样的能力

依稀记得大二的时候看了马士兵的俄罗斯方块,然后写了一个 flappy bird

超赞,钩起无数回忆啊~

赞👍,楼主头像挺好看的,Sapphire

谢谢大家的赞赏,没玩过 Github,就这样将就的放在这里吧。不好意思哈。😄

我前面提到的小问题,没有前辈可以指点一下吗?😅

希望通过这个小游戏,能激起新手对 Ruby 的兴趣,壮大 Ruby 的队伍💪

Github 不会整,刚刚在码云上乱整了一通,代码都上传到码云上了,也不知道可不可行。

链接:https://git.oschina.net/ysjrdfaps/tankgame.git

大家看看能不能点过去。如果不行我也没办法了😇 图片打包放在项目的附件中。

很不错啊

ysjrdfaps 回复

不错不错,赞一个!

另外可以补充下码云上的 Readme,我可以给你推荐哦。

赞赞赞。👍 强烈 github 走一走啊

Zoker 回复

补充下码云上的 Readme,是什么的意思,不懂啊😁 应大家的要求才乱整了一下码云,不知道你说的推荐是什么意思。

厉害,有种高手在民间的感觉

Zoker 回复

参考了其它的项目,补充了 Rendme,也不知道是不是这样,正好学习了。还没搞清楚你所说的推荐是什么😂

看到这个帖子不禁让我联想到

这是一个与时代脱节的手艺人 (不会使用 git) 纯靠着自己的手艺 (ruby) 雕刻出一件精美的艺术品 (坦克大战)

这才是程序员应该保有的匠心精神啊!!👍

Gemfile ?

厉害厉害!

厉害了,一向都是看到 web 相关的,第一次看到 gtk 的分享。

勉强用手机回答下问题,如果有错误请下面的朋友更正

1,因为大部分的的 Ruby 开发者(我也没用)都很少使用 windows,所以很多第三方的 gem 并没有考虑到兼容 windows

2, 不会当掉的。一般部署在服务器的时候 ruby 进程都是以 deamon 的形式运行的,关了窗口也没事。比如 thin -d, puma -d。

可以不用 nginx 反代,但是 nginx 可以在缓存,处静态资源,负载均衡等等上提供更好的性能

并发要看情况,如果程序逻辑复杂,还有锁什么的,就会很慢了。简单的输出 hello world 的话,单核 rails 应该能够 1000req 每秒,sinatra 会更高。

tangmonk 回复

非常感谢你的回复,解开了我的一些迷惑。也不再担心服务器的问题了,您说的 deamon 是 Linux 下的守护进程吗?windows 好像木有这个东西

deamon 就是后台进程的意思

windows 也有 deamon,其实就是 service(服务),可以注册某个 exe 为服务,然后就成为 deamon 了

你看维基百科的解释哈: https://en.wikipedia.org/wiki/Daemon_computing)(

你说的守护进程是另一种确保后台进程不会出现意外事故挂掉的程序

ruby tank_game.rb 
/home/pynix/.gem/ruby/2.4.0/gems/gobject-introspection-3.1.6/lib/gobject-introspection/loader.rb:317:in `invoke': Failed to open file './img/0p/Up.png': No such file or directory (GLib::FileError)

arch 上玩,出错,看错误提示大概是因为文件系统大小写敏感,把图片路径修改了下就跑起来了,文字还是方格。

pynix 回复

因为我是在 Windows 上跑的,路径的问题我之前在 Ubuntu 上遇到过,忘记改了,文字是方格是因为系统文字的原因,修改 tank_game.rb 文件的 121 行的字体就可以了。修改成你系统中有的字体或注释掉好像也可以的。

换了个中文字体就好了。。。

pynix 回复

还行吧?😄 虽然游戏可玩性不算强,毕竟 Ruby 实现,还是挺喜欢的,要是能实现网络版就更爽了。

ysjrdfaps 回复

发现一个 bug,我不小心把老鹰干掉了。。。

pynix 回复

我记得小时候就可以直接把老鹰干掉的,这是一个 feature 😄

kgen 回复

关键是干掉老鹰之后游戏还能继续玩。。

pynix 回复

哈哈,这个的确 bug 了

kgen 回复

说实在,这个不是 bug,是因为项目没完成,这个小案例不是为了玩游戏而开发的,是为了演示 Rbuy 也可以很好的在 Windows 下开发桌面小程序,如果有兴趣改进这个小游戏,其实就是在子弹类中的 hit_wall 方法中判断一下子弹打中墙,因为总部也是墙,打中就 Game over 就可以了,还可以判断自己的坦克打中总部子弹消失而总部不消亡或穿过总部之类的,大家有时间可以自己考虑完善一下。😄 ……我现在在研究 Sinatra 这个 DSL……

ysjrdfaps 回复

就是把你的项目推到码云首页,让大家都能看到,我给你提了个 PR,补充了下截图,你看看。

Zoker 回复

谢谢你哈,虽然我还是没搞懂,不断学习中……

童年的回忆啊,我读书时候也自己写过一个坦克大战

大牛啊,能直接开发一款小游戏,特别棒!

在 mac 下跑起来卡的基本不能动,一小会直接卡死了,这是平台兼容性太差吗。。。

壮哉我大ruby,先赞一个,明天看看 😀

h8849326 回复

Mac 我没试过,Ubuntu 试过,是根本没法玩,我也找不到原因,自己估计可能是线程刷新的问题吧。

ysjrdfaps 回复

差不多把,卡到死的感觉,看来要玩楼主的代码,还得装个 windows 虚拟机。。。

ysjrdfaps 回复

楼主大神你好。我刚开始自学 Ruby,看到您的这个作品很羡慕,源码也已经在“码云”上下载了。想请教您的是:在 Windows 下运行如图所示(只有大本营周围有墙),请问是什么原因?感谢!

ghost2006bj 回复

这个其实就是关卡的问题,我已经更新了从第一关开始。你再试一下吧。

h8849326 回复

我的 Mac 怎么不能用,bundle install 就有错误了,gem 'cairo' 安装不了,请问是怎么解决的呢;

真不错,现在 ruby 都可以在 windows 上玩了么?不知道 rails 项目可不可以

pynix 回复

我出现了和你一样的问题,能看下你是怎么改的字体吗,谢谢

https://github.com/bingo8670/tankgame , Mac 版可安装运行---GitHub 地址

bingo8670 回复

修改 tank_game.rb 文件的 121 行的字体

pynix 回复

改成什么样,截个图好不😊

bingo8670 回复

早没了。。。你这坟挖得。。。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号