人工知能に関する断創録

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

栗ボー登場

ここで敵キャラを作ります。その名も栗ボー(自作)です。はい!登場。

f:id:aidiary:20091122211725p:plain

プレイヤーが踏んだら倒せるようにします。ちなみに画像が2つあるのはアニメーションのためです。2つの画像を交互に表示させるとジャンプしてるように見えます。

mariolike08.jar

Kuriboクラス

栗ボーを実装しているのはKuriboクラスです。前回、作成したSpriteクラスを拡張して作りましょう。面倒くさいのでコードを全部載せます。前回のCoinクラスと比較してみてください。

public class Kuribo extends Sprite {
    // スピード
    private static final double SPEED = 1;
    // 速度
    protected double vx;
    protected double vy;
    // 踏まれたときの音
    private AudioClip sound;

    ・・・

    public void update() {
        // 重力で下向きに加速度がかかる
        vy += Map.GRAVITY;

        // x方向の当たり判定
        // 移動先座標を求める
        double newX = x + vx;
        // 移動先座標で衝突するタイルの位置を取得
        // x方向だけ考えるのでy座標は変化しないと仮定
        Point tile = map.getTileCollision(this, newX, y);
        if (tile == null) {
            // 衝突するタイルがなければ移動
            x = newX;
        } else {
            // 衝突するタイルがある場合
            if (vx > 0) {
                // 右へ移動中なので右のブロックと衝突
                // ブロックにめりこむ or 隙間がないように位置調整
                x = Map.tilesToPixels(tile.x) - width;
            } else if (vx < 0) {
                // 左へ移動中なので左のブロックと衝突
                // 位置調整
                x = Map.tilesToPixels(tile.x + 1);
            }
            // 移動方向を反転
            vx = -vx;
        }

        // y方向の当たり判定
        // 移動先座標を求める
        double newY = y + vy;
        // 移動先座標で衝突するタイルの位置を取得
        // y方向だけ考えるのでx座標は変化しないと仮定
        tile = map.getTileCollision(this, x, newY);
        if (tile == null) {
            // 衝突するタイルがなければ移動
            y = newY;
        } else {
            // 衝突するタイルがある場合
            if (vy > 0) {
                // 下へ移動中なので下のブロックと衝突(着地)
                // 位置調整
                y = Map.tilesToPixels(tile.y) - height;
                // 着地したのでy方向速度を0に
                vy = 0;
            } else if (vy < 0) {
                // 上へ移動中なので上のブロックと衝突(天井ごん!)
                // 位置調整
                y = Map.tilesToPixels(tile.y + 1);
                // 天井にぶつかったのでy方向速度を0に
                vy = 0;
            }
        }
    }

    ・・・
}

ここで注目してほしいのはupdate()です。前回のコインは動く必要がなかったのでupdateは空っぽでした。今回は動く必要があるのでupdate()に動くための処理を書きます。

じゃ、update()を見てみます。どっかで見たことありませんか?実はプレイヤーのupdate()とほとんど同じなのです。栗ボーも重力の影響を受ける必要があります。つまり、床がなかったら下に落ちるようにしないとだめです。この重力で下に落ちたり、壁との当たり判定したりする処理はプレイヤーと全く同じでいいのです。そのまま流用します。栗ボー独自の処理は壁に当たったときに移動方向を反転させる

    // 移動方向を反転
    vx = -vx;

の部分だけです。もし、ぱた○たみたいなのを作りたければここにジャンプする処理を書き加えれば簡単にできると思います。

栗ボーの配置

次に栗ボーをマップ上に配置しましょう。これはコインの場合(2005/6/27)と全く同じです。マップデータを見てください(等幅フォントでないとずれます)。

 20
 60
 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
 B                                                          B
 B                                                          B
 B                                                          B
 B                                                          B
 B                                                          B
 B                                                          B
 B       oo                                             k   B
 B     BBBBB                                     BBBBBBBBBBBB
 B                  k                    B k B              B
 B                  ooo                  BBBBBBBB           B
 B             k                 BBB                        B
 B                                                          B
 B                        BBB                               B
 B                  BBB                            B        B
 B                                                          B
 B    BBB     BBB            ooo          BoooooooooooooooooB
 B                        BBBBBBBBBB      BoooooooooooooooooB
 B       k                                B            k    B
 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB

kが栗ボーです。空中に配置されてるのもいますねー。コインの場合は宙に浮いたままですが、栗ボーはupdate()で重力をかけてるのですぐ下に落ちます。プレイヤーの上あたりに配置すると落ちてくるのが見られますよ。

