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

文字の描画ができないと色々不便なのでFontクラスを作ろう。
DXRubyはWindowsAPIをベースに設計されているが、SDL_ttfはそれとは違うものなので、制約される条件がまったく違う。互換性を取るためにはそれなりの細工が必要であるが、そこまでして同じ動作をさせる必要があるかどうかはよくわからない。とはいえSDL_ttfに合わせて再設計するのもそれなりに大変なのでとりあえずはDXRuby互換インターフェイスを作る方向で行ってみよう。

Fontクラス

こんな感じになる。

module DXRuby
  class Font
    attr_accessor :_font

    # {ファイル名=>{フェイスのファミリー名=>index}}というハッシュで情報を保持する
    @@_fonts = {}

    def self.install(filename)
      tmp = {}
      result = []

      # 情報を得るためにとりあえずopenしてみる
      font = SDL::TTF.open_font(filename, 24)
      name = SDL::TTF.font_face_family_name(font)
      tmp[name] = 0
      result << name

      # 複数フェイスある場合は全部openしてみる
      (1...SDL::TTF.font_faces(font)).each do |i|
        fontt = SDL::TTF.open_font_index(filename, 24, i)
        name = SDL::TTF.font_face_family_name(fontt)
        tmp[name] = i
        result << name
        SDL::TTF.close_font(fontt)
      end
      SDL::TTF.close_font(font)

      @@_fonts[filename] = tmp
      result
    end

    def initialize(size, fontname="IPAGothic")
      @@_fonts.each do |filename, hash|
        if hash.include?(fontname)
          @_font = SDL::TTF.open_font_index(filename, size, hash[fontname])
          break
        end
      end
      raise unless @_font
    end

    self.install("./font/ipag.ttf")
    @_default_font = Font.new(24)

    def self.default
      @_default_font
    end
  end
end

フォントの名前ってファミリー名でいいのかな?よくわからん。WindowsAPIみたいにインストールされてるフォントが扱えるわけではないのでFont.installしてからFont.newする感じで。デフォルトはfontディレクトリ下のIPAゴシックとしてみた。手元にあったから。

SDL_ttfの情報と制約

微妙にややこしいことになっているが、なぜこうなっているのか。
まず、WindowsAPIではフォントの指定はファイル単位ではなく、何かしらのフォントの情報をOSに渡すと近いものを自動的に選んでくれるようになっている。その情報の一つに名前がある。ところが、SDL_ttfでは指定はファイル名とインデックスである。開いてみるまで名前はわからないし、いくつのフォントがそのファイルに含まれているかもわからない。
ということでとりあえずFont.install時に開いてみるわけなのだが、TTF_OpenFontは引数にサイズがあるのでこれも適当に渡さなければならない。開くとフェイス数が取得できるのだが、名前の取得は開いたフェイスのみしかできないので、取得したフェイス数をみながら別インデックスを指定して開きまくって情報を集めることになる。また、適当に開いてみただけでそのサイズを後で使うかどうかはわからないのでとりあえじ閉じておく。開く時にサイズを渡さないならそのままキャッシュできるのにな。
ともあれ、こうやって集めた情報をもとに、Font.new時にはファイル名とインデックスを決め打ちできるので、指定サイズでTTF_Fontオブジェクトを生成してFontインスタンスに保持するわけだ。ようするにSDL_ttfはフォントの名前で指定するようなライブラリでは無い。まあ、名前で指定しろと言われても名前わかんないってのはいつもの話なので、Font.installした時にそのファイルに含まれている名前の配列を返すようにしてある。わかんないときはこれで確認できる。
BoldとかItalicの指定はいつでもTTF_Fontのステータス変更で好きなものに変えられる。Fontを作るときに指定するWindowsではこれができないのでFont生成時に指定してImmutableにしていたが、SDL_ttfではこのあたりの指定をdraw_fontメソッドに移動することもできそうだ。隠蔽して細工すれば大概のことはできるからOSの制約でそうなったとも言えないんだけども。

描画

描画するほうのロジックはこんな感じになる。

module DXRuby
  class RenderTarget
    def draw_font(x, y, str, font, hash = {})
      option = {
        color: [255, 255, 255, 255],
        z: 0,
      }.merge(hash)
      sur = SDL::TTF.render_utf8_blended(font._font, str, DXRuby._convert_color_dxruby_to_sdl(option[:color]))
      tex = SDL.create_texture_from_surface(Window._renderer, sur)
      SDL.free_surface(sur)
      _, _, w, h = SDL.query_texture(tex)

      prc = ->{
        SDL.set_texture_blend_mode(tex, SDL::BLENDMODE_BLEND)
        SDL.render_copy(Window._renderer, tex, nil, SDL::Rect.new(x, y, w, h))
      }
      @_reservation << [option[:z], prc]
    end
  end
end

文字を描画したSurfaceを作って、そこからテクスチャを作って、Surfaceを破棄して、描画する。
SDL_ttfでは渡す文字列のエンコーディングによって関数が分かれているが、Rubyは文字列がエンコーディングを持っているので何を渡してもutf-8に変換されて渡されるようになっている。ので、エンコーディングごとのメソッドを作る必要も本来なくて、render_blendedなどのメソッドに統一してやったほうが使い勝手はいいのだろうが、SDL2と違う名前にするとマニュアルがないと困ることになるので、そういう調整は後回しである。いずれ完成してマニュアル作るときになったらメソッド名などは整理する。かもしれない。

おしまい

DXRubyのフォント関連にある袋文字や影の機能はCのパワーでごり押ししているのでRubyで書くと辛いことになりそうである。また、SDL_ttfがSurfaceを作ってくれる(ていうか作らなければならない)ので文字を描画したImageオブジェクトを作る、などの機能があると効率よさげ。
BoldとかItalicなどの機能はFontクラスに情報を持たせればすぐ対応できそうなので気が向いたらやる。というと長期間やらないことが多いのだが。
なお、今回までのコードはこちら
そういえばここまで作って公開しておいてアレだけどIPAゴシックはフェイスが1しかないので複数フェイスあるファイルを読んだときに正しく動くかはわからない。