人工知能に関する断創録

人工知能、認知科学、心理学、ロボティクス、生物学などに興味を持っています。このブログでは人工知能のさまざまな分野について調査したことをまとめています。最近は、機械学習、Deep Learning、Kerasに関する記事が多いです。



画像をまとめる

今回はキャラクターやマップチップごとに別々のファイルになってた画像を1つのファイルにまとめてしまいます。

rpg13.jar

画像をまとめる

今までは勇者(hero.gif)、王様(king.gif)、兵士(soldier.gif)といったようにイメージファイルをばらばらに用意していました。今回はキャラクターをchara.gifという1つのファイルにまとめてしまいます。ゲーム開始前にこのファイルを1つロードしてしまえばよいのでキャラクターごとにロードする必要はなくなります。chara.gifは下のようなファイルです。

f:id:aidiary:20100327200906g:plain

番号や白線はわかりやすいように加えただけで元のファイルにはありません。上の場合、8×4で32人のキャラクターを登録できます。マップチップファイルも同様です。床(floor.gif)、壁(wall.gif)、玉座(throne.gif)もmapchip.gifにまとめてしまいます。 mapchip.gifは下のようなファイルです。

f:id:aidiary:20100327200911g:plain

こっちは8×8で64個のマップチップを登録できます。

画像ファイルのロード

画像ファイルのロードはCharaクラスのコンストラクタで行います。前は

    public Chara(int x, int y, String filename, Map map) 
        // イメージをロード
        loadImage(filename);
    }

というコンストラクタを使ってました。キャラクターごとにファイル名を指定して、そのキャラクターを作るときにイメージを読み込んでるわけです。これを

    // キャラクターのイメージ
    private static Image charaImage;

    public Chara(int x, int y, int charaNo, Map map) {
        // 初回の呼び出しのみイメージをロード
        if (charaImage == null) {
            loadImage();
        }

        this.charaNo = charaNo;
    }

に変更しました。コンストラクタへの引数としてファイル名ではなくキャラクター番号(charaNo)を渡しています。この番号は上の chara.gifの図の番号と同じです。charaImageがnullの場合のみloadImage()でchara.gifを読み込んでます。つまり、Charaオブジェクトを最初に作ったときだけイメージがロードされ、2回目以降はロードされません。charaImageはstaticなので1度読み込まれればプログラムが終了するまで保たれます。また、staticにしておけばいくらCharaオブジェクトを作ってもCharaクラスで1つだけイメージを持つことになるのでメモリの節約にもなります。charaImageは登場するすべてのキャラクターが含まれているのであとはcharaNoにあわせて描画するキャラクターを決めてやればいいわけです。マップチップも同じです。最初にマップを作ったときだけイメージがロードされます。staticで保存してあるのですべてのマップでイメージを共有できます。

    private static Image chipImage;

    public Map(String filename, MainPanel panel) {
        // マップをロード
        load(filename);
        
        // 初回の呼び出しのみイメージをロード
        if (chipImage == null) {
            loadImage();
        }
    }

キャラクター描画処理の変更

キャラクターの描画処理を見てみます。

    public void draw(Graphics g, int offsetX, int offsetY) {
        int cx = (charaNo % 8) * (CS * 2);
        int cy = (charaNo / 8) * (CS * 4);
        // countとdirectionの値に応じて表示する画像を切り替える
        g.drawImage(charaImage,
            px + offsetX, py + offsetY,
            px + offsetX + CS, py + offsetY + CS,
            cx + count * CS, cy + direction * CS,
            cx + CS + count * CS, cy + direction * CS + CS, null);
    }

cxとcyが新しい部分です。(cx,cy)はcharaNoに対応するキャラクターのchara.gif上での座標を表しています。たとえば、兵士の座標を計算してみます。兵士のcharaNoは上のchara.gifを見ると2です。マスのサイズ(CS)は32であるため

    cx = (2 % 8) * (32 * 2) = 2 * 64 = 128
    cy = (2 / 8) * (32 * 4) = 0 * 128 = 0

と計算できます。つまり、兵士の画像は(128,0)にあります。%は余りを計算する。int型の割り算は整数部分のみが返される点に注意してください。別の例として、まだキャラクターは登録されていませんがcharaNo=9の位置は

    cx = (9 % 8) * (32 * 2) = 1 * 64 = 64
    cy = (9 / 8) * (32 * 4) = 1 * 128 = 128

と計算できます。つまり、charaNo=9のキャラクターの画像は(64,128)にあります。この式のミソはcharaNo % 8、charaNo / 8の8にあります。この8はchara.gifの1行に8人並べていることを表してます。なのでchara.gifの1行の人数を変更するとうまくいかなくなります。こういう場合は定数にしといたほうがいいですね。(cx,cy)を求めたように%と/で座標を求めるテクニックはよく使います。タイル番号から(X,Y)を求める方法を参考にしてください。

マップ描画処理の変更

マップも似たようなものです。マップチップの番号は下のマップファイル(map.dat)の番号と同じです。

1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111000000001111
1111022222201111
1111020220201111
1111000000001111
1111000000001111
1111000000001111
1111111011111111
1111000000001111
1111111111111111
1111111111111111
1111111111111111
1111111111111111

マップを描画する処理は下のようになってます。

    for (int i = firstTileY; i < lastTileY; i++) {
        for (int j = firstTileX; j < lastTileX; j++) {
            int mapChipNo = map[i][j];
            // イメージ上の位置を求める
            // マップチップイメージは8x8を想定
            int cx = (mapChipNo % 8) * CS;
            int cy = (mapChipNo / 8) * CS;
            g.drawImage(chipImage, 
                tilesToPixels(j) + offsetX,
                tilesToPixels(i) + offsetY,
                tilesToPixels(j) + offsetX + CS,
                tilesToPixels(i) + offsetY + CS,
                cx, cy,
                cx + CS, cy + CS, panel);
        }
    }

マップ上の番号(mapChipNo)を読み取って対応する画像の位置(cx,cy)を求めて描画しています。これもmapchip.gifの1行が8チップと想定しています。8というマジックナンバーをプログラム中でそのまま使うのは好ましくないので定数で表しておいたほうがいいです。最後に画像のサイズを大きくしたのでCommonクラスのCSを16から32に変更しています。

    // チップセットのサイズ(単位:ピクセル)
    public static final int CS = 32;

タイル番号から(X,Y)を求める方法

これは補足です。

f:id:aidiary:20100327200916g:plain

上のようにタイル番号が0から19まで振ってあったとします。このとき、タイル番号から(X,Y)という座標に変換する方法を考えます。たとえば、タイル番号が6のときは(2,1)といった感じです。タイル番号をNとするとXとYは次のように求められます。

    X = N % 4
    Y = N / 4

4は1行のタイル数を表しています。