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は使わなくてもボタンの状態の監視はできるのでゲームパッドとしては機能する。でも、毎フレーム全ボタンの状態をチェックするのはちょっとナンだなぁ。