人工知能に関する断創録

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

王様登場

勇者が1人のさびしい世界でしたがついに王様

f:id:aidiary:20100327200910g:plain

の登場です。ついでに王様のまわりをぶらぶらしてる護衛である兵士も登場させましょう。

f:id:aidiary:20100327200914g:plain

今回はこの2人を世界に組み込みます。勇者を表すCharaクラスを流用すれば簡単にキャラクターを増やすことが可能です。オブジェクト指向の威力が存分に味わえます。

rpg12.jar

王様と兵士の作成

前にオブジェクト化のところでCharaクラスを解説しました。Charaクラスのコンストラクタは

    public Chara(int x, int y, String filename, Map map);

のようになっています(ゲームループとキー操作の改良からMainPanelを渡さなくなってます)。(x,y)はキャラクターの位置(マス単位)、filenameはイメージファイル名、mapはキャラクターが存在するマップへの参照です。たとえば、勇者を作るには

    // 勇者
    private Chara hero;

    // 勇者を作成
    hero = new Chara(4, 4, "image/hero.gif", map);

としてました。これと同様に王様と兵士を作りたい場合は、

    // 王様
    private Chara king;
    // 兵士
    private Chara soldier;

    // 王様を作成
    king = new Chara(6, 6, "image/king.gif", map);
    // 兵士を作成
    soldier = new Chara(8, 9, "image/soldier.gif", map);

とします。すごく簡単ですね。MainPanelにこの処理が追加されてます。

キャラクターのマップへの追加

MainPanelで勇者、王様、兵士を作成した後に

    // マップにキャラクターを登録
    // キャラクターはマップに属す
    map.addChara(hero);
    map.addChara(king);
    map.addChara(soldier);

という処理が入っています。これは、今作成したキャラクターをマップに登録する処理です。すべてのキャラクターはいずれかのマップに属しているとします。今の場合、マップはラダトーム城の王様の間(map)しか作ってませんのでmapに全キャラクター(勇者、王様、兵士)を登録します。

勇者も登録してる点に注意してください。パンピー(注:一般ピープル)はマップ間移動はできませんが勇者はできます。マップ間移動する場合は今までいたマップからキャラクターを削除し、移動先のマップに登録し直すことになります。マップ間移動は後ほど取り上げます。

addChara()をも少し詳しく見てみます。MapクラスのaddChara()を見てください。

    // このマップにいるキャラクターたち
    private Vector charas = new Vector();

    /**
     * キャラクターをこのマップに追加する
     * @param chara キャラクター
     */
    public void addChara(Chara chara) {
        charas.add(chara);
    }

MapクラスにcharasというVectorオブジェクトを作って、そこにキャラクター(chara)をaddしてます。Vectorは容量が自動的に大きくなる配列みたいなもんです。add()でVectorにオブジェクトを追加できます。

f:id:aidiary:20100327200907g:plain

配列のように添え字がついていて各オブジェクトにアクセスできます。配列の場合は配列名[添え字]のようにアクセスしますが、Vectorの場合は get(添え字)を使います。勇者を取り出したいときは

  Chara hero = (Chara)charas.get(0);

です。Chara型にキャストする必要があるので注意してください。キャラクターを描画するときに具体例を示します。

(注)JDK1.5からGenericsが導入されてVectorのようにVectorに入れるオブジェクト型を指定できるようになってますがここでは使ってません。

キャラクターの描画

前回まではMainPanelにマップの描画とキャラクター(勇者)の描画処理を書いてました。

    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        ・・・

        // マップを描く
        map.draw(g, offsetX, offsetY);

        // 勇者を描く
        hero.draw(g, offsetX, offsetY);
    }

今回からはすべてのキャラクターはマップに属すことにしたのでマップを描くときにそのマップに属すキャラクターも一緒に描画するように変更しましょう。

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        ・・・

        // マップを描く
        // キャラクターはマップが描いてくれる
        map.draw(g, offsetX, offsetY);
    }

Mapクラスのdraw()です。

    public void draw(Graphics g, int offsetX, int offsetY) {

        // このマップにいるキャラクターを描画
        for (int n=0; n<charas.size(); n++) {
            Chara chara = (Chara)charas.get(n);
            chara.draw(g, offsetX, offsetY);
        }
    }

charasというVectorに登録されたキャラクターを1人ずつ取り出して順に描画しています。勇者もパンピーも同様に描画できます(以前のバージョンでは勇者とパンピーは分けていたので少し面倒でした)。

キャラクター同士の衝突判定

たくさんのキャラクターがマップ中をうろうろし始めるのでキャラクター同士の衝突判定を実装する必要が出てきます。さもないと互いに踏み合う殺伐とした世界になってしまいます(笑)Charaの移動ルーチンを見てみると、

    private boolean moveUp() {
        // 1マス先の座標
        int nextX = x;
        int nextY = y - 1;
        if (nextY < 0) nextY = 0;
        // その場所に障害物がなければ移動を開始
        if (!map.isHit(nextX, nextY)) {
            // 移動処理
        }
    }

のようにMapクラスのisHit()で障害物がないか調べてます。今までは壁や玉座などとぶつからないか調べていただけでしたが、ここに他のキャラクターとぶつからないか調べる処理を追加しましょう。

    public boolean isHit(int x, int y) {
        // (x,y)に壁か玉座があったらぶつかる
        if (map[y][x] == 1 || map[y][x] == 2) {
            return true;
        }

        // 他のキャラクターがいたらぶつかる
        for (int i = 0; i < charas.size(); i++) {
            Chara chara = (Chara) charas.get(i);
            if (chara.getX() == x && chara.getY() == y) {
                return true;
            }
        }

        // なければぶつからない
        return false;
    }

charasから1人ずつ取り出してきて、自分の座標 (x,y) と他のキャラクター座標 (chara.getX(),chara.getY()) が等しいか調べてます。等しい場合はぶつかっているのでtrueを返します。

兵士をぶらぶらさせる

最後に兵士をぶらぶらさせてみましょう。1箇所に長時間突っ立ってるのはかなり苦痛です。兵士を楽にしてあげましょう。MainPanelクラスのゲームループrun()を見てください。

    public void run() {
        while (true) {
            // キー入力をチェックする
            checkInput();

            // 勇者の移動処理
            heroMove();
            // キャラクターの移動処理
            charaMove();

            repaint();

            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

わかりやすくするために勇者の移動処理(キー入力に従って移動)と他のキャラクター(パンピー)の移動処理を分けています。charaMove() を詳しく見てみます。

    /**
     * 勇者以外のキャラクターの移動処理
     */
    private void charaMove() {
        if (soldier.isMoving()) {  // 移動中なら
            soldier.move();  // 移動を続ける
        } else if (rand.nextDouble() < 0.02) {
            // 移動してない場合は0.02の確率で再移動する
            // 方向はランダムに決める
            soldier.setDirection(rand.nextInt(4));
            soldier.setMoving(true);
        }
    }

移動の仕方は基本的に勇者と同じです。しかし、パンピーはプレイヤーがキーボードで操ってるわけではないのでどの方向に移動するかは自動で決める必要があります。ここではうろうろさせたいのでランダムに移動させます。ここでは兵士だけうろうろさせます(王様はやっぱまずいでしょう)。もし兵士が移動中ならそのまま移動を続けさせます。もし静止しているなら、 0.02の確率(2%)で方向をランダムに決めて移動を開始します。0.02を大きくすれば頻繁に移動するようになります。試してみてください。この方法はまだ汎用性がありませんね。イベントの導入でキャラクターごとに移動するかしないかを決められるように拡張していきます。