人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

キー入力改良

地味ですがキー入力の改良です。前回まではジャンプキー(↑キー)を押し続けると、押している間は何回もジャンプしてしまいます。一般的には、ジャンプキーを1回押したら1回だけジャンプする方が自然です。一方、左右への移動はジャンプと違って押し続けている間、移動を続けるほうが自然です。今回はこの2つのキー操作を使い分けられるようにします。アプリケーションを実行してみてください。ジャンプキー(↑キー)を押し続けても1回だけしかジャンプしません。もう1回ジャンプするにはキーを離してからもう1度押す必要があります。

mariolike10.jar

使い方

詳細を説明する前に使い方を見ておきます。まず使いたいキー操作ごとにActionKeyオブジェクトを用意します。

    // アクションキー
    private ActionKey goLeftKey;
    private ActionKey goRightKey;
    private ActionKey jumpKey;

左へ移動するキー(goLeftKey)、右へ移動するキー(goRightKey)、ジャンプするキー(jumpKey)を表しています。 goLeftKeyとgoRightKeyは押し続けている間移動するように、jumpKeyはキーを押し続けても1回だけジャンプするようにします。この2つのモードは、オブジェクトを作成するときに次のように指定することで切り替えられます。

    // アクションキーを作成
    goLeftKey = new ActionKey();
    goRightKey = new ActionKey();
    // ジャンプだけはキーを押し続けても1回だけしかジャンプしないようにする
    jumpKey = new ActionKey(ActionKey.DETECT_INITIAL_PRESS_ONLY);

goLeftKeyとgoRightKeyは、引数なしのコンストラクタを使ってオブジェクトを作っています。こうすると押し続けている間アクションを起こすようになります。一方、jumpKeyは、ActionKey.DETECT_INITIAL_PRESS_ONLYという定数を指定しています。こうするとキーを押し続けても1回だけしかアクションを起こさないようになります。次にそれぞれのアクションキーを実際のキーボード操作と結び付けます。

    /**
     * キーが押されたらキーの状態を「押された」に変える
     * 
     * @param e キーイベント
     */
    public void keyPressed(KeyEvent e) {
        int key = e.getKeyCode();

        if (key == KeyEvent.VK_LEFT) {
            goLeftKey.press();
        }
        if (key == KeyEvent.VK_RIGHT) {
            goRightKey.press();
        }
        if (key == KeyEvent.VK_UP) {
            jumpKey.press();
        }
    }
    
    /**
     * キーが離されたらキーの状態を「離された」に変える
     * 
     * @param e キーイベント
     */
    public void keyReleased(KeyEvent e) {
        int key = e.getKeyCode();

        if (key == KeyEvent.VK_LEFT) {
            goLeftKey.release();
        }
        if (key == KeyEvent.VK_RIGHT) {
            goRightKey.release();
        }
        if (key == KeyEvent.VK_UP) {
            jumpKey.release();
        }
    }

ActionKeyは、press()とrelease()というメソッドを持っています。これらはキーの状態を変えるメソッドです。press()を呼び出すと押されたという状態に変わり、release()を呼び出すと離されたという状態に変わります。最後にActionKeyを実際に検出する場面を見てください。MainPanelのゲームループ内です。

    if (goLeftKey.isPressed()) {
        // 左キーが押されていれば左向きに加速
        player.accelerateLeft();
    } else if (goRightKey.isPressed()) {
        // 右キーが押されていれば右向きに加速
        player.accelerateRight();
    } else {
        // 何も押されてないときは停止
        player.stop();
    }
    
    if (jumpKey.isPressed()) {
        // ジャンプする
        player.jump();
    }

isPressed()を使って押されているか調べています。もし押されていたらそれぞれのアクションを実行しています。このisPressed ()の実装が今回のミソです。goLeftKeyとgoRightKeyはキーを押し続けている間ずっとisPressed()はtrueを返します。一方、jumpKeyはキーを押し続けていても最初の1回だけしかisPressed()はtrueを返しません。後はfalseを返し続けます。キーを1 回離せばまたtrueを返すようになります。

ActionKeyクラス

