雑念日記

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

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)

関連記事。


これまで過去記事でGameControlPlusを使ってゲームパッドを扱う方法を書いてきましたが、いずれも自分(開発者)だけが自分のために使うような方法ばかりでした。
予め使用するゲームパッドの詳細を調べた上で設定ファイルを書いておく、だとか、アプリ起動時に以下のようなゲームパッドの設定画面が表示されるので画面に従って設定をする、だとか。

f:id:hoshi_sano:20140612024357j:plain

しかし汎用的にいろんな人に使ってもらうようなアプリを作ろうと思ったら、これらの手順はウラでやっておきたいこと、表に出したくないことだったりします。
とにかくアプリを起動したらすぐゲームパッドを使えるような状況にしたい!必要ならキーコンフィグ画面はアプリ側で作るから!みたいな需要もあるはず。

それを実現してみたのが以下。

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)

関連記事。


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)にはイベントハンドラを登録することはできない模様。

参考: Javadocs: GameControlPlus