sdl2rでゲームライブラリを作る(2)

前回は基本部分をとりあえず作ってみたところまでだった。今回はもうちょい進めたいところだが、その前に少しだけ周辺の話。
まず、Rubyのいいところに「既存のクラスを好きなように拡張できる」というのがある。モンキーパッチであり、オープンクラスである。これを使うと、例えば前回のサンプルコードをちょといじって、

require_relative 'dxsdl2r'

module Input
  def self.mouse_xy
    [mouse_x, mouse_y]
  end
end

Window.loop do
  x, y = Input.mouse_xy
  Window.caption = "#{x}, #{y}"
end

という感じで勝手にInput.mouse_xyメソッドを生やしてしまうことができる。これはsdl2rじゃなくてDXRubyでも同様のことができて、便利なメソッドを追加したいと思えば、思ったときにその場ですぐ実装することができてしまう。
dxsdl2r.rbの場合は更に、sdl2rの機能を使って機能拡張をやりたい放題となるので、これは極めて重要である。言い方を変えれば、必要とあらばSDL2の機能を直接使って処理を行うことができるし、機能を追加できる、ということだ。
こういうことをやりやすいように、Windowモジュールなどの内部情報を簡単に引き出せるようにしておくとよい。

module DXRuby
  module Window
    def self._window;@window;end
    def self._renderer;@renderer;end
  end
end

こんな感じで。

とりあえずの描画機能を作る

まだ画面に何も描画できないので、簡単な描画機能を作ってみよう。SDL2にはSDL_FillRectという関数があって矩形が手軽に描画できるので、まずはそれを使ってみる。

require_relative 'dxsdl2r'

module DXRuby
  module Window
    def self._renderer;@renderer;end
  end
end

Window.loop do
  SDL.set_render_draw_color(Window._renderer, 255, 0, 0, 255)
  SDL.render_fill_rect(Window._renderer, SDL::Rect.new(Input.mouse_x, Input.mouse_y, 50, 50))
end

これで赤い四角をマウスカーソルの位置に描画することができる。

Windowに機能追加する

んじゃあ、この機能をDXRubyのようにWindowモジュールのWindow.draw_box_fillとして実装してみる。

require_relative 'dxsdl2r'

module DXRuby
  module Window
    def self.draw_box_fill(x1, y1, x2, y2, color, z=0)
      SDL.set_render_draw_color(@renderer, color[0], color[1], color[2], 255)
      SDL.render_fill_rect(@renderer, SDL::Rect.new(x1, y1, x2 - x1 + 1, y2 - y1 + 1))
    end
  end
end

Window.loop do
  x, y = Input.mouse_x, Input.mouse_y
  Window.draw_box_fill(x, y, x + 50, y + 50, [255, 0, 0])
end

この時点で気になることが2点。まず、色の配列がRGBしか扱えなくてα値が指定できない。これをどうにかするにはDXRubyの色の指定方法を処理するメソッドなりなんなりが必要になるだろう。だがまだない。DXRubyとは違うのだよーとか言ってRGBA配列で表現してもいいっちゃいいんだけど。
次に、z値が無視されている。DXRubyではz値が小さい順にzソートされてから描画されるが、そのへんのロジックがまだない。

色配列対応

とりあえず適当に対応してみよう。

require_relative 'dxsdl2r'

module DXRuby
  module Window
    def self._window;@window;end
    def self._renderer;@renderer;end

    def self.draw_box_fill(x1, y1, x2, y2, color, z=0)
      tmp = DXRuby._convert_color_dxruby_to_sdl(color)
      SDL.set_render_draw_color(@renderer, tmp[0], tmp[1], tmp[2], tmp[3])
      SDL.render_fill_rect(@renderer, SDL::Rect.new(x1, y1, x2 - x1 + 1, y2 - y1 + 1))
    end
  end

  def self._convert_color_dxruby_to_sdl(color)
    if color.size == 3
      color + [255]
    else
      color[1..3] << color[0]
    end
  end
end

Window.loop do
  x, y = Input.mouse_x, Input.mouse_y
  Window.draw_box_fill(x, y, x + 50, y + 50, [255, 0, 0])
end

こんな感じで内部用のメソッドを作っておけばよさげか。

zソート

DXRubyでは描画は描画予約として、描画パラメータと描画関数のポインタを保持しておいて、描画直前にz値でソートをするという、地味に面倒なことをして自動zソートを実現している。それができるようにプログラムの構成やデータ構造が作られている。
じゃあ、Rubyで書く場合も同じように頑張る必要があるのか、というと、実際そんなことはなくて、Cで書くとめんどいだけで、Rubyでは思った以上に簡単に作れる。
具体的にどうするかというと、描画予約はProcオブジェクトにパラメータと描画ロジックを詰め込んでおいて、z値とProcをセットした配列を作って、zでソートする。んで、あとでProcを順番に呼び出す。

require_relative 'dxsdl2r'

module DXRuby
  module Window
    def self._window;@window;end
    def self._renderer;@renderer;end
    @reservation = []

    def self.draw_box_fill(x1, y1, x2, y2, color, z=0)
      tmp = DXRuby._convert_color_dxruby_to_sdl(color)
      prc = ->{
        SDL.set_render_draw_color(@renderer, tmp[0], tmp[1], tmp[2], tmp[3])
        SDL.render_fill_rect(@renderer, SDL::Rect.new(x1, y1, x2 - x1 + 1, y2 - y1 + 1))
      }
      @reservation << [z, prc]
    end

    def self.loop
      timer = FPSTimer.instance
      timer.reset
      SDL.set_window_size(@window, @width, @height)
      SDL.show_window(@window)
      Kernel.loop do
        timer.wait_frame do
          return if Input.update
          SDL.set_render_draw_color(@renderer, 0, 0, 0, 255)
          SDL.render_clear(@renderer)

          yield

          @reservation.sort_by!{|v|v[0]}.each{|v|v[1].call}
          @reservation.clear

          SDL.render_present(@renderer)
        end
      end
    end
  end

  def self._convert_color_dxruby_to_sdl(color)
    if color.size == 3
      color + [255]
    else
      color[1..3] << color[0]
    end
  end
end

Window.loop do
  x, y = Input.mouse_x, Input.mouse_y
  Window.draw_box_fill(100, 100, 200, 200, [0, 0, 255], 0)
  Window.draw_box_fill(150, 150, 250, 250, [0, 255, 0], 2)
  Window.draw_box_fill(x, y, x + 50, y + 50, [255, 0, 0], 1)
end

Window.loopを修正する必要があったのでちょと長くなってしまった。ともあれこれで描画した順序ではなく指定したz値で描画順が制御されることになる。Cで書いたときは大変だったのにRubyだと簡単にできるので感動である。

おしまい

今日のコードを含めたライブラリ側はこちらに置いておいた。
わりと頑張ったのだが進んだような進んでないような感じである。せめてImageクラスができてくれればいいのだが。