雑念日記

主に技術的なことをつらとらと(書ければいいな)。

ruby-processingでゲームパッドを使おう

以下の記事でProcessingでのゲームパッド利用についていろいろ書きましたが、

最終目標はruby-processingでゲームパッドを扱うことなのでやってみた。

サンプルコード

コンセプトは以下。

  • 環境依存しないで自動でゲームパッドを認識すること
    • 名前指定によるデバイス取得をしない
    • 設定ファイルによるデバイス取得をしない
  • ボタンを押したタイミングで、どのボタンを押したか表示する
  • 十字キーを押している間、押された軸方向と数値を表示する
load_library :GameControlPlus
import "org.gamecontrolplus.ControlIO"

STATUS = {push: 0, release: 1, down: 2}

# 取得するデバイスのタイプ候補を定義
DEVICE_TYPES = [
                Java::NetJavaGamesInput::Controller::Type::STICK,
                Java::NetJavaGamesInput::Controller::Type::FINGERSTICK,
                Java::NetJavaGamesInput::Controller::Type::GAMEPAD,
               ].map(&:to_string).freeze

def setup
  @buttons = []
  @sliders = {}
  @status = {}
  load_gcp_library

  # 候補に合致するデバイスを取得
  control = ControlIO.get_instance(self)
  list = control.get_devices
  device = list.find {|dev| DEVICE_TYPES.include?(dev.get_type_name) }

  return if device.nil?

  # デバイスからボタンを取得し、ステータスを初期化する
  device.get_number_of_buttons.times do |i|
    @buttons[i] = device.get_button(i)
    @status[@buttons[i].name] = STATUS[:release]
  end

  # デバイスからスライダーを取得
  device.get_number_of_sliders.times do |i|
    slider = device.get_slider(i)
    @sliders[slider.name] = slider
  end

  # デバイスの有効化
  device.open
end

# ライブラリのロード処理の補完
def load_gcp_library
  gcp_library_path =
    @@library_loader.send(:get_library_directory_path, "GameControlPlus")
  java.lang.System.setProperty("java.library.path", gcp_library_path)
  loader = java.lang.Class.for_name("java.lang.ClassLoader")
  field = loader.get_declared_field("sys_paths")
  if field
    field.accessible = true
    loader = java.lang.Class.for_name("java.lang.System").get_class_loader
    field.set(loader, nil)
  end
end

def draw
  # 毎フレーム全ボタンのステータスをチェック
  @buttons.each do |btn|
    key = btn.name
    if btn.pressed
      # ボタンが押されている場合は最初のフレームのみpush、
      # 以降のフレームではdownとする
      case @status[key]
      when STATUS[:release];
        @status[key] = STATUS[:push]
      when STATUS[:push]
        @status[key] = STATUS[:down]
      end
    else
      @status[key] = STATUS[:release]
    end
  end

  # ステータスがpushのボタンを表示
  @status.each do |name, status|
    puts "PUSHED: #{name}" if status == STATUS[:push]
  end

  # スライダーを操作すると値を表示
  ['x', 'y'].each do |dir|
    if @sliders[dir] && @sliders[dir].value.abs > 0
      puts "#{dir}: #{@sliders[dir].value}"
    end
  end
end

メモ

load_gcp_libraryについて

普通にload_libraryしてimportしてもまず以下で躓きました。

load_library :GameControlPlus
import "org.gamecontrolplus.ControlIO"

def setup
  control = ControlIO.get_instance(self)
end

jinputのロードに失敗してる。

Failed to load 64 bit library: no jinput-linux64 in java.library.path
Java::JavaLang::UnsatisfiedLinkError
no jinput-linux in java.library.path
        java.lang.ClassLoader.loadLibrary(ClassLoader.java:1878)
        java.lang.Runtime.loadLibrary0(Runtime.java:849)
        ...(省略)...

ソースを追いかけてみると、ここで対象のライブラリをrequireするところまではうまくいっている。
https://github.com/jashkenas/ruby-processing/blob/2.4.4/lib/ruby-processing/library_loader.rb#L60

その後、ここでjava.library.pathにパスを追加する処理を行っているのだけれど、
https://github.com/jashkenas/ruby-processing/blob/2.4.4/lib/ruby-processing/library_loader.rb#L62-L78

その対象となっているパスが(僕の環境では)以下の2つ。

/path/to/sketchbook/libraries/GameControlPlus/library/linux
/path/to/sketchbook/libraries/GameControlPlus/library/linux64

しかしGameControlPlusのライブラリが配置してあるディレクトリの構成は

$ ls -l /path/to/sketchbook/libraries/GameControlPlus/library
合計 636
-rw-r--r-- 1 hoshi hoshi 254735 Jun  8 15:41 GameControlPlus.jar
-rw-r--r-- 1 hoshi hoshi  73728 Jun  8 15:41 jinput-dx8.dll
-rw-r--r-- 1 hoshi hoshi  65024 Jun  8 15:41 jinput-dx8_64.dll
-rw-r--r-- 1 hoshi hoshi  69632 Jun  8 15:41 jinput-raw.dll
-rw-r--r-- 1 hoshi hoshi  69632 Jun  8 15:41 jinput-wintab.dll
-rw-r--r-- 1 hoshi hoshi  10204 Jun  8 21:36 libjinput-linux.so
-rw-r--r-- 1 hoshi hoshi  14512 Jun  8 16:08 libjinput-linux64.so
-rw-r--r-- 1 hoshi hoshi  65944 Jun  8 15:41 libjinput-osx.jnilib