このマップデータを読み込んで栗ボーを作ってるのもコインと同じくMapクラスのload()です。

    /**
     * マップをロードする
     * 
     * @param filename マップファイル
     */
    private void load(String filename) {
        try {
            ・・・
            // マップを作成
            map = new char[row][col];
            for (int i = 0; i < row; i++) {
                line = br.readLine();
                for (int j = 0; j < col; j++) {
                    map[i][j] = line.charAt(j);
                    switch (map[i][j]) {
                        case 'o':  // コイン
                            sprites.add(new Coin(tilesToPixels(j),
                                                 tilesToPixels(i),
                                                 "coin.gif", this));
                            break;
                        case 'k':  // 栗ボー
                            sprites.add(new Kuribo(tilesToPixels(j),
                                                   tilesToPixels(i),
                                                   "kuribo.gif", this));
                            break;
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

コインの場合と全く同じですね。マップのkというシンボルを読み込んだときにKuriboオブジェクトを生成してスプライトリストに登録しています。ここで注意してほしいのはコインも栗ボーも同じスプライトリストspritesに登録していることです。スプライトリストは

    // スプライトリスト
    private LinkedList sprites;

このように宣言されています。LinkedListはどんなオブジェクトでも登録することができます。CoinオブジェクトもKuriboオブジェクトもまとめて放り込むことができるわけです。

(注)Java 5.0からGenericsという機能が導入されLinkedListも LinkedListとなってます。でこのリストに登録するオブジェクトのクラスを指定します。上でやったようにいろんなクラスのオブジェクトをごちゃまぜにしてぶち込むのはよくないのかもしれません。その場合は、CoinごとKuriboごとにスプライトリストを作ればいいと思います。

栗ボーを踏んづける

最後にプレイヤーが栗ボーを踏めるようにしましょう。コインと同じくMainPanelクラスのrun()を見てください。

            // マップにいるスプライトを取得
            LinkedList sprites = map.getSprites();            
            Iterator iterator = sprites.iterator();
            while (iterator.hasNext()) {
                Sprite sprite = (Sprite)iterator.next();
                
                // スプライトの状態を更新する
                sprite.update();

                // プレイヤーと接触してたら
                if (player.isCollision(sprite)) {
                    if (sprite instanceof Coin) {  // コイン
                        Coin coin = (Coin)sprite;
                        // コインは消える
                        sprites.remove(coin);
                        // ちゃり〜ん
                        coin.play();
                        // spritesから削除したので
                        // breakしないとiteratorがおかしくなる
                        break;
                    } else if (sprite instanceof Kuribo) {  // 栗ボー
                        Kuribo kuribo = (Kuribo)sprite;
                        // 上から踏まれてたら
                        if ((int)player.getY() < (int)kuribo.getY()) {
                            // 栗ボーは消える
                            sprites.remove(kuribo);
                            // サウンド
                            kuribo.play();
                            // 踏むとプレイヤーは再ジャンプ
                            player.setForceJump(true);
                            player.jump();
                            break;
                        } else {
                            // ゲームオーバー
                            gameOver();
                        }
                    }
                }
            }

マップからスプライトリストを取得し、Iteratorを使って順にスプライトを処理してます。スプライトリストにはCoinオブジェクトと Kuriboオブジェクトがごちゃまぜに入っているのでCoinとKuriboのどっちかをinstanceof演算子で調べて処理を分けてます。

もし、Kuriboの場合はプレイヤーが上から当たったかどうか調べます。上から当たってる場合のみ栗ボーを踏んづけてやっつけることができるからです。これは単にプレイヤーの座標が栗ボーの座標より小さいか調べればいいです。Javaの座標系では上に行くほど小さいので小さい方が上にいます。もちろん横からとか下から当たってたらゲームオーバー。

最後にに栗ボーを踏むと再ジャンプできるようにしましょう。敵をふんづけるとその勢いで自動的にジャンプします。この処理を入れたいわけです。これは、Playerクラスのjump()を少し改造するだけですみます。前回までのjump()は

    public void jump() {
        if (onGround) {
            // 上向きに速度を加える
            vy = -JUMP_SPEED;
            onGround = false;
        }
    }

のように地上にいるときだけジャンプできるようにしていました。今回はこの条件に再ジャンプ可能かの条件を付け加えます。

    public void jump() {
        // 地上にいるか再ジャンプ可能なら
        if (onGround || forceJump) {
            // 上向きに速度を加える
            vy = -JUMP_SPEED;
            onGround = false;
            forceJump = false;
        }
    }

地上にいなくてもforceJumpがtrueならジャンプできるわけです。今回は、栗ボーを踏んだときに

    player.setForceJump(true);

を呼び出してforceJumpをtrueにしているので自動的にジャンプします。