次にActionKeyクラスの詳細を見てみます。そんなに大きなクラスではないので全部載せます。

public class ActionKey {
    // キーのモード
    // キーが押されている間はisPressed()はtrueを返す
    public static final int NORMAL = 0;
    // キーがはじめに押されたときだけisPressed()はtrueを返す
    // キーが押され続けても2回目以降はfalseを返す
    // このモードを使うとジャンプボタンを押し続けてもジャンプを繰り返さない
    public static final int DETECT_INITIAL_PRESS_ONLY = 1;
    
    // キーの状態
    // キーが離された
    private static final int STATE_RELEASED = 0;
    // キーが押されている
    private static final int STATE_PRESSED = 1;
    // キーが離されるのを待っている
    private static final int STATE_WAITING_FOR_RELEASE = 2;
    
    // キーのモード
    private int mode;
    // キーが押された回数
    private int amount;
    // キーの状態
    private int state;
    
    /**
     * 普通のコンストラクタではノーマルモード
     */
    public ActionKey() {
        this(NORMAL);
    }
    
    /**
     * モードを指定できるコンストラクタ
     * @param mode キーのモード
     */
    public ActionKey(int mode) {
        this.mode = mode;
        reset();
    }
    
    /**
     * キーの状態をリセット
     */
    public void reset() {
        state = STATE_RELEASED;
        amount = 0;
    }
    
    /**
     * キーが押されたとき呼び出す
     */
    public void press() {
        // STATE_WAITING_FOR_RELEASEのときは押されたことにならない
        if (state != STATE_WAITING_FOR_RELEASE) {
            amount++;
            state = STATE_PRESSED;
        }
    }
    
    /**
     * キーが離されたとき呼び出す
     */
    public void release() {
        state = STATE_RELEASED;
    }
    
    /**
     * キーが押されたか
     * @return 押されたらtrueを返す
     */
    public boolean isPressed() {
        if (amount != 0) {
            if (state == STATE_RELEASED) {
                amount = 0;
            } else if (mode == DETECT_INITIAL_PRESS_ONLY) {
                // 最初の1回だけtrueを返して押されたことにする
                // 次回からはSTATE_WAITING_FOR_RELEASEになるため
                // キーを押し続けても押されたことにならない
                state = STATE_WAITING_FOR_RELEASE;
                amount = 0;
            }
            
            return true;
        }
        
        return false;
    }
}

まあコメントに書いてあるとおりです。ActionKeyはNORMALとDETECT_INITIAL_PRESS_ONLYの2つのモードがあります。NORMALモードはキーを押し続けている間ずっとアクションを起こします。DETECT_INITIAL_PRESS_ONLYモードはキーを押し続けても1回だけしかアクションを起こしません。

この違いを実現するためにActionKeyは3つの状態をとります。STATE_RELEASED、STATE_PRESSED、 STATE_WAITING_FOR_RELEASEです。キーが押されたとき(press()が呼ばれる)、STATE_PRESSED状態になります。キーが押されたという状態です。一方、キーが離されたとき(release()が呼ばれる)、STATE_RELEASED状態になります。キーが離されたという状態です。

isPressed()はキーが押されているか調べるメソッドです。これはキーのモードによって動作が変わります。NORMALモードのときは、キーが押されている状態(STATE_PRESSED)のときずっとtrueを返します。一方、DETECT_INITIAL_PRESS_ONLYモードのときは、キーの状態をSTATE_WAITING_FOR_RELEASEに変えてからtrue を返します。キーの状態がSTATE_WAITING_FOR_RELEASEに変わってしまうので、次にisPressed()が呼ばれてもfalse を返すようになります。こうすれば、キーを押し続けても初回のisPressed()呼び出しのみtrueを返し、あとはfalseを返すようになるわけです。

今回実装する方法は、アクションに限らずさまざまな場面に応用できます。たとえば、RPGで会話ウィンドウの▼マーク(会話の続きを促すときに出てくるマーク)でキーを押し続けてどんどん会話がスクロールしてしまっては困ります。キーを押し続けても1回だけ押したことになった方が便利です。