クォータービューを描画しよう

スマホゲーム向けにJavaとAndEngineでクォータービューを描画しよう思います。

クォータービューのタイルチップは

「ひし形だけでできているタイルチップ」

f:id:Takachan:20160116160606p:plain:h100

f:id:Takachan:20160116160634p:plain:h100

「足がついてるタイルチップ」


の2種類がありますが、今回ここで扱うのは後者の「足がついてるタイルチップ」です。

クォータービューとは

クォータービューが何かというのはGoogle検索の画像のページのページをみると分かりやすいのです。斜めからの見下ろし視点で奥行きがある表現方法です。

今回、クォータービューで地面を描画するためにはどうしたらいいかを考えて実装しようと思います。

考えかた(1)

まず、基本の考え方です。
以下のようにx方向とy方向に区切った四角形を考えます。

f:id:Takachan:20160113235800p:plain:h300

これを45度左に倒します。

f:id:Takachan:20160114232645p:plain:h300

この絵を想像しながら奥から手前に、一番手前まで来たら次の列へ順番に書いていきます。(順序を間違えると前後関係がおかしい変な絵面になるので注意してください)

描画順序は

(9,1) → (8,1) → (7,1) → ... (1,1) → (9,2) → (8,2) → (7,2)

です。

使用するリソース

今回は自分で作った、32*32のタイルチップを使います。
以下画像は左側がブランクで右側が表示ありの2パターンで64*32の画像を使用します。

f:id:Takachan:20160110180339p:plain

また、タイルを敷き詰める際に必要になる数値を参考までに記載します。

f:id:Takachan:20160114002209p:plain:h300

上面のひし形のサイズは下側の足の部分があるので高さの4分の1です。
(今回は権利関係を気にしてチップを自作しましたが自身が無い人はアセットストアで素材を買った方が幸せになれます)

考え方(2)

タイルの敷き詰め方ですが、使うライブラリの座標系はAndEngineは左上が座標(0,0)で右下にいくにつれて各々値が増加します。
従って、y軸は下方向が正で上方向を負で扱います。

まず、タイルの高さ「h」、タイルの横幅「w」として以下初期値が与えられた場合、

h = 32, w = 32

描画開始地点を(1,1)の左側の角の頂点を原点とします。

f:id:Takachan:20160116161435p:plain:h150

// 初期値
offset_x = 200
offsey_y = 200

と仮にします。

それから最初に(1,1)のタイルを設置したら次の(1,2)のタイルの設置座標は具体的には以下のようになります。

x y px py
0 0 0 0
1 0 16 -8
2 0 32 -16
3 0 48 -24
0 1 16 8
1 1 32 0
2 1 48 -8
3 1 64 -16

これを式に表すと

int px = offset_x + 1/2 w * (x + y);
int py = offset_y + 1/4 h * (-x + y);

となります。この式を使って順番に描画すればタイルチップがマップとして表示されるはずです。

早速実装してみます

(少しライブラリ専用のおまじないを唱えてます)

実行結果は以下の通りです。

f:id:Takachan:20160115000028p:plain:h250

無事にマップが表示されました。

コード

以下実際に使ったコードです。

package sample;

// Activityで取得できる変数を格納するクラス
import sample.BaseResource;

// コンストラクタでcreateSceneを呼んで簡単化
// SceneのセットはActivityで実施(ここには出てこない)
import sample.BaseScene;

import org.andengine.entity.sprite.Sprite;
import org.andengine.opengl.texture.atlas.bitmap.BitmapTextureAtlas;
import org.andengine.opengl.texture.atlas.bitmap.BitmapTextureAtlasTextureRegionFactory;
import org.andengine.opengl.texture.region.ITiledTextureRegion;

public class TileMapScene extends BaseScene
{
    //
    // 定数定義
    // - - - - - - - - - -

    // マップの大きさ
    private static final int MAP_SIZE_X = 9;
    private static final int MAP_SIZE_Y = 9;

    // マップチップを画面に表示するときの大きさ
    private static final int CHIP_WIDTH = 32;
    private static final int CHIP_HEIGHT = 32;

    // 初期位置
    private static final int OFFSET_X = 200;
    private static final int OFFSET_Y = 200;

    //
    // フィールド変数
    // - - - - - - - - - -

    // テクスチャ類
    private BitmapTextureAtlas texture;
    private ITiledTextureRegion textureRegion;

    // タイルチップのマップ
    private int[][] map;

    //
    // Public Methods
    // - - - - - - - - - -

    @Override
    public void createScene()
    {
        this.initArray();
        this.loadTexture();
        for (int y = 0; y < MAP_SIZE_Y; y++)
        {
            for (int x = MAP_SIZE_X - 1; x >= 0; x--)
            {
                // チップの番号に応じたスプライトの取得
                Sprite sp = this.getTileChip(this.map[y][x]);

                // タイルチップを配置する座標の計算
                sp.setX(OFFSET_X + CHIP_WIDTH / 2 * (x + y));
                sp.setY(OFFSET_Y + CHIP_HEIGHT / 4 * (-x + y));

                // 画面にチップを追加して描画されるように指定
                this.attachChild(sp);
            }
        }
    }

    // マップの初期化
    private void initArray()
    {
        // 描画が正しくされているか確認するために1つだけ0を入れておく
        this.map = new int[][] {
                { 1, 1, 1, 1, 1, 1, 1, 1, 1 },
                { 1, 0, 1, 1, 1, 1, 1, 1, 1 },
                { 1, 1, 1, 1, 1, 1, 1, 1, 1 },
                { 1, 1, 1, 1, 1, 1, 1, 1, 1 },
                { 1, 1, 1, 1, 1, 1, 1, 1, 1 },
                { 1, 1, 1, 1, 1, 1, 1, 1, 1 },
                { 1, 1, 1, 1, 1, 1, 1, 1, 1 },
                { 1, 1, 1, 1, 1, 1, 1, 1, 1 },
                { 1, 1, 1, 1, 1, 1, 1, 1, 1 },
                { 1, 1, 1, 1, 1, 1, 1, 1, 1 }
        };
    }

    // IDに応じたマップチップSpriteの取得
    private Sprite getTileChip(int id)
    {
        return new Sprite(0, 0,
                this.textureRegion.getTextureRegion(id),
                BaseResource.getVertexBufferObjectManager());
    }

    // 画面にSpriteを表示するための前準備
    private void loadTexture()
    {
        this.texture =
                new BitmapTextureAtlas(
                        BaseResource.getActivity().getTextureManager(), 64, 32
                );

        this.texture.load();

        // 1行2列で初期化
        this.textureRegion =
                BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(
                        this.texture,
                        BaseResource.getActivity(),
                        "gfx/tile_chip.png",
                        0, 0, 2, 1
                );
    }
}

最後に

アルゴリズムとか描画の難しさはクォータービュー > トップビューだと思います。
この後、どんなゲームをこの上に実装するかによりますが、トップビューの管理方法をクォータービューで描画するような変換する部分さえ作れば後はほとんど同じになりそうです。

参考

書籍

AndEngineでつくるAndroid 2Dゲーム (Smart Game Developer)

AndEngineでつくるAndroid 2Dゲーム (Smart Game Developer)

サイト
https://westplain.sakuraweb.com/translate/AndEngine/westplain.sakuraweb.com