人工知能に関する断創録

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

コインの実装

コインを実装してプレイヤーが取れるようにします。

mariolike07.jar

スプライト

コインクラスを実装する前にスプライトを作ります。スプライトの定義はよく知らないのですが画面上に出てくるキャラクターとかそんな意味で使っています。さっぱりしておいしい炭酸飲料ではないので注意してください。

今回はコインを導入するわけですが、コインとプレイヤーは共通の機能を持っています。たとえば、位置、大きさ、状態の更新、描画、アニメーションなどは共通の機能です。これらを抜き出してスプライトクラスにまとめています。スプライトクラスの骨格を抜き出すと下のようになります。

public abstract class Sprite {
    protected double x;   // 位置
    protected double y;
    protected int width;  // 幅
    protected int height; // 高さ
    protected Image image;// スプライト画像
    protected int count;  // アニメーション用カウンタ
    protected Map map;    // マップへの参照

    public Sprite(double x, double y, String fileName, Map map) {}

    /**
     * スプライトの状態を更新する
     */
    public abstract void update();

    /**
     * スプライトを描画
     */
    public void draw(Graphics g, int offsetX, int offsetY) {}

    /**
     * 他のスプライトと接触しているか
     */
    public boolean isCollision(Sprite sprite) {}

    public double getX() {}
    public double getY() {}
    public int getWidth() {}
    public int getHeight() {}

    /**
     * イメージをロードする
     */
    private void loadImage(String filename) {}

    // アニメーション用スレッド
    private class AnimationThread extends Thread {}
}

update()にabstractがついている点に注意してください。update()はこのクラスの拡張クラス(PlayerとCoin)が独自に実装することになります。update()がabstractなためこのクラスは抽象(abstract)クラスと呼ばれます。抽象クラスから直接オブジェクトを作ることはできません。つまり、

    Sprite player = new Sprite();

とやるとエラーになります。PlayerクラスはSpriteクラスから拡張します。

    public class Player extends Sprite

Playerクラスではupdate()もきちんと実装しているのでオブジェクトを作ることができるわけです。また位置、大きさ、描画機能などは Spriteクラスで実装されているのでPlayerクラスでは省略できます。各メソッドの実装は前回までのPlayerクラスと同じです。

コインの実装

次にCoinクラスを見てみます。

public class Coin extends Sprite {
    // コインをとったときの音
    private AudioClip sound;

    public Coin(double x, double y, String fileName, Map map) {
        super(x, y, fileName, map);
        
        // サウンドをロード
        sound = Applet.newAudioClip(
                    getClass().getResource("se/coin03.wav"));
    }

    public void update() {
    }
    
    /**
     * サウンドを再生
     */
    public void play() {
        sound.play();
    }
}

これで全部です。かなり短いですね。コインの位置とか描画機能がないじゃないかと思った方もいると思いますが、これらの機能はSpriteクラスですでに実装しているからCoinではいらないのです。Spriteクラスを拡張することででSpriteクラスの機能を使うことができます。オブジェクト指向って便利ですねー。

またSpriteでabstract指定していたupdate()をちゃんと実装している点に注意してください。中身がなくても何もしないメソッドとして実装しています。動くコインを作りたい場合はupdate()の中で移動する処理を付け足してください。

Coinにはサウンド機能を付け足しています。play()を呼び出すとチャリーンという気持ちのいい音が鳴ります。あとでコインを取ったときに鳴るようにします。

コインの配置

今度は今作ったコインを実際にマップに配置してみます。配置の方法はすごく悩んだんですが

  • コインの初期位置はマップファイルに書き込む
  • Mapにスプライトのリストを用意する
  • マップをロードしたときにCoinオブジェクトを作成してスプライトリストに登録する

としました。たぶんもっといい方法ありそうな気がしますが。まずはマップデータを見てみます(等幅フォントでないとずれます)。

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

