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

ここらで方針を決めておこう。どういったものを作るつもりでいるのか。それが定まっていないと二転三転してしまうから。
まず、DXRuby完全互換は目指さない。もっといいアイデアがあればそのように作るし、それがよさげであればDXRubyのほうに取り込まれちゃったりもするかもしれない。ともあれ、SDL2を使うわけだし、同じものが作れるわけでもない。SDL2の良さを引き出す方向性のほうがいいはずだ。じゃあなんで逆にDXRuby的なものを作るのかというと、俺にとってそれが扱いやすいから、である。結局のところ、自分で使うものを作っているだけとも言える。
DXRubyでできることでもSDL2ではできない、ということもあるだろうが、その逆に、SDL2でしかできないこともあるだろう。そういったものは、せっかくできるのだから活用する方向で考える。例えばDXRubyとは違ってRubyで構築するのだから、sdl2rオブジェクトはユーザが操作できる。動作がわかっていてsdl2rのオブジェクトを直接操作するのであれば、それは問題ないので許可する方向となる。
こういう考え方でもって昨日のImageクラスを作り直す。

Imageクラス再び

昨日の時点のsdl2rに対してこのようにコードを書いて、ImageクラスとWindow.drawを再定義する。

require_relative 'dxsdl2r'

module DXRuby
  C_RED = [255, 0, 0]
  module Window
    def self.draw(x, y, image, z=0)
      prc = ->{
        image._create_texture unless image._texture
        SDL.render_copy(@renderer, image._texture, nil, SDL::Rect.new(x, y, image.width, image.height))
      }
      @reservation << [z, prc]
    end
  end

  class Image
    attr_accessor :_surface, :_texture, :_pixels

    def initialize(w, h, color=[0, 0, 0, 0])
      # wとhの両方が0の場合はSurfaceを生成しない
      return if w == 0 and h == 0

      # IntelCPUはリトルエンディアンだがビッグエンディアンにも一応対応しておく
      # 画像フォーマットは32bit固定
      if SDL::BYTEORDER == SDL::BIG_ENDIAN
        @_surface = SDL.create_rgb_surface(0, w, h, 32, 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff)
      else
        @_surface = SDL.create_rgb_surface(0, w, h, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000)
      end

      # 指定色で塗りつぶす
      SDL.fill_rect(@_surface, nil, DXRuby._convert_color_dxruby_to_sdl(color))

      # Pixelsオブジェクト取得
      @_pixels = @_surface.pixels
    end

    def width
      @_surface.w
    end

    def height
      @_surface.h
    end

    # テクスチャを破棄する
    # 次に描画で使われる際に再生成される
    def _modify
      if @_texture
        SDL.destroy_texture(@_texture)
        @_texture = nil
      end
    end

    # テクスチャ生成
    def _create_texture
      # テクスチャ生成
      @_texture = SDL.create_texture_from_surface(Window._renderer, @_surface)
    end

    # ピクセルに色を置く
    def []=(x, y, color)
      @_pixels[x, y] = DXRuby._convert_color_dxruby_to_sdl(color)
      self._modify
    end

    # ピクセルの色を取得する
    def [](x, y)
      @p_ixels[x, y]
      self._modify
    end
  end
end

image = Image.new(100, 100, C_WHITE)
image[50,50] = C_RED

Window.loop do
  x, y = Input.mouse_x, Input.mouse_y
  Window.draw(x, y, image)
end

解説

昨日のImageクラスと大きく変わったところは、まずattr_readerがattr_accessorに変わったところだろう。sdl2rオブジェクトはユーザが差し替えることも可能にするわけだ。当然、使い方を間違えるとおかしな動きをするだろうが、それはユーザの責任である。
あと、initializeの頭でwとhが0の場合はSurfaceを生成しないようにしている。これはImage.loadを作るための布石で、Image.loadはクラスメソッドであり、外部からImageインスタンスを生成する必要があるのだが、普通にnewすると無駄にSurfaceを生成してしまうので、Surfaceを作らずあとで外部から設定できるようにした。dxruby_sdlで使われていた手をパクった。
他の大きな違いとしては、Textureをinitializeで生成しないようにした。Imageは中身を書き換えることができるオブジェクトになるので、それはつまりSurfaceを変更できるということで、Surfaceが変わればTextureは再生成しなければならない。直接Textureを更新するのもできるかどうかわからないのだが、なんか調べるのもめんどいので再生成である。んで、Surfaceを変更した場合にImage#_modifyを呼んでTextureを破棄し、描画系メソッド内で破棄されていたら再生成する感じになる。このルールに則ってさえいれば、例えばユーザが勝手にSurfaceを書き換えるとか、他のものに差し替えるだとかしても問題は無い。ということになる。
また、描画時に渡すImageオブジェクトはクラスをチェックしないので、必要なメソッドが存在し、想定される動作をしてくれれば、それがImageオブジェクトでなくても構わない。DXRubyではC構造体を持っているので厳密にチェックせざるをえないが、dxsdl2rの場合はRubyで書かれていて必要な情報はsdl2rオブジェクトなどでありRubyから扱えるので、似たようなものをユーザが作ればImageクラスである必要が無い。このあたりもDXRubyとは一味違う、Rubyならではのダックタイピングを活用した自由度となる。
ついでにピクセルの設定と読み出しのメソッドを追加しておいた。これはSDL::Surface::Pixelsオブジェクトを使う。このオブジェクトはSDL2には無くて、sdl2r独自のものである。SDL_Surfaceは様々な色フォーマットを生データで管理するので、Rubyでそれを書くのは面倒すぎるということで、抽象化した。ピクセルの読み書きを簡単にできるはずだ。

おしまい

ということで、DXRubyと似たような使い勝手ではあるものの本質的には随分と違うものになりそうなdxsdl2rが独自の一歩を踏み出した。
SDL2を使うのでできること、できないことがあるだろうけども、まあそのうち形にはなってくるんじゃなかろうか。DXRubyにあるメソッドを全部作るとかたぶん死ねるのでさすがに作らないと思うし、いずれはgithubにおいて放置するので誰か実装してぷるりちょうだいね。
ということでここまでのコードはこちらにおいといた。