石橋を叩いて壊すページ

地図を描く

FF14のレベル上げもだいたい一区切りついたので、久しぶりの更新。
今日は地図を描いてみる。
なお、この記事の内容は、開発バージョンのBukkit(1.7.2-R0.1 Development Build #2923)を使用している。
また、地図の挙動についてはMinecraft Wikiの地図の頁と 地図アイテムフォーマットの頁が大変詳しいので、あわせてご覧いただきたい。

まず最初に、地図を作る方法は以下のとおり。

/**
 * プレイヤーがクリックしたときに呼ばれる
 * 
 * @param e
 */
@SuppressWarnings("deprecation")
@EventHandler
public void onPlayerInteractEvent(PlayerInteractEvent e) {

    // 紙を手に持って右クリックしたのか
    if ((e.getAction().equals(Action.RIGHT_CLICK_AIR) || e.getAction()
            .equals(Action.RIGHT_CLICK_BLOCK))
            && e.getPlayer().getItemInHand().getType()
                    .equals(Material.PAPER)) {

        //新しい地図データを作る
        MapView view=getServer().createMap(e.getPlayer().getWorld());

        //座標と縮尺を設定
        view.setCenterX(0);
        view.setCenterZ(0);
        view.setScale(MapView.Scale.CLOSEST);
        
        //地図データから地図IDを取り出し、アイテムとしての地図を作成しインベントリに格納
        e.getPlayer().getInventory().addItem(new ItemStack(Material.MAP,1,view.getId()));
        
        //インベントリデータの変更をクライアントに通知
        e.getPlayer().updateInventory();
    }
}

ServerクラスのcreateMap(…)メソッドで、地図データを新規作成し、
そこに地図の中心となるX座標・Z座標と縮尺を設定している。

地図の縮尺は5段階あり、"CLOSEST"は一番狭い範囲しか表示されない地図(バニラのマインクラフトで一番最初に作成できる地図)の縮尺である。
CLOSEST→CLOSE→NORMAL→FAR→FARTHESTの順に広くなっていく。


縮尺を変更してみた例。左から、CLOSEST・CLOSE・NORMAL・FAR・FARTHEST。

バニラのマインクラフトで作る地図は、地図の中心座標は必ず128の倍数となるが、
setCenterX(…)・setCenterZ(…)で指定する座標は自由な値を指定できるので、
バニラより自由度の高い地図を作ることができる。

ServerクラスのcreateMap(…)メソッドは、あくまでも地図のデータを作るだけで、アイテムとしての地図は作られない。
いうなれば、地図IDを払い出しただけの状態である。
このため、new ItemStack(…)の部分で、地図のデータに基づいたアイテムとしての地図を作成している。
具体的には、地図のダメージ値に地図IDを指定することで、指定した地図IDの地図を作成することができる。

マインクラフトの地図は、実のところ画像を表示しているに過ぎず、
地図以外の画像を表示することもできる。

上記したコードに、地図に四角と文字を描画するコードを追加すると、以下のようになる。

/**
 * プレイヤーがクリックしたときに呼ばれる
 * 
 * @param e
 */
@SuppressWarnings("deprecation")
@EventHandler
public void onPlayerInteractEvent(PlayerInteractEvent e) {

    // 紙を手に持って右クリックしたのか
    if ((e.getAction().equals(Action.RIGHT_CLICK_AIR) || e.getAction()
            .equals(Action.RIGHT_CLICK_BLOCK))
            && e.getPlayer().getItemInHand().getType()
                    .equals(Material.PAPER)) {

        //新しい地図データを作る
        MapView view=getServer().createMap(e.getPlayer().getWorld());

        //座標と縮尺を設定
        view.setCenterX(1);
        view.setCenterZ(0);
        view.setScale(MapView.Scale.CLOSEST);
        
        //レンダラーを設定
        view.addRenderer(new MyRenderer());
        
        //地図データから地図IDを取り出し、アイテムとしての地図を作成しインベントリに格納
        e.getPlayer().getInventory().addItem(new ItemStack(Material.MAP,1,view.getId()));
        
        //インベントリデータの変更をクライアントに通知
        e.getPlayer().updateInventory();
    }
}

//地図を描画するクラス
class MyRenderer extends MapRenderer
{
    //地図に描画する文字のフォント
    private MinecraftFont font=new MinecraftFont();
    
    //地図を描画する
    @Override
    public void render(MapView view, MapCanvas canvas, Player player) {
        
        //地図の左上に白い四角を描く
        for(int x=10;x!=20;x++)
        {
            for(int y=10;y!=20;y++)
            {
                //指定した座標にカラー番号56(白)のピクセルを打つ
                canvas.setPixel(x, y, (byte)56);
            }
        }
        
        //地図の左下に文字を書く
        canvas.drawText(10, 100, font, "Minecraft §56;Map §18;Test");
    }
}


上記のコードを実行するとこうなる。左上に四角が、左下に文字が描画されている。

地図への描画は、MapRendererクラスを拡張したクラスのrender(…)メソッドで行う。
見てのとおり、指定したピクセルに点を打ったり、文字を描画するメソッドがMapCanvasクラスに用意されているので、これを使う。

マインクラフトの地図は、フルカラーではなく、パレットカラー(インデックスカラー)である。
このためあまり精細な写真を地図に描画することはできない。また、色の指定はパレットのIDを直接指定して行う。
…というか、パレットの各色を色名で指定できるMapPaletteというクラスもあるにはあるのだが、
ほとんど全要素がDeprecated(非推奨)になっているので、使える部分がほとんどない。


地図で使用可能なすべての色。左から右に向かってIDがあがっていく。
左上のIDゼロ~3の範囲は透明なパレットで、地図の紙の模様が見えている。

文字を描画する際は、文字ごとに色を指定することができる。
コードをみればわかるとおり、§56;のように、セクション・パレットのID・セミコロンを続けて書いて指定する。
セクションを使うあたり、以前に書いたチャットの文字色指定とよく似ているが、
後ろにセミコロンが必要なこと、指定する数値は地図の十進のパレットのIDでチャットの十六進の文字色IDとは全く関係ないこと、
チャットでは使用可能だった太字や斜体といった装飾系コードが使用できないことから、基本的に違うものと考えたほうがよさそうだ。

ひとつ注意点として、描画できない文字を地図に描画すると、サーバ側は問題ないが、クライアント側がゲーム続行不可能になるので気をつけること。
あまり詳しく検証していないが、たぶんASCII文字程度なら問題なく描画できる。しかし全角系の文字を指定するととたんにクライアントがエラー画面になる。
しかも、地図を手放せばエラーは収まるものの、そのためにはクライアントを起動せねばならず、
そうは言ってもクライアントを起動するとやっぱりエラー画面が出る、という無限ループに陥ってしまう。
もしこうなってしまったら、Bukkitを再起動すれば、レンダラーが初期化されるので復旧する。
なお、文字描画時の色指定で、後ろにセミコロンを付け忘れると、§を描画できないためこの状態になるほか、
ありえないパレットID(128など)を指定した場合も同様なので、特に注意が必要である。

また、上記の「描画できる文字」について補足すると、
文字の描画には、drawTextメソッドの3つ目の引数で指定したMinecraftFontオブジェクトに格納された字形データを使っている。
このため、このクラスに登録されていない字形を使おうとすると、クライアントがエラーになるようだ。
幸いなことに、MinecraftFontの親クラスであるMapFontには、新たに文字を登録するsetChar(…)メソッドがついているので、
これを使って字形を登録することができる。

//MS Pゴシックの9ピクセルサイズの「あ」をbooleanで表現したもの。
//trueの部分が文字の黒いドット、falseの部分が文字のない透過ドット。
//もし意味がわからないなら、下のtrueとfalseの羅列を目を細めてぼんやりみれば、横に間延びした「あ」の文字が見えてくるはず。
boolean[] cha={
    false,false,false,true ,false,false,false,false,
    true ,true ,true ,true ,true ,true ,true ,false,
    false,false,true ,false,false,false,false,false,
    false,false,true ,true ,true ,true ,false,false,
    false,true ,true ,false,true ,false,true ,false,
    true ,false,true ,false,true ,false,false,true ,
    true ,false,true ,false,true ,false,false,true ,
    true ,false,false,true ,false,false,false,true ,
    false,true ,true ,false,false,true ,true ,false,
};

//字形を登録する
MapFont.CharacterSprite ch=new MapFont.CharacterSprite(8, 9, cha);
font.setChar('あ', ch);


「あ」を描画してみた例。コードとしては簡単だが、字形データをプラグインに入れて配布してしまうと
フォントの利用ライセンスにひっかかる可能性が高いので、敷居は高い。

地図に画像を描画するには、以下のようにする。(エラー処理は適当。)
craftbukkit.jarと同じパスにimage.pngというファイル名の画像を置くと想定している。

(冗長なのでrender(…)メソッドだけ抜粋)
//地図を描画する
@Override
public void render(MapView view, MapCanvas canvas, Player player) {

    BufferedImage image = null;
    try {
        //画像を読み込む
        image = ImageIO.read(new File("image.png"));
        
        //画像を地図に貼り付ける
        canvas.drawImage(0, 0, image);

        //メモリを開放
        image.flush();
    }
    catch(Exception e)
    {
        player.sendMessage(e.toString());
        return;
    }
}


Windows標準のカラーパレットを読み込んでみた例。かなり色が丸められている。

上記の例を見てもわかるとおり、MapCancasのdrawImageを使うと、細かい色はかなり丸められて
全く違う画像になってしまう。このため、画像の精度を高めたい場合は、画像を予めフリーソフトなどで
減色処理などしておき、さらにdrawImage(…)に頼らずにsetPixel(…)を使って色を配置するといいようだ。


「君たちの拠点は、すべて私がいただいた。」 - AYBABTU
setPixel(…)を使って画像を読み込んだ例。PHPで画像を減色してパレットの色コード値に変換したあとファイルに書き出し、
それをBukkitに読み込ませた。

その他、気づいたこと。

MapRendererクラスのrender(…)メソッドは、地図がプレイヤーのインベントリに入っているだけで、
プレイヤーが地図を手に持っていなくても、毎秒20回呼び出される。
このため、静止画を表示するだけなら、一度描画したらあとは何も描画しないほうが処理が軽くなる。
うまく使えばアニメーションを地図に表示させることもできそうだ。

地図には、地図を生成した時点ですでに、地形を表示するためのMapRendererクラスのサブクラスのオブジェクトが登録されている。
このオブジェクトは、MapViewクラスのgetRenderers()メソッドを使うと取得できる。
また、MapViewクラスのremoveRenderer(…)メソッドを使ってそのオブジェクトを削除すると、地図には地形が表示されなくなる。

一枚の地図に複数のレンダラーオブジェクトが登録されている場合、後から追加したレンダラーの描画内容が地図に前面に表示される。
また、前面のレンダラーがパレット0~3(透明)の色を使った場合、後ろのレンダラーの描画内容が見えるのではなく、
後ろのレンダラーの描画内容までも透明となり、結果として地図の紙の模様が透けて見えるようになる。

addRenderer(…)やremoveRenderer(…)で地図のレンダラーを追加・削除しても、Bukkitを再起動すると
レンダラーはすべて初期化されてバニラの状態になるため、普通に地形が表示され、プラグインからの描画はできなくなる。

ServerクラスのgetMap(…)メソッドや、MapViewクラスのgetId()メソッドは、
Deprecated(非推奨)ということになっているが、特に問題なく使える。
というか、これ使えなくなったら毎回createMap(…)で新しい地図を払い出さないといけなくなるし
getId()で地図IDの参照をせずにどうやって地図アイテムを作成したらいいのかいまいち疑問。

この記事を評価

この記事にコメント

  1. ...

【この記事にコメント】
お名前:
コメント:

Menu