Javaでゲームパッドの入力をJInputを使って実現する
突然、ゲームを作ってみたくなりました。
ミニゲーム的なものは過去に何度も作ったことはありましたが、アクションとかRPGのような本格的なゲームは作ったことがありません。
それで、欲張りなことに、UnityやAndroidやSwift、つまりゲームエンジンやスマホアプリ開発に手を出そうとしています。なお、全然捗っていません。集中できるまとまった時間が確保できないと頭に入ってこないのです。
そこで、やはりというか、Javaでできる範囲なら、片手間でもなんとかなるんじゃないかと思いました。
手始めに、手元にある無線ゲームパッドの入力でキャラクターを動かすのに挑戦。
詳しくは後述しますが、JInputを使うことにしました。
今回は、JavaでJInputを使って、ゲームパッド*1の入力を判定する練習をしてみた話です。
JInputに決めた経緯
最初は下記のサイトなどを参考にして、LWJGL2か3を試してみたんですが、キーボードは入力できてもゲームパッドの入力が動かなかったんですね。
- LWJGL でキーボートやジョイスティックを使ってみる / 桃缶食べたい。
念のため、LWJGLとは"Lightweight Java Game Library"というJavaのゲーム開発用ライブラリーです。バージョン2と3で入力のAPIが全然違います。
色々と調べた結果、JInputというライブラリーを使えば良さそうでした。
しかし残念なことに、参考にしたページのコードも動きませんでした。
(参考にしたページが見つからない...)
仕方ないので、自力でなんとかやってみました。
参考サイト
実はあまり参考にしていませんが、いくつかはっておきます。
- Joystick in Java with JInput v2 | TheUzo007
環境
- Windows 7 professional 64bit
- Java8
- JInput 2.0.6
今回はJava8の新機能を使っているので、Java8必須です。
JInputはネイティブライブラリーが含まれていて、Windows(32bit/64bit)、Mac、Linuxに対応しています。
JInputをセットアップ
コンパイル用にJInputライブラリーを設定します。今回は何故かGradleです。
- GradleでJInputの依存関係を設定
dependencies { compile 'net.java.jinput:jinput:2.0.6' }
実行時には、Javaのライブラリーだけでなく、ネイティブライブラリーのパスが通っている必要があります。ここではWindows用のものを説明します。
ネイティブライブラリー(dllファイル)がある場所に、パス(PATH)を通します。
クラスパスではないので注意。
- JInputのWindows用ネイティブライブラリー
jinput-wintab.dll jinput-dx8.dll jinput-dx8_64.dll jinput-raw.dll jinput-raw_64.dll
ライブラリー名を見ると分かるように、内部ではDirectX 8を使っています。DirectX 8が入っていない環境では使えないはずです。
使い方
順を追って見ていきましょう。
Controllerのインスタンスを取得する
net.java.games.input.Controller
インターフェイスのインスタンスを得ることから始まります。
ゲームパッドとキーボードでそれぞれインスタンスを取得するようにしてみます。
- コード:
// import net.java.games.input.*; static Controller detectController(Controller.Type type) { Controller[] controllers = ControllerEnvironment.getDefaultEnvironment().getControllers(); for (Controller controller : controllers) { if (controller != null && controller.getType() == type) { return controller; } } return NullController.INSTANCE; } /** * コントローラーが無効な場合のスタブ。 */ final class NullController implements Controller { static final Controller INSTANCE = new NullController(); // 略 } public static void main(String[] args) { Controller gamepad = detectController(Controller.Type.GAMEPAD); Controller keyboard = detectController(Controller.Type.KEYBOARD); }
デフォルトのコントローラーとして登録されているリストから、指定したタイプのコントローラーを取り出しています。
NullController
というのは、いわゆるNullObject
パターンのNullオブジェクトです。
コードが無駄に長いので、ここでは省略します。最後にまとめて掲載します。
ゲームパッドの入力判定
今回は、XY座標とボタン3(下のボタン、プレステで言うと×ボタンの位置)の判定だけを行います。
まず最初に、コントローラーの入力が受け付けられたかどうかをチェックするために、controller.poll()
を呼び出します。
これがtrue
を返した場合のみ、以下の処理を行います。
XY座標は、Controller
のインスタンスcontroller
に対して、それぞれ
controller.getComponent(Identifier.Axis.X).getPollData()
controller.getComponent(Identifier.Axis.Y).getPollData()
を呼び出します。
ここで返される数値がちょっと分かりにくくて、下記のようになっています。
ゼロ(未入力): -0.000015 マイナス(左): -1.000000 プラス(右) : 1.000000
もしかしたら、アナログコントローラーの細かい感度まで取得できるのかも知れませんが、私の環境ではできませんでした。
ボタン3の判定は
// Button._2はボタン3(0オリジン) boolean button3Pushed = controller.getComponent(Identifier.Button._2).getPollData() > 0.0f
のようにします。番号は0オリジンなので少しややこしいです。
返される値は、押されていない時が0.0
、押された時が1.0
で、こちらは分かりやすくなっています。
キーボードの入力判定
キーボードは、十字キーとスペースキーに対応させましょう。
ゲームパッドと同じく、最初にcontroller.poll()
を呼び出します。
判定は、こちらは全てcontroller.getComponent(Identifier.Key.???).getPollData() > 0.0f
で良いです。???
の部分は、LEFT
,RIGHT
,UP
,DOWN
,SPACE
を入れます。
クラスにまとめる
以上を踏まえて、いくつかのクラスにまとめてみます。
ボタン3は、攻撃ボタン(attackButton
)としています。
解説はコードの後で。
- コード:ゲームパッドとキーボードの入力を定期的にチェック
// import java.util.Optional; // import java.util.concurrent.ForkJoinPool; // import java.util.function.Consumer; // import net.java.games.input.*; // import net.java.games.input.Component.Identifier; /** * コントローラー入力。 */ interface ControllerInput { Optional<ControllerInput.State> getState(); boolean available(); static Controller detectController(Controller.Type type) { Controller[] controllers = ControllerEnvironment.getDefaultEnvironment().getControllers(); for (Controller controller : controllers) { if (controller != null && controller.getType() == type) { return controller; } } return NullController.INSTANCE; } /** * コントローラー入力の状態。 */ static final class State { int x; int y; boolean attackButtonPushed; State(int x, int y, boolean attackButtonPushed) { this.x = x; this.y = y; this.attackButtonPushed = attackButtonPushed; } } } /** * コントローラーが無効な場合のスタブ。 */ final class NullController implements Controller { static final Controller INSTANCE = new NullController(); @Override public Controller[] getControllers() { return new Controller[0]; } @Override public Type getType() { return Controller.Type.UNKNOWN; } @Override public Component[] getComponents() { return new Component[0]; } @Override public Component getComponent(Identifier id) { return null; } @Override public Rumbler[] getRumblers() { return new Rumbler[0]; } @Override public boolean poll() { return false; } @Override public void setEventQueueSize(int size) { } @Override public EventQueue getEventQueue() { return null; } @Override public PortType getPortType() { return Controller.PortType.UNKNOWN; } @Override public int getPortNumber() { return -1; } @Override public String getName() { return "NullController"; } } /** * ゲームパッドコントローラー入力。 */ final class GamepadControllerInput implements ControllerInput { final Controller controller; volatile boolean button3Released; GamepadControllerInput() { this.controller = ControllerInput.detectController(Controller.Type.GAMEPAD); this.button3Released = true; } @Override public boolean available() { return controller != NullController.INSTANCE; } @Override public Optional<ControllerInput.State> getState() { if (controller.poll()) { float x0 = controller.getComponent(Identifier.Axis.X).getPollData(); float y0 = controller.getComponent(Identifier.Axis.Y).getPollData(); int x = x0 > 0.0f ? 1 : x0 < -0.1f ? -1 : 0; int y = y0 > 0.0f ? 1 : y0 < -0.1f ? -1 : 0; float b3 = controller.getComponent(Identifier.Button._2).getPollData(); System.out.println("b3=" + b3); // Button._2はボタン3(0オリジン) boolean button3Pushed = controller.getComponent(Identifier.Button._2).getPollData() > 0.0f; boolean attackButtonPushed = button3Released && button3Pushed; button3Released = !button3Pushed; if (x != 0 || y != 0 || attackButtonPushed) { return Optional.of(new ControllerInput.State(x, y, attackButtonPushed)); } } return Optional.empty(); } } /** * キーボードコントローラー入力。 */ final class KeyboardControllerInput implements ControllerInput { final Controller controller; volatile boolean spaceKeyReleased; KeyboardControllerInput() { this.controller = ControllerInput.detectController(Controller.Type.KEYBOARD); this.spaceKeyReleased = true; } @Override public boolean available() { return controller != NullController.INSTANCE; } @Override public Optional<ControllerInput.State> getState() { if (controller.poll()) { int x = controller.getComponent(Identifier.Key.LEFT).getPollData() > 0.0f ? -1 : controller.getComponent(Identifier.Key.RIGHT).getPollData() > 0.0f ? 1 : 0; int y = controller.getComponent(Identifier.Key.UP).getPollData() > 0.0f ? -1 : controller.getComponent(Identifier.Key.DOWN).getPollData() > 0.0f ? 1 : 0; // Spaceキー boolean spaceKeyPushed = controller.getComponent(Identifier.Key.SPACE).getPollData() > 0.0f; boolean attackButtonPushed = spaceKeyReleased && spaceKeyPushed; spaceKeyReleased = !spaceKeyPushed; if (x != 0 || y != 0 || attackButtonPushed) { return Optional.of(new ControllerInput.State(x, y, attackButtonPushed)); } } return Optional.empty(); } } final class App { static void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } } public static void main(String[] args) { ControllerInput gamepadInput = new GamepadControllerInput(); ControllerInput keyboardInput = new KeyboardControllerInput(); Consumer<ControllerInput.State> changeState = x -> { // TODO 状態を変える }; // ゲームパッド入力 if (gamepadInput.available()) { ForkJoinPool.commonPool().execute(() -> { while (true) { gamepadInput.getState().ifPresent(changeState); sleep(15L); } }); } // キーボード入力 if (keyboardInput.available()) { ForkJoinPool.commonPool().execute(() -> { while (true) { keyboardInput.getState().ifPresent(changeState); sleep(15L); } }); } } }
ControllerInput
というインターフェイスと、そのサブクラスとしてControllerInput.State
という状態クラスを設けています。
サブクラスのいずれの入力も、XとYの入力があったかどうか、攻撃ボタンが押されたかどうかを返せるようにしています。入力があったかどうかは、Optional
で返します。
XY座標は、-1
,0
,+1
に置き換えています。
ボタン3の判定で何やらややこしいことをしていますが、これはボタンを離したことを検知する部分です。あまり上手い実装ではないと自分でも思いますが、今後の課題ということで。
ControllerInput#getState
メソッドを呼び出せば、ControllerInput.State
で入力状態が得られるというわけです。
あとは、ゲームパッド入力とキーボード入力を監視するスレッドを立ち上げて、完成です。