ホーム < ゲームつくろー! < IKD備忘録

設計な話
レイアウト(HUD)を考えてみる


 ゲームの画面にはほぼいわゆる「HUD」が出ています。HPゲージとかスコアとか、メニューであればリストやボタンやラベルなどあらゆる所にHUDがあります。ゲーム画面に出る本意気のHUDは動きも付いて凝っています。それがそのゲームの味や雰囲気を作り出したりもするわけです。

 そういう画面内のHUDやその動作を含めた物を良く「レイアウト」と呼びます。通常レイアウト(Layout)とは構図の事ですが、ゲーム制作の場合はその動きや変化も含める事があります。カッコイイHUDを作りたいなぁとゲーム制作者なら誰しもが思います。では、それをどうやって作るのか?試行錯誤の始まりです。



@ とりあえず置いてみますか

 HUDはスクリーン空間を基準に世界に置かれます。スクリーンが世界の大元です。3DなアーキテクチャのDirectXではHUDは板ポリで表現されます。これをスクリーン世界に置くわけです。板ポリを世界に置くには、板ポリの頂点座標にスクリーン座標を指定します。例えば、

Vtx vtx[3] = {
    { 50.0f, 60.0f},
   { 50.0f, 30.0f},
   {100.0f, 60.0f},
};

などと定義します。「ん?HUD(板ポリ)って四角形だよねぇ。」と思ったあなた!そうとは限りません!多角形だったり、四角形でもポリゴンが分割されたりと、HUDの頂点数は不定なんです。で、位置を変更する時にはこの頂点座標を書き換えて描画に回すわけですが…、例えば上の板ポリをX軸方向に40だけ移動させたい時に、

Vtx vtx[3] = {
   { 90.0f, 60.0f},
   { 90.0f, 30.0f},
   {140.0f, 60.0f},
};

と全部の座標を直接書き換えて保持しておくのもありですが、

float x, y;
x = 40.0f;
y = 0.0f;

と移動量のみを別途保持する手もあります。多くの場合HUDの移動は全部の頂点が等しく動くため、こういう差分値があった方が使う側が扱いやすいのです。こうした場合、最初の頂点座標は「基準位置」となります。

 さて、ではHUDを回転する事を次に考えてみます。具体的な例を挙げてみます。HUDを+30度回転させるという指示があった時、皆さんはどうイメージするでしょうか?

 HUDの中心から30度回転する上の図を想像した方、ぶっぶー!残念でした。正解はこちら:

 右下を基点として30度回転でした〜。「なんじゃそれ!」となりますが、その気持、重要なんです。回転には必ず「回転軸」が必要になります。単に30度ではどこが基準となって回転するか分からないわけです。つまり、HUDの回転を記述するためには回転角度と一緒に軸(ピボット座標)を指定しなければなりません。例えば先程の頂点座標、

Vtx vtx[3] = {
   { 50.0f, 60.0f},
   { 50.0f, 30.0f},
   {100.0f, 60.0f},
};

に対してピボット座標を、

float pivotX = 75.0f;
float pivotY = 45.0f;

と設定します。ピボット座標は基準ポリゴンに対して設定します。基準ポリゴンの値自体を先に設定した移動値を変えた場合にも変えないのと同じで、ピボット座標も移動値に対して不変です。

 では、ここまでの情報をクラスにまとめます。板ポリゴンの頂点座標情報はRegionというクラスのメンバにします。ピボット座標は頂点座標に基準があるためRegionクラスが保持します。移動量と回転角度はRegionとは独立した値であり、またこれらの値を使ってHUDを動かす事を考えると、別のクラス(構造体)に入れるのが良さそうです。Param構造体を作ってそのメンバにしましょう:

namespace Layout {
    class Region {
        Vector<Vtx> vertices;    // 頂点座標
        float pivotX, pivotY;    // 回転ピボット位置
    };

    struct Param {
        float x, y;    // 移動量
        float rot;     // 回転角度(Degree)
    };
}

HUDを描画する時にはこのRegionオブジェクトとParamオブジェクトの両方から両方を貰い、例えば次のように描画します:

g_pD3DDev->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(40,40,80), 1.0f, 0 );
g_pD3DDev->BeginScene();

// 領域から頂点情報を取得して頂点バッファを作成
struct Vtx {
    float x, y, z, w;
    DWORD color;
    Vtx(const Float2 &p) : x(p.x), y(p.y), z(), w(1.0f), color(0xffffffff) {}
};
Vector<Float2> &vtx = region.getVertices();
const Float2 &pv = region.getPivot();
const float rot = OX::Math::toRad(param.rot);

for (unsigned i = 0; i < vtx.size(); i++) {
    // 回転させるためピボット座標を一度原点に
    Float2 p = vtx[i] - pv;
    // 回転
    Float2 rotP(cosf(rot) * p.x - sinf(rot) * p.y, sinf(rot) * p.x + cosf(rot) * p.y);
    // ピボット位置へ戻す
    p = rotP + pv;
    // さらに移動
    p += param.trans;

    memcpy(buf + i * sizeof(Vtx), &Vtx(p), sizeof(Vtx));
}

// 描画
g_pD3DDev->SetFVF(D3DFVF_XYZRHW | D3DFVF_DIFFUSE);
g_pD3DDev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, buf, sizeof(Vtx));

g_pD3DDev->EndScene();
g_pD3DDev->Present( NULL, NULL, NULL, NULL );

forループ内がポイントです。最初にローカル頂点座標からピボット座標を引き算しています。これにより、ピボット位置が原点に戻ります。これはピボットを中心とした回転を行うためです。次に頂点座標を回転させています。回転後ピボット位置に戻し、さらに移動量分オフセットしています。

 矩形領域(0, 0, 200, 80)でピボット座標(100, 40)、移動量(310, 85)、回転量30度なHUDを上の描画コードで描いてみた結果が以下のとおりです:

うん、綺麗に思った通りに描画されています(^-^)。あ、初期位置とか矢印とか数字は後付けです。これでRegionに領域とピボット座標を指定し、Paramに移動量と回転量を指定すれば、スクリーンにどんどんHUDを置く基盤ができました。ここまでのサンプルはこちらからダウンロードできます。

 ここまではただ描画しただけです。レイアウトの機能としては、描画したHUDをクリックするなどした時に何らかの変化が起こらないといけません。そう「状態遷移」です。



A クリックしたら色が変わる

 先の矩形をクリックしたら色が変わるようにしてみましょう。そうですねぇ、クリック前が青色で、クリックしたら赤色にします。

 HUDをクリックした時に、それが領域内にあるか否かをどう判定するのか?これはRegionとParamの合わせ技です。上の図の回転後位置から判定するのは中々に難しいのはわかりますね。ですから発想を変えてクリックされた位置を逆変換して左上の初期位置に戻して比較するんです。矩形であればその判定は非常に軽い処理になります。

 逆変換は本当に逆順に変換していきます。まず最初に(310, 85)だけ移動しました。これを逆にするのですから(-310, 85)だけクリック位置を移動させます。次にピボット座標(100, 40)たけ動かしていたのでこれも逆変換(-100, -40)します。30度回転も-30度に、一番最初に行ったピボット座標分戻す変換も(100, 40)分逆変換します。@ (-310, -85)移動、A (-100, -40)移動、B -30度回転、C (100, 40)移動 です。

 試しにカーソル位置を描画してみたのが下のスクリーンショットです:

右側が実際のカーソル位置で、左上がそれをHUDのローカル空間へ逆変換した位置です。ちゃんとそれらしい位置を指していますよね。

 この変換位置を計算するにはRegionとParamの両方が必要です。どうやらそろそろ上のHUDを表すObjectクラスを新設する必要がありそうです:

class Object {
    sp<Region> region;
    Param param;

public:
    Object();
    virtual ~Object();

    // 領域設定
    void setRegion(sp<Region> region) {this->region = region;}

    // 領域取得
    sp<Region> getRegion() {return region;}

    // パラメータ設定
    void setParam(const Param &param) {this->param = param;}

    // パラメータ取得
    const Param &getParam() const {return param;}

    // 点が領域内にある?
    bool isContain(const Float2 &pos);
};

Objectクラスにregionとparamメンバを入れてあります。spはいわゆるスマートポインタです。isContainメソッドが引数の点が領域内に入っているか否かを判定するメソッドで、実装はこんな感じになります:

// 点が領域内にある?
bool Object::isContain(const Float2 &pos) {
    // オブジェクト空間へ変換
    // カーソル位置をHUDのローカル位置へ変換
    Float2 cp(pos);
    // @ 逆移動
    cp -= param.trans;
    // A 逆ピボット移動
    cp -= region->getPivot();
    // B 逆回転
    float rot = -Math::toRad(param.rot);
    cp = Float2(cosf(rot) * cp.x - sinf(rot) * cp.y, sinf(rot) * cp.x + cosf(rot) * cp.y);
    // C 正ピボット移動
    cp += region->getPivot();

    return region->isContain(cp);
}

引数のカーソル位置(スクリーン空間)を逆変換して領域空間へ持って行って判定しています。

 さて、では本番。このHUD領域をクリックした時に赤色に変わる部分を作ります。Objectにどうやってクリック情報を伝えるべきでしょうか?一つの考え方はObject::clickAction()のようなメソッドを作り、その中ですべき事(赤色に変える)を実装してしまうという手です。ただ、これは外からの刺激分だけメソッドを増やす必要にかられます。右クリック時、左クリック解除時、カーソルが領域に入ってきた時、ダブルクリック時など、マウス情報だけでもかなりな種類があります。外の人が逐一これらのメソッドを呼び出すのは苦痛です。そこで、カーソル情報をまとめたクラスを作り、そのオブジェクトをObject::mouseAction(Mouse *mouse)メソッドに渡すという方法を取ります。そして内部でカーソル情報を取得し、適切な振る舞い(アクション)を行わせます。どういうアクションになるかはわかりませんので、mouseActionメソッドは純粋仮想メソッドになりそうです。

 では、クリックすると赤色になるTurnRedObjectクラスをObjectクラスから派生させて作り、mouseActionメソッドを実装してみましょう:

// マウスアクション
void TurnRedObject::mouseAction(Mouse &mouse) {
    // クリックされたらパラメータを赤色にする
    if (mouse.isDown(Mouse::L) && this->isContain(mouse.getPos()))
        param.color = 0xffff0000;
}

Param構造体にcolorメンバを追加しています。条件文はマウスの左クリックが行われていて、且つポイント位置が領域内にある時です。Mouseクラスの各メソッドはどうとでも実装できますので割愛。描画側はParam::colorを頂点カラーとして反映させます。

 ほんではやってみましょう。HUD領域にマウスカーソルを合わせて・・・ぽちっとな:

お〜変わる変わる(^-^)。これでマウスアクションに対応したHUDの原型のような物が出来ました。ここまでのサンプルはこちらからダウンロードできます。

 さて、今の段階はマウスが押された瞬間に赤にするといういわゆる「ワンタイムアクション」です。でも、そのアクションの後、何も起こっていません。せっかくアクションを起こしたのですから、誰かに反応してもらわないと無意味なスイッチみたいなもので、意味があまりないわけです。では、そのアクションの結果をどうやって外に伝えるのか?それは・・・次の章で考えてみましょう。