前回とほとんど同じですが、oを見てください。これがコインです。マップを読み込むところを見てください。

    /**
     * マップをロードする
     * 
     * @param filename マップファイル
     */
    private void load(String filename) {
        try {
            // ファイルを開く
            BufferedReader br = new BufferedReader(
                new InputStreamReader(
                    getClass().getResourceAsStream("map/" + filename)));

            // 行数を読み込む
            String line = br.readLine();
            row = Integer.parseInt(line);
            // 列数を読み込む
            line = br.readLine();
            col = Integer.parseInt(line);
            // マップを作成
            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;
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

oが見つかったらコインオブジェクトを作ってspritesにaddしています。spritesは

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

LinkedListってのを使っています。オブジェクトの入れ物だと思ってください。add(オブジェクト)でオブジェクトをリストに入れることができます。

マップ上のB(ブロック)はオブジェクトじゃないの?と思った方がいるかもしれません。Bは大量に使うためオブジェクトにしていません。ここら辺は人によって作り方が違うと思います。

コインの描画

LinkedList spritesにコインを登録できました。次はコインを画面に表示しないといけません。MainPanelのpaintComponent()で描いています。

    /**
     * 描画処理
     * 
     * @param 描画オブジェクト
     */
    public void paintComponent(Graphics g) {
        ・・・

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

        // プレイヤーを描画
        player.draw(g, offsetX, offsetY);
        
        // スプライトを描画
        LinkedList sprites = map.getSprites();            
        Iterator iterator = sprites.iterator();
        while (iterator.hasNext()) {
            Sprite sprite = (Sprite)iterator.next();
            sprite.draw(g, offsetX, offsetY);
        }
    }

スプライトはマップに登録されているのでmap.getSprites()でLinkedListを取り出します。そして、リストに登録されているスプライトをsprite.draw()で描画しています。iteratorというのはリストの要素を順番に取り出すための方法です。大体意味わかりますよね。スプライトがまだある(iterator.hasNext()がtrue)ならそれを取り出して(iterator.next())描画しています。

わざわざmapからスプライトのリストを取り出すくらいならMapのdraw()で描けばいいじゃないかと思った方は鋭い・・・Mapのdraw()でスプライトを描いてもいいと思います。

コインを取る

長かったですがこれで最後。しかも重要なところです。コインを描画してもプレイヤーが取れるようにしなければいけません。じゃないとかわいそすぎる(笑)

プレイヤーとコインのあたり判定はゲームループ、つまり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;
             }
         }
     }

スプライトリストをマップから取得します。そして各スプライトの状態を更新したあとにプレイヤーと接触しているか調べます。プレイヤーと接触しているかはisCollision()で調べられます。isCollision()はSpriteクラスのメソッドです。

    /**
     * 他のスプライトと接触しているか
     * @param sprite スプライト
     */
    public boolean isCollision(Sprite sprite) {
        Rectangle playerRect = new Rectangle((int)x, (int)y,
                                             width, height);
        Rectangle spriteRect = new Rectangle((int)sprite.getX(),
                                             (int)sprite.getY(),
                                             sprite.getWidth(),
                                             sprite.getHeight());
        // 自分の矩形と相手の矩形が重なっているか調べる
        if (playerRect.intersects(spriteRect)) {
            return true;
        }
        
        return false;
    }

単純に矩形が重なっているか調べています。次にもし接触していてそのスプライトがコインであった場合はコインを消して(リストから削除する)ちゃりーんという音を再生します。コインをいくつとったか保存したい場合はここにその処理を入れます。

以上で全部です。

えっ、もっと大量のコインをばらまきたい?どうぞどうぞ。マップをoで埋め尽くしてください。いくらとっても使い道はないですがね(笑)あとコイン1つに対してオブジェクトを1つ作ってしまっているので作りすぎると動作が遅くなるのでほどほどに願います。たぶんもっといい実装法があると思います。