argius note

プログラミング関連

Javaでゲームパッドの入力をJInputを使って実現する

突然、ゲームを作ってみたくなりました。

ミニゲーム的なものは過去に何度も作ったことはありましたが、アクションとかRPGのような本格的なゲームは作ったことがありません。


それで、欲張りなことに、UnityやAndroidやSwift、つまりゲームエンジンスマホアプリ開発に手を出そうとしています。なお、全然捗っていません。集中できるまとまった時間が確保できないと頭に入ってこないのです。

そこで、やはりというか、Javaでできる範囲なら、片手間でもなんとかなるんじゃないかと思いました。
手始めに、手元にある無線ゲームパッドの入力でキャラクターを動かすのに挑戦。
詳しくは後述しますが、JInputを使うことにしました。


今回は、JavaでJInputを使って、ゲームパッド*1の入力を判定する練習をしてみた話です。



JInputに決めた経緯

最初は下記のサイトなどを参考にして、LWJGL2か3を試してみたんですが、キーボードは入力できてもゲームパッドの入力が動かなかったんですね。


念のため、LWJGLとは"Lightweight Java Game Library"というJavaのゲーム開発用ライブラリーです。バージョン2と3で入力のAPIが全然違います。


色々と調べた結果、JInputというライブラリーを使えば良さそうでした。
しかし残念なことに、参考にしたページのコードも動きませんでした。

(参考にしたページが見つからない...)


仕方ないので、自力でなんとかやってみました。


参考サイト

実はあまり参考にしていませんが、いくつかはっておきます。



環境

  • Windows 7 professional 64bit
  • Java8
  • JInput 2.0.6

今回はJava8の新機能を使っているので、Java8必須です。
JInputはネイティブライブラリーが含まれていて、Windows(32bit/64bit)、MacLinuxに対応しています。


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で入力状態が得られるというわけです。


あとは、ゲームパッド入力とキーボード入力を監視するスレッドを立ち上げて、完成です。


ゲームに組み込む

これを応用して、自キャラを動かしてビームらしきものを発射するところまではできました。

f:id:argius:20160226163317g:plain

参考までにコードは載せておきますが、かなり拙いのであくまで参考程度にご覧ください。





まとめ

JInputはゲームを作らない方にはあまり用のないライブラリーかも知れません。

でも、もしかしたらゲームじゃなくても、ゲームパッドをリモコン代わりに使ってみたりするのには使えるかも?

*1:ここではプレステ風ゲームコントローラーを指します。