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

入力がマウスの座標しかないので、もう少し入力できるようにしておこう。とりあえずマウスのボタンとキーボードのよく使うやつだけって感じで。
DXRubyではマウスやパッド、キーの入力は定数を定義してあって、それを使う。内部的にはそれぞれのボタンの状態を配列に保存していて、定数には配列の添え字を代入してある。従って、単純に渡された値で配列を覗いてその結果を返すだけ、というのが入力系のロジックである。
基本的には定数を使ってアクセスするはずで、数値が入っていることを前提にしたプログラムはお行儀が悪い。なので、定数の中身は数値以外のものに変更しても問題は無いのだが、まあ、例えばWSはお行儀の悪いコードが書かれているのでそういう変更をすると動かなくなる。全くダメである。
それはともあれ、じゃあ数値以外の何かを入れるとして、どのような感じにすればより扱いやすくなるのか、というのが今回の話。

入力の取得方法

SDL2ではマウスやキーボードの操作はイベントで受け取ることができるが、イベントで受け取らなければならない、というルールは無くて、直接取得できる関数も用意されている。SDL2内部でハードウェアをチェックして、イベントを生成するときに状態を保持していて、それをユーザが使えるようになっている。
それを使ってInput.update内で取得し、インスタンス変数に保持しておくように作る。

module DXRuby
  module Input
    # マウスの情報
    @mouse_button = @mouse_x = @mouse_y = 0
    @old_mouse_button = @old_mouse_x = @old_mouse_y = 0

    # キーボードの情報
    @keys = []
    @old_keys = []

    # 内部情報の公開
    def self._mouse_button;@mouse_button;end
    def self._old_mouse_button;@old_mouse_button;end
    def self._keys;@keys;end
    def self._old_keys;@old_keys;end

    def self.update
      # 押されているキー一覧を取得する
      @old_keys = @keys
      @keys = SDL.get_keyboard_state

      # マウスの状態を取得する
      @old_mouse_button, @old_mouse_x, @old_mouse_y = @mouse_button, @mouse_x, @mouse_y
      @mouse_button, @mouse_x, @mouse_y = SDL.get_mouse_state

      # SDL2のイベント処理
      while event = SDL.poll_event do
        case event.type
        when SDL::QUIT
          return true
        end
      end
      false
    end
  end
end

定数に入れるもの

さて、主題の定数に入れるオブジェクトについてだが、今回はRubyでクラスを作って、それのインスタンスを設定するようにしてみよう。これは非常にコンパクトなものであり、ボタン1つに対してオブジェクトを1個生成して割り当てていく。

module DXRuby
  module Input
    # マウスボタン判定用クラス
    class MouseButton
      def initialize(b) # 1が左、2が真ん中、3が右
        @button = b
      end

      def down?
        (SDL::BUTTON(@button) & Input._mouse_button) != 0
      end

      def push?
        (SDL::BUTTON(@button) & Input._mouse_button) != 0 and
        (SDL::BUTTON(@button) & Input._old_mouse_button) == 0
      end

      def release?
        (SDL::BUTTON(@button) & Input._mouse_button) == 0 and
        (SDL::BUTTON(@button) & Input._old_mouse_button) != 0
      end
    end
  end

  M_LBUTTON = Input::MouseButton.new(1)
  M_MBUTTON = Input::MouseButton.new(2)
  M_RBUTTON = Input::MouseButton.new(3)
end

MouseButton#initializeの引数はSDL2リファレンスのSDL_BUTTONマクロのところに書いてあったので固定で渡す感じ。これでマウスボタン3つ分のオブジェクトをそれぞれ生成すると、このオブジェクト自身が自分に割り当てられたボタンを判定することができる。
したがって、DXRuby互換の入力チェックメソッドは以下のようになる。

def self.mouse_push?(button)
  button.push?
end

また、以下のようにも書ける。

M_LBUTTON.push?

ただし、こう書くとpush?メソッドを持つオブジェクトを定数に入れることが義務付けられてしまうので、あまり嬉しい話ではなく、オススメはできない。

キーボードの入力

キーボードのほうも同様に、

module DXRuby
  module Input
    # キーボード判定用クラス
    class Keyboard
      def initialize(k)
        @key = k
      end

      def down?
        Input._keys[@key]
      end

      def push?
        Input._keys[@key] and !Input._old_keys[@key]
      end

      def release?
        !Input._keys[@key] and Input._old_keys[@key]
      end
    end
  end

  K_LEFT    = Input::Keyboard.new(SDL::SCANCODE_LEFT)
  K_RIGHT   = Input::Keyboard.new(SDL::SCANCODE_RIGHT)
  K_UP      = Input::Keyboard.new(SDL::SCANCODE_UP)
  K_DOWN    = Input::Keyboard.new(SDL::SCANCODE_DOWN)
  K_SPACE   = Input::Keyboard.new(SDL::SCANCODE_SPACE)
  K_ESCAPE  = Input::Keyboard.new(SDL::SCANCODE_ESCAPE)
  K_Z       = Input::Keyboard.new(SDL::SCANCODE_Z)
  K_X       = Input::Keyboard.new(SDL::SCANCODE_X)
  K_C       = Input::Keyboard.new(SDL::SCANCODE_C)
end

てな感じで作っていけば、同じように扱うことができる。この場合、mouse_push?にK_Zを渡しても問題なく判定できてしまうが、わざわざエラーに倒す必要もなかろう。共通で扱うことを明示的に表すInput.push?などを用意しておいたほうがいいかもしれない。

スキャンコードの話

SDL2ではSDL_SCANCODE_で始まるスキャンコードと、SDL_K_で始まるキーコードの2種類が存在する。スキャンコードはキーボードの物理的な配置を表現していて、キーコードはそれが表す文字を表現する。
SDL2リファレンスによると、例えばドイツのキーボードではQWERTZ配列というのがあって、QWERTY配列のキーボードのYの位置にZがある。らしい。こういう場合、Zで弾を撃つと言ってみても、Zって押しにくい場所にあるな!って話になってしまうので、スキャンコードを使って、左シフトの横のキーで弾を撃つようにする。逆に、頭文字で意味をもたせてショートカットキーを作る場合はItemのI、SkillのS、みたいになるのでキーコードを使ったほうがよい。ということらしい。
とはいえ、物理的なキーの並び自体が世の中のキーボードで共通というわけでもないはずで、左シフトの横にキーが無い場合があったりするかもしれないなど考えると、スキャンコードも完璧ではない、という話にもなる。結局、最良の対策はキーコンフィグを用意しましょう、ということである。まあ、標準的じゃないキーボードを使ってる人にとっては日常茶飯事だろうし、よくある話なのかもしれない。
sdl2rはSDL2バインダなのでスキャンコードもキーコードも両方扱えるようになっている。今回はスキャンコードを使っているが、キーコードが使いたければそのように作ればよいだけである。

おしまい

これがDXRubyよりも良い仕様かどうかはわからないのだが、少なくともDXRubyで悩んでいた部分は解決できそうだ。あとはゲームパッド周りができていないので、そこをどうするかを考えなければならない。
定数にFixnumを詰め込んでいたのと比べると、細かいオブジェクトを数百ほど生成することになるので、そういう意味では効率が悪い。また、オートリピート関連の処理をどう作るかを全く考えていない。SDL1.2の頃はオートリピート機能があったのだが、SDL2では無くなっているので、自前でなんとかする必要がある。
ということで、作ってはみたけど大きく変わっちゃうかもしれないね〜って感じの入力系でした。
今回までのコードはこちら