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

トウフが表示できるだけではさすがにしょんぼりなので、今回はImageクラスと、Imageを描画するWindow.drawメソッドを作ってみよう。
とりあえずコードを出そう。

module DXRuby
  C_WHITE = [255, 255, 255]

  class Image
    attr_reader :_surface, :_texture, :width, :height

    def initialize(w, h, color=[0, 0, 0, 0])
      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))
      @_texture = SDL.create_texture_from_surface(Window._renderer, @_surface)
      @width, @height = w, h
    end
  end
end

んで、SDL2で画像を扱う際のいくつかの注意点を挙げる。

SDL2の画像

SDL2では画像データはSDL_Surfaceで扱う。sdl2rではSDL::Surfaceクラスとなる。これはSDL1.xの頃からある古いもので、世の中に無数にあるアーキテクチャを柔軟にサポートできるように作られている。例えば画像の色フォーマットについてもそうだし、CPUのエンディアンについてもそう。とにかく柔軟なので、これを使うのはとにかくめんどくさい。
今回のコードに出てくるSDL::BYTEORDERなる定数はCPUのエンディアンを表していて、例えばMacとかWindowsではIntelのCPUが使われているのでリトルエンディアンになるわけだが、SDL2は他のCPUアーキテクチャもサポートするのでビッグエンディアンのCPUで使われても動くようにする必要がある。かもしれない。sdl2rはRubyとSDL2が動く環境ならどこでも動くはずであり、そういう意味ではどこでも動いて欲しいので、とりあえずビッグエンディアンをサポートするコードを入れておいた。ちなみにこの書き方はリファレンスマニュアルのコピペである。
エンディアンを見て変更しているのは32bitカラーの順番であるわけだが、SDL_Surfaceは別に32bitカラー限定ではない。モノラルから他いろいろ、たいがいのフォーマットはサポートしている。カラーパレットも使える。それらすべてで共通のコードで色を置いたり読んだりできるかというと、実は全然そうでもなくて、色データへのポインタが取得できるだけで、フォーマットごとに違うアドレス計算などはユーザ任せになっている。ひどい話である。
sdl2rではありがちなフォーマットについてはアドレス計算を網羅してあり、色フォーマットごとの色データの違いはSDL2がうまいこと処理してくれるので、Rubyから扱う場合はSDL::Surface#pixelsで座標とSDL::Colorだけで処理できるようになっている。

Image#initialize

Image#initializeではとりあえずSurfaceを指定サイズで作って、それの全体を指定色で塗りつぶして、そこからSDL_Textureオブジェクトを生成する。SDL2のRenderer経由で描画するにはSDL_Textureオブジェクトが必要だからである。これに関する処理あれこれはまたあとで。
最後に幅と高さを保存して終了する。

Window.draw

Imageだけ作れても意味がないので描画メソッドも作る。

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

Procを使ったりする部分は前回と一緒で、描画処理そのものはSDL_RenderCopyを使う。これがRenderer経由でTextureを描画してくれる関数である。
SDL_RenderCopyは地味に曲者で、描画先を表すSDL_Rectで描画サイズを変更することができる。これを使うとスケーリングが自由にできる、ということになるわけだが、指定できるのは整数だけなので、スケーリング時の位置が微妙に調整できない。これはよろしくない。

全体のコード

その他サンプルコードまで含めて今回のコードはこんな感じになる。

require_relative 'dxsdl2r'

module DXRuby
  C_WHITE = [255, 255, 255]

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

  class Image
    attr_reader :_surface, :_texture, :width, :height

    def initialize(w, h, color=[0, 0, 0, 0])
      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))
      @_texture = SDL.create_texture_from_surface(Window._renderer, @_surface)
      @width, @height = w, h
    end
  end
end

image = Image.new(100, 100, C_WHITE)

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

前回までのsdl2rと一緒にこれを動かすとマウスの位置にトウフが出るはずである。Imageクラス作ったのに今回もトウフで終わってしまった。
ここまでのコードをsdl2r.rbとしてまとめたものはここにおいておいた。ほとんど使い物にはならないが使ってみたい人はどうぞ。