人工知能に関する断創録

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

ゲームループとキー操作の改良

今まではキーイベントハンドラ keyPressed() に直接移動するコードを書いてました。こうしてしまうとキーを押し続けた場合、「ととととと」と勇者が速く移動してしまいます。

そこで、今回はキー操作を改良して「とっ、とっ、とっ」と勇者がゆっくり移動できるようにしてみます。キークラスオブジェクトを作ってキーの状態を保存しておき、ゲームループでそのキーを読み取って移動するという方法で実現できます。

この手法はゲームでキー操作を受け付けるときよく使われるようです。C言語でゲームを作る場合は、Windows APIにGetAsyncKeyState()やGetKeyboardState()という関数が用意されているのですが、Javaにはないため自作する必要があります。

rpg09.jar

アクションキークラス

まずキーの押された状態を記録するアクションキークラスを使います。これは横スクロールアクションで作ったActionKey(2005/7/3)と全く同じです。

ここでは使い方だけ説明します。ActionKeyの実装の詳細は上のページを見てください。ActionKeyには、キーが押された、離されたなどの状態が記録されます。このキーの状態に応じて操作を実行するわけです。まず、使いたいキーごとにActionKeyオブジェクトを作ります。キーオブジェクトを作っているのは本体の

    // アクションキー
    private ActionKey leftKey;
    private ActionKey rightKey;
    private ActionKey upKey;
    private ActionKey downKey;

    public MainPanel() {
        ・・・
        // アクションキーを作成
        leftKey = new ActionKey();
        rightKey = new ActionKey();
        upKey = new ActionKey();
        downKey = new ActionKey();
        ・・・
    }

の部分です。上下左右のそれぞれのキーに対応するActionKeyオブジェクトを作っています。王様に話しかけるときに別のキーが必要になりますが、それは後で追加しましょう。

キーイベントハンドラの変更

次にキーイベントハンドラを変更します。前回までは

    public void keyPressed(KeyEvent e) {
        // 押されたキーを調べる
        int key = e.getKeyCode();

        switch (key) {
            case KeyEvent.VK_LEFT :
                // 左キーだったら勇者を1歩左へ
                move(LEFT);
                break;
            case KeyEvent.VK_RIGHT :
                // 右キーだったら勇者を1歩右へ
                move(RIGHT);
                break;
            case KeyEvent.VK_UP :
                // 上キーだったら勇者を1歩上へ
                move(UP);
                break;
            case KeyEvent.VK_DOWN :
                // 下キーだったら勇者を1歩下へ
                move(DOWN);
                break;
        }

        // 勇者の位置を動かしたので再描画
        repaint();
    }

    public void keyReleased(KeyEvent e) {
    }

    public void keyTyped(KeyEvent e) {
    }

こうなっていました。キーが押されたとき keyPressed() に勇者の位置をmove()で直接移動させています。キーが離されたとき keyReleased()、キーがタイプされたとき keyTyped() には特に何もしていません。これを下のように変えます。

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

        if (keyCode == KeyEvent.VK_LEFT) {
            leftKey.press();
        }
        if (keyCode == KeyEvent.VK_RIGHT) {
            rightKey.press();
        }
        if (keyCode == KeyEvent.VK_UP) {
            upKey.press();
        }
        if (keyCode == KeyEvent.VK_DOWN) {
            downKey.press();
        }
    }

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

        if (keyCode == KeyEvent.VK_LEFT) {
            leftKey.release();
        }
        if (keyCode == KeyEvent.VK_RIGHT) {
            rightKey.release();
        }
        if (keyCode == KeyEvent.VK_UP) {
            upKey.release();
        }
        if (keyCode == KeyEvent.VK_DOWN) {
            downKey.release();
        }
    }

    public void keyTyped(KeyEvent e) {
    }

各キーが押されたとき、そのキーに対応するActionKeyオブジェクトの状態をpress()メソッドで変更しています。離されたときは release()メソッドを使います。あとキーイベントハンドラ内では勇者を移動していないことに注意してください。ActionKeyオブジェクトの状態を変えているだけです。実際に ActionKeyオブジェクトの状態を見て勇者を移動させる処理は、次に説明するゲームループの中に書きます。

ゲームループ起動

ゲームを作るときの定石としてゲームループ(メインループ)があります。ゲームループは次のような構造をしているのが一般的です。

    全体の初期化;
    // 無限ループ
    while (true) {
        キーボード、マウスなどの入力情報を取得;
        ゲーム内オブジェクトの状態を更新;
        画面に描画;
        ループ休止;
    }

「ゲーム内オブジェクトの状態を更新」はボールの座標を変えたり、勇者を上に移動させるなどの処理のことです。この形に合わしてゲームループをスレッドで実装してみます。ゲームループの起動はMainPanelのコンストラクタでやってます。

    public MainPanel() {
        ・・・       
        // ゲームループ開始
        gameLoop = new Thread(this);
        gameLoop.start();
    }

start()でスレッドを起動すると下のrun()が実行されます。

    public void run() {
        while (true) {
            // Keyオブジェクトの状態を見て移動する
            if (leftKey.isPressed()) {
                move(LEFT);
            } else if (rightKey.isPressed()) {
                move(RIGHT);
            } else if (upKey.isPressed()) {
                move(UP);
            } else if (downKey.isPressed()) {
                move(DOWN);
            }
            
            // 再描画
            repaint();
            
            // 休止
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

ActionKeyオブジェクトのisPressed()を使ってキーボードの入力を調べています(=キーボード、マウスなどの入力情報を取得)。そして押されたボタンに応じて勇者を移動させています(=ゲーム内オブジェクトの状態を更新)。あとは再描画して休止すると続きゲームループの構造をとっています。すっきりしてわかりやすくなりました。

キーボード入力を調べるときif・elseを使っている点に注意してください。if・elseを使っているのでmove()は上下左右のどれか1つだけしか呼ばれません。つまり、上+下のような斜めへの移動はできません。

実際にプログラムを実行してみてください。キーボードを押しっぱなしにしても前回までのように「ととととと」と移動しないで「とっ、とっ、とっ」とゆっくり移動していることがわかると思います。200ミリ秒おきに移動が行われるからです。ただこの方法は問題があります。キーボードを一回だけ「ぽん」と押してみてください。ときどき勇者が動かないことがあります。これは、ゲームループで 200ミリ秒おきにキーボードの入力情報を調べているためです。200ミリ秒の休止中に押されたキーは無視されてしまうのです。また1回だけキーを押したのに2歩以上進んでしまうこともあります。これは、キーを1回押している間にゲームループが2回回ってしまうためです。そもそもゲームループで200ミリ秒も休止するのは長すぎます。だからと言ってこれを20秒くらいにしてしまうと・・・別の問題が起きます。この問題点は後でスクロール処理(2005/10/22)を実装すると少しはましになります。

追記

今までCharaにMainPanelを渡していましたが今回からは渡さないように変更しています。今までAnimationThreadでカウントを切り替えたあと再描画するためにMainPanelへの参照が必要でしたが、今回からはゲームループで定期的に再描画されるため AnimationThread内で再描画を指示する必要はなくなります。