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は使わなくてもボタンの状態の監視はできるのでゲームパッドとしては機能する。でも、毎フレーム全ボタンの状態をチェックするのはちょっとナンだなぁ。
Processingでゲームパッドを使おう (その4)
関連記事。
- Processingでゲームパッドを使おう (その1) - 雑念日記
- Processingでゲームパッドを使おう (その2) - 雑念日記
- Processingでゲームパッドを使おう (その3) - 雑念日記
これまで過去記事でGameControlPlusを使ってゲームパッドを扱う方法を書いてきましたが、いずれも自分(開発者)だけが自分のために使うような方法ばかりでした。
予め使用するゲームパッドの詳細を調べた上で設定ファイルを書いておく、だとか、アプリ起動時に以下のようなゲームパッドの設定画面が表示されるので画面に従って設定をする、だとか。
しかし汎用的にいろんな人に使ってもらうようなアプリを作ろうと思ったら、これらの手順はウラでやっておきたいこと、表に出したくないことだったりします。
とにかくアプリを起動したらすぐゲームパッドを使えるような状況にしたい!必要ならキーコンフィグ画面はアプリ側で作るから!みたいな需要もあるはず。
それを実現してみたのが以下。
import org.gamecontrolplus.*; import java.util.List; import net.java.games.input.Controller; ControlIO control; ControlDevice device; ControlButton[] buttons; // 取得対象デバイス候補となるタイプの一覧 // ここでは "Stick", "Fingerstick", "Gamepad" が対象となる List<String> deviceTypeCandidates = new ArrayList<String>() { { add(Controller.Type.STICK.toString()); add(Controller.Type.FINGERSTICK.toString()); add(Controller.Type.GAMEPAD.toString()); } }; void setup() { control = ControlIO.getInstance(this); List<ControlDevice> deviceList = control.getDevices(); // 利用可能なデバイスのうち、取得対象タイプに含まれるもので // 一番最初に見つかったものを取得・有効化 for (ControlDevice dev : deviceList) { String type = dev.getTypeName(); if (deviceTypeCandidates.contains(type)) { device = dev; device.open(); break; } } if (device == null) { println("ERROR: No available devices found."); return; } // 取得したデバイスが持つボタンをすべて取得して配列に持つ // ボタンを押したときに pressButton() が呼び出されるようにする int buttonNum = device.getNumberOfButtons(); buttons = new ControlButton[buttonNum]; for (int i = 0; i < buttonNum; i++) { buttons[i] = device.getButton(i); buttons[i].plug(this, "pressButton", ControlIO.ON_PRESS); } } // 押されたボタンの名前を表示する void pressButton() { for (ControlButton b : buttons) { if (b.pressed()) println("PRESSED: ", b.getName()); } } void draw() { // 入力待ち }
ちょっとした解説
以下の行でこのコードを走らせているマシンで利用可能なデバイスのリストが取得できるため、
List<ControlDevice> deviceList = control.getDevices();
このリストの中からゲームパッドを探します。
それが次の部分です。
for (ControlDevice dev : deviceList) { String type = dev.getTypeName(); if (deviceTypeCandidates.contains(type)) { ...
getTypeName()メソッドでデバイスのタイプが取得できるため、リストに含まれるデバイス1個1個に対し、そのタイプが取得候補に含まれるかどうかをチェックしています。
ちなみに今回、取得候補はSTICK, FINGERSTICK, GAMEPADとしましたが、それ以外に使われるタイプとしてはUNKNOWN, MOUSE, KEYBOARD, HEADTRACKER, RUDDER, TRACKBALL, TRACKPAD, WHEELがあるようです。
その後、ゲームパッドかそれに近いデバイスが見つからなかったら処理を中断。見つかったら以下の部分でそのデバイスのボタンの数を取得し、
int buttonNum = device.getNumberOfButtons();
そのボタン全てに対してイベントハンドラを登録します(その3参照)。
buttons[i].plug(this, "pressButton", ControlIO.ON_PRESS);
欠点
動的にボタンを取得するので、どの変数がどのボタンに割り当てられるかがまったくわかりません。機器依存になりそうです。前述してますが、ひとまずゲームパッドとそのボタンを認識する、というところまでをアプリ起動時に動的にやってしまい、ボタンとイベントの関連付けはアプリ内でやる、という用途に適しているんじゃないかなーと思います。
Processingでゲームパッドを使おう (その3)
関連記事。
- Processingでゲームパッドを使おう (その1) - 雑念日記
- Processingでゲームパッドを使おう (その2) - 雑念日記
- Processingでゲームパッドを使おう (その4) - 雑念日記
GameControlPlusを使ってゲームパッドを扱うにあたって、ボタンにイベントハンドラを追加することができます。
以下のようにすることで、draw()メソッド内に複雑な処理を書かなくて済むようになる上に、ボタンを押した(離した)とき1回だけの処理を簡単に記述することができます。
サンプルコード:
import org.gamecontrolplus.*; ControlIO control; ControlDevice device; ControlButton button; void setup() { control = ControlIO.getInstance(this); device = control.getMatchedDevice("gamepad_config"); button = device.getButton("B_0"); button.plug(this, "pressButton", ControlIO.ON_PRESS); button.plug(this, "releaseButton", ControlIO.ON_RELEASE); } // ボタンが押されたタイミングでメッセージを表示 void pressButton() { println("PRESSED!"); } // 押されたボタンを離すタイミングでメッセージを表示 void releaseButton() { println("RELEASED!"); } void draw() { // 入力待ち }
イベントハンドラの追加を行うplug()メソッドの詳細は以下です。
public void plug(Object i_object, String i_methodName, int i_eventType)
引数 | 説明 |
---|---|
i_object | 第3引数で指定するイベントが発生したときに、ここで指定したオブジェクトをレシーバとして、第2引数で指定するメソッドを実行する。 |
i_methodName | イベント発生時に実行するメソッドの名前を指定。 |
i_eventType | トリガーとなるイベントを指定。 |
トリガーとなるイベントは以下の3つの中から選べます。
定数名 | 説明 |
---|---|
ON_PRESS | ボタン押下時に1度だけ発火するイベント。 |
ON_RELEASE | 押下状態のボタンを離したときに1度だけ発火するイベント。 |
WHILE_PRESS | ボタン押下中に発火し続けるイベント。 |
以下のように、1つのイベントに対して複数のイベントハンドラを追加することも可能です。
void setup() { control = ControlIO.getInstance(this); device = control.getMatchedDevice("gamepad_config"); button = device.getButton("B_0"); button.plug(this, "pressButton", ControlIO.ON_PRESS); button.plug(this, "pressButton2", ControlIO.ON_PRESS); } void pressButton() { println("PRESSED!"); } void pressButton2() { println("PRESSED 2 !"); }
なお、plug()メソッドが使えるのはボタン(ControlButton)とハット(ControlHat)のみで、スライダー(ControlSlider)にはイベントハンドラを登録することはできない模様。