ワイヤーフレーム型3Dダンジョン制作のヒント

追記:3Dダンジョンゲームを作る場合のコメント。本当はもうちょっと書くつもりでしたが、事故で投稿してしまい、今更消しても仕方がないので残します。

サイトが重いので、いつ更新できるか分かりません。この追記も投稿できるのかどうか。

都合により、話が唐突です。書き替える気力が回復したら更新するかもしれませんが、分かりません。


迷路のデータ構造を考える前に、どうやったらコーディングしやすいかをイメージします。

JavaC++などのオブジェクト指向言語なら、以下の行で壁の見え方を取得できるデザインが楽をさせてくれます。

// 壁の見え方を取得します
//    maze : ダンジョン(ダンジョン全体や一フロアなど)
//    pos  : 現在位置と向き
v = maze.getWallView( pos );

// 壁の通行の許可状況を取得します
rule = maze.getPassRule( pos );

以上の二行(一行にまとめて、二つの情報を合わせて取得するようにしてもいいです)を実現できるデータ構造を考えるところから始めます。メモリ構造は自由です。

移動クラス

全体の肝、pos(クラス名はPositionとかPosDirとか)は、現在位置を変更できる位置オブジェクトです。使い勝手がいいので、X座標・Y座標・方位をカプセル化してクラスを作っておきます。これをバラバラで扱うと難解なプログラムになります。

以下を見てみましょう。直感的で分かりやすいですよね。

// 方向転換
pos.toLeft();
pos.toRight();
pos.toBack();
pos.toNorth();
pos.toEast();
pos.toSouth();
pos.toWest();

// 移動
pos.move();       // 1歩進む。pos.walk()とかでもOK
pos.move( 3 );    // 前方に3歩
pos.moveFront();  // move()と同じ
pos.moveLeft();
pos.moveRight();
pos.moveBack();
pos.moveNorth();
pos.moveEast();
pos.moveSouth();
pos.moveWest();

// 上下への移動(何階にいるかの情報(Z座標)を持つ場合)
pos.up();
pos.down();

// 取得
x = pos.getX();
y = pos.getY();
z = pos.getZ();
dir = pos.getDir();  // 方位を返す

// 方向転換したものを取得(自身は変化しません)
posTmp = pos.getLeft();
posTmp = pos.getRight();
posTmp = pos.getBack();
posTmp = pos.getNorth();
posTmp = pos.getEast();
posTmp = pos.getSouth();
posTmp = pos.getWest();

// 移動したものを取得(自身は変化しません)
posTmp = pos.getMove();
posTmp = pos.getMoveFront();  // getMove()と同じ
posTmp = pos.getMoveLeft();
posTmp = pos.getMoveRight();
posTmp = pos.getMoveBack();
posTmp = pos.getMoveNorth();
posTmp = pos.getMoveEast();
posTmp = pos.getMoveSouth();
posTmp = pos.getMoveWest();
posTmp = pos.getUp();
posTmp = pos.getDown();

posは、主人公の現在位置を表すに留まりません。

  • 3Dの迷路や2Dの地図を描画するとき、一時的に現在位置のコピーを作り、移動しながら描く
  • ダンジョンを自動生成するとき、現在掘っている場所を移動しながら生成する

便利なので、最初に一気に作ってしまうのがいいです。どれだけ便利かというと、例えば、一歩進んで後ろを向いたところの壁を得るには、「pos.getMove().getBack()」を位置に指定すればいいわけです。ゲームでは一方通行扉などの仕掛けを実装するわけですから、そういう情報を手軽に調べられると便利です。

// 目の前に壁があるか?
if( maze.getWallView( pos ) ) {
    // ある
}
// 左に壁があるか?
if( maze.getWallView( pos.getLeft() ) ) {
    // ある
}
// 目の前の逆側から通れるか?
if( maze.getPassRule( pos.getMove().getBack() ) ) {
    // 通れる
}

迷路の中を一歩進むのに「x=x+1」とか「if(東向いてる?)」とか、いちいちやっていると大変です。

壁や扉のデータはどう表すか

壁や扉を1ビットで表すべきかは、開発環境ごとに考えてみます。壁を1ビットで表す手は、かつて手続き型のプログラミング言語では多用されました。ゲームで扱う「壁」が、壁と扉の2形状しかないなら、確かに2ビットで済みますが、この発想でプログラムを書くと、拡張性の乏しい壁システムになりがちです。

オブジェクト指向言語なら、壁データを持たずに、仮想関数で返す手があります。壁クラスの基底クラスを作るというのはどうでしょう。

// 基底クラスで仮想定義(Javaならvirtual,constは不要)

// 壁の見え方
virtual int getViewType( Position pos ) const
{
    return VIEW_NOTHING; /* 壁の形状なし */
}

// 通れるか通れないか
virtual int getPassRule( Position pos ) const
{
    return PASS_GO; /* 通れる */
}

壁の見え方や、通行許可状況についても、整数を使わずにクラス化できるならしてみましょう。

壁クラスでオーバーライド
// 壁の見え方。オーバーライド
int getViewType( Position pos ) const
{
    return VIEW_WALL; /* 壁の形をしている */
}

// 通れるか通れないか。オーバーライド
int getPassRule( Position pos ) const
{
    return PASS_STOP; /* 通れない */
}
一方通行壁クラスでオーバーライド

ウィザードリィの一方通行壁です。Desigeonの場合は北・西バージョンと、南・東バージョンの二つを作っています。

// 壁の見え方。オーバーライド
int getViewType( Position pos ) const
{
    // 北か西を向いているときだけ壁に見える
    return ( ( pos.getDir() == North || pos.getDir() == West ) ? VIEW_WALL : VIEW_NOTHING );
}

// 通れるか通れないか。オーバーライド
int getPassRule( Position pos ) const
{
    // 北か西を向いているときだけ通れない
    return ( ( pos.getDir() == North || pos.getDir() == West ) ? PASS_STOP : PASS_GO );
}

壁があるかどうかデータを持ち、ANDでビットを調べてもいいです。どのみち、細かい実装はカプセル化するのですから、内部構造は自由でいいと思います。

保存方法

ファイルに保存するときは、シリアライズの機構が良いと思います。Javaなら標準装備ですが、C++の場合は自分で作るか、ライブラリの手を借りなければなりません。

Desigeon

Desigeonではこのようにしています。壁にはデータを割り当てていません。壁クラス、扉クラス、一方通行扉クラス……、と別れていて、通行を許可するかどうか、壁がどんな風に見えるかは、それぞれの仮想関数が返しています。

……ただ、それによって要求メモリの割に拡張性があってオッケー!とかはしゃげるような状況にはありません。作りやすさ、慣れ、自己満足などの要素が影響してこうなっているだけなので、やっぱり、それぞれの作者が作りやすい実装を考えていくしかなさそうです。