のようになっており、ruby-processingが想定する構成となっていないため、新しいライブラリのパスとして追加されません。

ので、以下のURLを参考に、GameControlPlusのライブラリのパスを別途追加してあげることで解決。
https://github.com/jashkenas/ruby-processing/blob/2.4.4/lib/ruby-processing/library_loader.rb#L54-L80

load_library :GameControlPlus
import "org.gamecontrolplus.ControlIO"

def setup
  gcp_library_path =
    @@library_loader.send(:get_library_directory_path, "GameControlPlus")
  java.lang.System.setProperty("java.library.path", gcp_library_path)
  loader = java.lang.Class.for_name("java.lang.ClassLoader")
  field = loader.get_declared_field("sys_paths")
  if field
    field.accessible = true
    loader = java.lang.Class.for_name("java.lang.System").get_class_loader
    field.set(loader, nil)
  end

  control = ControlIO.get_instance(self)
end

ここまででControlButtonの取得などは可能になったので、ボタン押下の検出などは可能になりました。

plugメソッドを使ったイベントハンドラの登録ができない

取得したボタンに対して以下のようにplugを試みたのですが、

  button.plug(self, 'press_button',
              Java::OrgGamecontrolplus::PCPconstants::ON_PRESS)

以下のようなエラーが発生してしまいました。

Error on plug: >press_button< procontrol found no method with that name in the given object.
	org.gamecontrolplus.Plug.initPlug(Unknown Source)
	org.gamecontrolplus.Plug.<init>(Unknown Source)

同じことで悩んでいる過去のフォーラムを発見するも、どうも根が深そう。
http://processing.org/discourse/beta/num_1235723223.html

JRubyではJava側で定義されたクラスを継承したり、メソッドをオーバーライドしたりできるけれど、それが見えるのはJRuby側からのみで、Java側からは見えない。
なので、上でplugするときに渡しているpress_buttonという名前のメソッドをスケッチのどこかに定義していても、Java側でそのメソッドを見つけられないという状況かな。

以下に試行錯誤の足跡を貼るが、結局plugはruby-processingで扱えなかったという話。

# class RP5Plug < Java::OrgGamecontrolplus::Plug
class RP5Plug
  def initialize(object, method_name)
  end

  def call(*args)
    p 'fire!'
  end
end

class Java::OrgGamecontrolplus::ControlButton
  field_reader :onPressPlugs, :onReleasePlugs, :whilePressPlugs

  # JRuby(ruby-processing)用にplug()と同等のメソッドを用意
  def rb_plug(object, method_name, event_type)
    plug = RP5Plug.new(object, method_name)
    case event_type
    when Java::OrgGamecontrolplus::ControlIO::ON_PRESS
      plug_list = self.onPressPlugs
    when Java::OrgGamecontrolplus::ControlIO::ON_RELEASE
      plug_list = self.onReleasePlugs
    when Java::OrgGamecontrolplus::ControlIO::WHILE_PRESS
      plug_list = self.whilePressPlugs
    else
      raise "Error on plug #{method_name} check the given event type"
    end
    plug_list.add(plug)
  end

  # callPlugs()が呼ばれるのはJava側から。
  # JRuby側で定義/オーバーライドしたメソッドはJava側からは見えないので、
  # ここで定義しているcallPlugsは呼ばれない。
  def callPlugs(plug_list)
    plug_list.size.times do |i|
      plug = plug_list.get(i)
      plug.call
    end
  end
end

取得したボタンに対して自前で定義したrb_plugメソッドを使って、JRubyから見えるメソッドイベントハンドラに登録してやろうとした形跡。

rb_plug自体は上手く動くが、その後イベント発火時に呼ばれるcallPlugsメソッドで以下のエラーが発生する。

Java::JavaLang::ClassCastException
org.jruby.RubyObject cannot be cast to org.gamecontrolplus.Plug
        org.gamecontrolplus.ControlButton.callPlugs(Unknown Source)
        org.gamecontrolplus.ControlButton.update(Unknown Source)
        org.gamecontrolplus.ControlDevice.update(Unknown Source)
        org.gamecontrolplus.ControlIO.run(Unknown Source)
        java.lang.Thread.run(Thread.java:724)

callPlugs内でControlButtonが持っているPlugのリストから取り出すときにキャストしているのがうまくいかないため。なのでキャストしなくてもいいように、と思ってcallPlugsをオーバーライドしようと思ったがコメントにも書いているようにそんなことはできず。

JRubyはクセがあって難しい。
plugは使わなくてもボタンの状態の監視はできるのでゲームパッドとしては機能する。でも、毎フレーム全ボタンの状態をチェックするのはちょっとナンだなぁ。