Redpoll's 60
 Home / 3Dプログラミング入門 / 第2章 $§$2-22
第2章 2D空間におけるオブジェクトの運動
$§$2-1 オブジェクトの初期状態$§$2-17 衝突判定 5
$§$2-2 行列による変換の詳細 1$§$2-18 初期状態における頂点情報の取得について
$§$2-3 行列による変換の詳細 2$§$2-19 衝突判定 6 (軸平行な長方形同士の衝突)
$§$2-4 自転と公転$§$2-20 衝突判定 7 (円盤 vs 長方形)
$§$2-5 一体化したオブジェクトの運動 1$§$2-21 衝突判定 8 (回転した長方形同士の衝突)
$§$2-6 一体化したオブジェクトの運動 2$§$2-22 衝突判定 9 (「倉庫番」プログラムの作成)
$§$2-7 一体化したオブジェクトの運動 3$§$2-23 衝突判定 10 (円盤 vs 三角形)
$§$2-8 指定方向へのオブジェクトの移動 1$§$2-24 衝突判定 11 (直線 vs 長方形、円盤、直線)
$§$2-9 指定方向へのオブジェクトの移動 2$§$2-25 円と直線による補間曲線 1
$§$2-10 指定方向へのオブジェクトの移動 3$§$2-26 円と直線による補間曲線 2
$§$2-11 指定方向へのオブジェクトの移動 4 (連射プログラムの実装)$§$2-27 その他の重要事項 1 (UnityのTransformクラスによる記述 ; カメラ移動の基本)
$§$2-12 指定方向へのオブジェクトの移動 5$§$2-28 その他の重要事項 2 (画面に表示されるXY平面の範囲 ; ミニマップの実装)
$§$2-13 衝突判定 1 (点 vs 円盤、長方形 ; ローカル座標からワールド座標への変換 2D)$§$2-29 その他の重要事項 3 (スクリーン座標からワールド座標への変換 2D; スクリーンショットの撮影範囲)
$§$2-14 衝突判定 2$§$2-30 課題 1
$§$2-15 衝突判定 3$§$2-31 課題 2
$§$2-16 衝突判定 4

$§$2-22 衝突判定 9 (「倉庫番」プログラムの作成)


2Dゲームの中にはパズルゲームが数多く存在するが、パズルゲームでは軸平行な長方形や正方形を使うことが多い。その中ではやはり何らかの形で軸平行な長方形同士の衝突判定が行われているのである。
衝突判定といっても、2-19節で扱った長方形の射影区間を比較するといった’込み入った’やり方はパズルゲームでは必要としない場合も多い。
図1 格子状のステージで行われるパズルゲーム
例えば、パズルゲームの多くは右図に示されるような格子状に区切られたステージの中で行われる。そういったゲームにおけるオブジェクトの移動は上下左右の4方向だけであったり、格子状のマス目1つ分の移動といったような限られたものでしかない。
このような場合におけるオブジェクト同士の衝突判定は、今まで見てきた衝突判定に比べてはるかに単純に実装することができる。
本節では、パズルゲームの作成を通して今述べたような特別な場合における正方形同士の衝突判定について扱う。
ここで作成するゲームは「倉庫番」といわれるゲームである。


# Code1
このゲームは図2に示されるような格子状のステージで行われる。本節のプログラムで使われるステージは全て $9\times9$ の格子状であり、$81$個の「セル」によって構成されている (1つ1つのマス目のことを本節では「セル」と呼ぶことにする)。表計算ソフトのように「行」「列」という語を用いれば、各ステージは$9$行$9$列で構成されており、図2に示されるように、一番上の段が $0$行目、その下が $1$行目であり、一番左の列が $0$列目、その1つ右の列が $1$列目である ($0$から始まることに注意)。例えば、図3の赤いセルは $3$行$2$列目のセルであり、青いセルは $7$行$6$列目のセルである。なお、セルは縦横の長さが等しい正方形であり、その長さは $2.0$ である。

図2  9行9列の格子状のステージ
図3 赤いセルは 3行2列目、青いセルは 7行6列目

また、本節では「ロケーション」という単語が使われるが、ロケーションはセルが何行何列目であるかを表すものである。図3の例でいえば、赤いセルのロケーションは $3$行$2$列目、青いセルのロケーションは $7$行$6$列目である。

図4は Square という名前のオブジェクトであり、これはユーザーが操作するオブジェクトである。Squareの大きさはちょうどセル1つ分の大きさであり、Squareは初期状態でその中心が原点に置かれている。
ここでは、まずSquareの移動から始める。

図4 Square 初期状態
図5

Squareの移動は 上、下、左、右 の4方向のみであり(図5)、各方向への移動には以下のキー操作が対応している。

H  :  左へセル1つ分だけ移動する。
J  :  下へセル1つ分だけ移動する。
K  :  上へセル1つ分だけ移動する。
L  :  右へセル1つ分だけ移動する。

すなわち、各キーを押すとSquareはちょうどセル1つ分だけ移動することになるが、以下ではこの実装について考えていこう。
次のプログラムは Kキーを1回押すと自動的に上方向に移動していくプログラムである。
if (i_MOVE)
{
    Vector2 newSqrPos = Square.GetPosition() + i_moveVtr;

    THMatrix3x3 T = TH2DMath.GetTranslation3x3(newSqrPos);
    Square.SetMatrix(T);

    return;
}

if (Input.GetKeyDown(KeyCode.K))
{
    i_MOVE = true;
    i_moveVtr = new Vector2(0.0f, 0.05f);
}

Kキーが押されると11行目のif文に入るが、そこでは2つのインスタンス変数に値がセットされる。
i_MOVEbool型のインスタンス変数で、初期値は false である。この変数は 1行目のif文の条件で使われており、この変数が true の場合には毎フレーム1行目のif文に入ることになる。
i_moveVtrVector2型のインスタンス変数で、Squareの1フレームあたりの移動量を表す。14行目では $(0.0,\ 0.05)$ がセットされているが、これはSquareが毎フレーム 上方向(y軸プラス方向)に $0.05$ ずつ進んでいくことを意味している。
1行目のif文内の処理は、まず 3行目でSquareの現在位置を取得し、そのフレームにおける移動先の位置 newSqrPos を計算する。そして、6行目において実際にSeuareをその位置へ移動させている。
このif文の終わりに return文があるので、i_MOVEtrueの間は10行目以降へは処理が進まない。

図6 Squareを2.0だけ移動させれば1セル分進むことになる
上のプログラムでは1度Kキーが押されると確かに、Squareは上方向へ進んでいくがそれ以降も止まることなく上方向へ進み続ける。目的とする処理は、キーを押した際に1セルだけ進ませるというものであったから、自動進行中に何らかの条件判定によって停止させる処理が必要となるわけである。
ステージ上の各セルは縦横の長さが $2.0$ の正方形であるから、キーを押してSquareが移動を始めてから $2.0$ 移動した時点で停止させれば、結果的にちょうど1セル分だけ進むことになる。
例えば、ここでは1セル分の移動に $20$フレームかかるものとしよう。1セルの長さは $2.0$ なので1フレームあたりの移動距離は $2.0 \div 20 = 0.1$ である。
ここで、移動を開始してから $20$フレーム後に停止させるためにインスタンス変数を1つ用意する。
int  i_moveCount
  :  移動を開始してから何フレーム経過したかを表すint型変数。移動のためのキーが押されると $0$がセットされ、移動中は毎フレーム $1$ずつ増加する。

次のプログラムは Kキーを押すと上方向に1セル分だけ進むプログラムである。
[Beta1]  (実行結果 図7)
if (i_MOVE)
{
    i_moveCount++;
    Vector2 newSqrPos = Square.GetPosition() + i_moveVtr;

    THMatrix3x3 T = TH2DMath.GetTranslation3x3(newSqrPos);
    Square.SetMatrix(T);

    if (i_moveCount == c_maxMoveCount)
    {   // あるセルに到達
        i_MOVE = false;
    }

    return;
}

if (Input.GetKeyDown(KeyCode.K))    // 上のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(0.0f, i_moveSpeed);
}

図7 Beta1 実行結果
このプログラムでは1セル分の移動を一定のフレーム数で行わせるようにしたものである。9行目で使われている c_maxMoveCount は、1セルの移動にかかるフレーム数を表す定数である(int型)。ここでの値は $20$ であるが、これはSquareが1セル分の移動を$20$フレームで行うという意味である。
Kキーが押されると17行目のif文に入るが、ここでは先程のプログラムで見られた変数の他に、上で述べたインスタンス変数 i_moveCount が追加されている。キーが押されてから移動を開始するので、キーが押されるたびに $0$ がセットされる。また 21行目の i_moveSpeed は、Squareの1フレームあたりの移動距離であり、それは上で計算した値 $0.1$ ($2.0 \div 20$) である。
キーが押されると、それ以降毎フレーム1行目のif文に入るが、ここではまず i_moveCount がインクリメントされる。キーが押されてから20フレーム後には、その値が $20$ となるが、定数c_maxMoveCountの値が $20$ なので、そのフレームにおいて9行目のif文に入り、そこで i_MOVEfalseにする。したがって、それ以降は次にキーが押されるまで1行目のif文に入ることはない。
4行目の newSqrPos は各フレームにおけるSquareの移動先の位置である。今回は i_moveVtr の値が $(0.0,\ 0.1)$ であり、毎フレーム $0.1$ ずつ上に移動する。したがって、各フレームにおいて4行目で計算される位置は そのときの位置から上方向に $0.1$ だけ上方向に進んだ位置になる。20フレーム後には移動開始位置から$2.0$進んだ位置が計算されるが、その位置が目的地(1つ上のセル)である。

Beta1は上方向のみの移動であったが、他の3方向への移動についても処理は同様である。以下のプログラムは4つのキー(H、J、K、L)によって左、下、上、右へ1セルずつ移動を行うようにしたものである。
[Code1]  (実行結果 図8)
if (i_MOVE) // セル移動処理
{
    i_moveCount++;
    Vector2 newSqrPos = Square.GetPosition() + i_moveVtr;

    THMatrix3x3 R = TH2DMath.GetRotation3x3(i_degSquare);
    THMatrix3x3 T = TH2DMath.GetTranslation3x3(newSqrPos);
    THMatrix3x3 M = T * R;
    Square.SetMatrix(M);

    if (i_moveCount == c_maxMoveCount)
    {   // あるセルに到達
        i_MOVE = false;
    }

    return;
}


if (Input.GetKeyDown(KeyCode.H))    // 左のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(-i_moveSpeed, 0.0f);
    i_degSquare = 90;
}
else if (Input.GetKeyDown(KeyCode.L))    // 右のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(i_moveSpeed, 0.0f);
    i_degSquare = 270;
}
else if (Input.GetKeyDown(KeyCode.J))    // 下のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(0.0f, -i_moveSpeed);
    i_degSquare = 180;
}
else if (Input.GetKeyDown(KeyCode.K))    // 上のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(0.0f, i_moveSpeed);
    i_degSquare = 0;
}


図8 Code1 実行結果
20行目以降のif文は4つのキーが押されたときの処理であり、いずれの場合も i_MOVEi_moveCount にセットする値は同じである。今までは上方向だけが問題であった、例えば Hキーが押された場合は左方向へ進むことになるが、 i_moveVtr の値は $(-0.1,\ 0.0)$ であり(24行目)、右方向(Lキー)であれば $(0.1,\ 0.0)$ である(31行目 ; i_moveSpeed は $0.1$ である)。
また、このプログラムでは i_degSquare というインスタンス変数が追加されているが、この変数はSquareの向きを表すための変数である(int型)。Squareは初期状態では上方向(y軸プラス方向)を向いているので、上に進む場合はSquareを回転させる必要はない。したがって i_degSquare の値は $0$ である。左に進む場合はSquareの向きが左を向くように$90$°回転させる。したがって、この場合は i_degSquare の値は $90$ である。
1行目のif文の内容はBeta1とほとんど同じである。Beta1においてはSquareは平行移動を行っていただけであったが、今回は進む方向へSquareを回転させる回転行列が追加されている(6行目)。これによって、Squareは移動方向と自身の向いている方向が一致するようになる。


# Code2
上のプログラムでは、指定のキーを押せば指定の方向に移動することができた。ここでは1つ設定を追加して、指定のキーを押しても指定の方向には移動できない状態が生じるようにする。
今までのプログラムにおけるステージでは、下図9の明るい色のセルだけが使われていた。このセルを以降「Floor」と呼ぶことにする。

  • 図9 Floor
  • 図10 Block
  • 図11 Blockに衝突した場合は先へは進めない

ここでは、図10に示される別の種類のセル「Block」を用意する。今までのステージはFloorだけで構成されており、どの位置においても自由に移動ができた。Blockはそれ以上先には進むことができないことを表すセルである。つまり、Squareが移動中にBlockに衝突した場合、その方向へはそれ以上進むことができない。
今回のプログラムでは、このBlockに衝突した際の「行き止まり」を実装する。

まずインスタンス変数を1つ用意する。
int[,]  i_cellInfo
  :  セルの種類が「Floor」であるか「Block」であるか、あるいは「Mark」であるかの情報を保持するint型の2次元配列 (「Mark」については後述する)。この2次元配列の要素数はステージのセル数に対応しており、9行9列、計81要素である。1つの要素が1つのセル情報を保持しており、Blockであれば要素の値は 0、Floorであれば値は 1 である。

図12 Blockに囲まれたステージ
例えば、右図12に示されるようなステージがあったとしよう。
このステージでは一番上の行 及び一番下の行、一番左の列 及び一番右の列のすべてのセルがBlockであり、それ以外のセルはすべてFloorで構成されている。
以下のコードはこのステージのセル情報である。
ステージのセル情報は上記の2次元配列 i_cellInfo にセットされる。その内容であるが、一番上の行 及び一番下の行、一番左の列 及び一番右の列の値がすべて $0$ となっており、それ以外の値はすべて $1$ である。
$0$ はBlockを表し、$1$ はFloorを表すので i_cellInfo の内容は図12のステージの各セルに1対1に対応している。

[図12のステージにおける各セルの情報]
i_cellInfo = new int[,]    // 9行9列
{
    {0, 0, 0, 0,  0,  0, 0, 0, 0}, 
    {0, 1, 1, 1,  1,  1, 1, 1, 0}, 
    {0, 1, 1, 1,  1,  1, 1, 1, 0}, 
    {0, 1, 1, 1,  1,  1, 1, 1, 0}, 
    {0, 1, 1, 1,  1,  1, 1, 1, 0}, 
    {0, 1, 1, 1,  1,  1, 1, 1, 0}, 
    {0, 1, 1, 1,  1,  1, 1, 1, 0}, 
    {0, 1, 1, 1,  1,  1, 1, 1, 0}, 
    {0, 0, 0, 0,  0,  0, 0, 0, 0}
};

さらに、次の補助的なメソッドを用意する。
このメソッドは、今から進もうとしているセルがBlockかどうかを調べるものである。
[CollisionTest_vs_Block(..)]
bool CollisionTest_vs_Block(Vector2 pos, Vector2 move_vtr)
{
    CellLoc nextLoc = GetCellLocation(pos);

    float eps = 0.001f;
    if (move_vtr.y > eps)
    {
        nextLoc.row--;
    }
    else if (move_vtr.y < -eps)
    {
        nextLoc.row++;
    }
    else if (move_vtr.x < -eps)
    {
        nextLoc.col--; 
    }
    else if (move_vtr.x > eps)
    {
        nextLoc.col++;   
    }


    // 進もうとしているセルがBlockかどうか
    if (i_cellInfo[nextLoc.row, nextLoc.col] == 0)
    {
        return true;
    }

    return false;
}

引数には現在の位置 pos と (そのフレームにおける)移動量 move_vtr が送られてくる。3行目の GetCellLocation(..) は引数に指定した位置にあるセルのロケーションを取得するメソッドである。戻り値は CellLoc という型で返されるが CellLoc は今回のプログラムのために用意された構造体である。2つのint型メンバ変数 rowcol を持ち、それらはセルのロケーション(行、列)を表す。

例えば、下図13のSquareの位置はXY平面上の $(4, 6)$ であるが、GetCellLocation(..) にこの値をセットすると、Squareが乗っているセルのロケーション $(row, col) =(1, 6)$ が返される (図14の 1行6列目のセル)。

図13 SquareのXY平面上の位置は (4, 6)
図14 Squareのステージ上でのロケーションは 1行6列目

今回の場合は、上記 CollisionTest_vs_Block(..) の引数posは、Squareの現在位置であるから3行目で取得されるロケーションはSquareの現在乗っているセルのロケーションが返される。
6行目から21行目の if/else文では今から進もうとしているセル(移動先のセル)のロケーションを求めている。例えば、そのフレームにおいて上に進む場合引数 move_vtr は $(0.0, 0.1)$ であるから6行目のif文に入るが、ここで今現在いるセルの1つ上のセルのロケーションを計算する。今現在いるセルは3行目において nextLoc にセットされているから、その1つ上のセルは nextLoc.row を $1$だけマイナスすればよい。図14を例にとると、3行目において nextLoc には $(row, col) = (1, 6)$ がセットされるが、上方向へ進む場合には6行目のif文内で nextLoc の値が $(row, col) = (0, 6)$ に変わる ( $(0, 6)$ は今から進もうとしているセルのロケーション)。
そして、25行目において今から進もうとしているセルがBlockかどうかを調べ、そうであればtrueが返される。

以下のプログラムはBlockに衝突した場合の「行き止まり」を実装したものである。
[Code2]  (実行結果 図15)
if (i_MOVE) // セル移動処理
{
    i_moveCount++;
    Vector2 newSqrPos = Square.GetPosition() + i_moveVtr;

    THMatrix3x3 R = TH2DMath.GetRotation3x3(i_degSquare);
    THMatrix3x3 T = TH2DMath.GetTranslation3x3(newSqrPos);
    THMatrix3x3 M = T * R;
    Square.SetMatrix(M);

    if (i_moveCount == c_maxMoveCount)
    {   // あるセルに到達
        i_MOVE = false;
    }

    return;
}


if (Input.GetKeyDown(KeyCode.H))    // 左のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(-i_moveSpeed, 0.0f);
    i_degSquare = 90;
}
else if (Input.GetKeyDown(KeyCode.L))    // 右のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(i_moveSpeed, 0.0f);
    i_degSquare = 270;
}
else if (Input.GetKeyDown(KeyCode.J))    // 下のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(0.0f, -i_moveSpeed);
    i_degSquare = 180;
}
else if (Input.GetKeyDown(KeyCode.K))    // 上のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(0.0f, i_moveSpeed);
    i_degSquare = 0;
}
else 
{
    // キーが押されていないときは、以降の処理は行わない
    return;
}


// Square vs Block
Vector2 posSqr = Square.GetPosition();
bool CANCEL_MOVE = CollisionTest_vs_Block(posSqr, i_moveVtr);
if (CANCEL_MOVE)
{
    i_MOVE = false;
    THMatrix3x3 R = TH2DMath.GetRotation3x3(i_degSquare);
    THMatrix3x3 T = TH2DMath.GetTranslation3x3(posSqr);
    THMatrix3x3 M = T * R;
    Square.SetMatrix(M);
}


図15 Code2 実行結果
47行目まではCode1と同じである。移動キー(H、J、K、L)が押されれば、次のフレームからセル移動処理(1行目のif文)が始まるが、今回は移動先のセルがBlockである場合にはセル移動処理をキャンセルする処理が追加されている。
それは55行目以降で、上で解説した CollisionTest_vs_Block(..) にSquareの現在位置及び移動量ベクトル(1フレームあたりの移動量を表すベクトル)をセットし、今から進もうとしているセルがBlockかどうかをまず調べている。移動先のセルがBlockであれば、ローカル変数 CANCEL_MOVEtrueになるので、58行目のif文に入り、次のフレームからのセル移動処理をキャンセルするために i_MOVEfalseにする。
なお実行結果(図15)に見られるように、セル移動処理がキャンセルされた場合でもSquareの向きをキーが押された方向(進もうとしていた方向)に変えるために、61行目以降で現在位置において向きだけを変化させる処理を行っている。
また、ここではキー操作の if/else文の最後に else文(48行目)が追加されている。もし、移動キーが押されていない場合はSquareの移動は発生しない、したがって移動先のセルを調べる必要もないので、移動キーが何も押されていないのであればそれ以降の処理を行わないようにするために、キー操作の最後のelseにおいて return文を入れている。


# Code3
ここでは新たにオブジェクトをもう1つ用意する。図16は Cargo というオブジェクトであり、Squareと同じくその大きさはセル1つ分の大きさである。
今回のプログラムでは Squareの移動中にCargoと衝突した際、SquareとCargoが一体となって同じ方向に移動するという処理を実装する (つまり SquareがCargoを押しながら移動する処理の実装である)。

図16 Cargo 初期状態
図17 Squareのすぐ上にCargoがある場合

例えば、図17のような場合 Squareのすぐ上にCargoがあるので、Kキーを押してSquareを上へ移動させるとき、SquareがCargoを押すような形で移動させなければならない。結果的にはCargoもセル1つ分上へ移動しているということになる。
この「押しながらの移動」は次のように簡単に実装できる。Squareはセル移動処理の際に 20フレームかけて1セル分の移動を行うが、「押しながら移動」の場合にはこの20フレームの間にCargoに対しても、毎フレーム Squareと同じだけの移動を実行すればよいのである。

今回のプログラムにおいても補助的なメソッドを用意する。
このメソッドは、今から進もうとしているセルにCargoがあるかを調べるものである。
[CollisionTest_vs_Cargo(..)]
int CollisionTest_vs_Cargo(Vector2 pos, Vector2 move_vtr, int crg_idx = -1)
{
    CellLoc nextLoc = GetCellLocation(pos);

    float eps = 0.001f;
    if (move_vtr.y > eps)
    {
        nextLoc.row--;
    }
    else if (move_vtr.y < -eps)
    {
        nextLoc.row++;
    }
    else if (move_vtr.x < -eps)
    {
        nextLoc.col--; 
    }
    else if (move_vtr.x > eps)
    {
        nextLoc.col++;   
    }


    for(int i=0; i<Cargo.Length; i++)
    {
        if(i == crg_idx) { continue; }

        CellLoc crgLoc = GetCellLocation(Cargo[i].GetPosition());
        if (nextLoc.row == crgLoc.row && nextLoc.col == crgLoc.col)
        {
            return i;
        }
    }

    return -1;
}

このメソッドは先ほどの CollisionTest_vs_Block(..) とほとんど同じである。
まず、3行目において引数posの位置にあるセルのロケーションを取得する (今回の場合はSquareが現在いるセルのロケーションが返される)。6行目から21行目では、引数move_vtrをもとにして今から進もうとしているセルのロケーションを計算する。
移動先のセルのロケーションが計算された後は 24行目のfor文において、その移動先のセルにCargoがあるかを調べる。そのためにはCargoの置かれているセルのロケーションが必要だが、それは28行目で計算される (Cargoの現在位置からそのCargoの置かれているセルのロケーションを求めている)。
今回のプログラムではCargoは1つしか使わないが、最終的には複数のCargoを使用する。したがって、24行目のfor文では移動先のセルにステージ上のCargoうちどれか1つがあるかどうかを調べているのである。
移動先のセルにCargoがある場合は、そのCargoのインデックスを返す。したがって、移動先のセルにCargoがある場合このメソッドから返される値は $0$ 以上である。
なお、26行目のif文で使われているメソッドの第3引数 crg_idx については後述する (この引数はCargo同士の衝突を調べる場合に使われる)。

では、SquareがCargoを押しながら移動する処理を実装したプログラムを以下に示す。
[Code3]  (実行結果 図18)
if (i_MOVE) // セル移動処理
{
    i_moveCount++;
    Vector2 newSqrPos = Square.GetPosition() + i_moveVtr;

    THMatrix3x3 R = TH2DMath.GetRotation3x3(i_degSquare);
    THMatrix3x3 T = TH2DMath.GetTranslation3x3(newSqrPos);
    THMatrix3x3 M = T * R;
    Square.SetMatrix(M);

    if (i_pushCargoIndex >= 0)
    {
        Vector2 newCrgPos = Cargo[i_pushCargoIndex].GetPosition() + i_moveVtr;
        Cargo[i_pushCargoIndex].SetPosition(newCrgPos);
    }

    if (i_moveCount == c_maxMoveCount)
    {   // あるセルに到達
        i_MOVE = false;
    }

    return;
}


if (Input.GetKeyDown(KeyCode.H))    // 左のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(-i_moveSpeed, 0.0f);
    i_degSquare = 90;
}
else if (Input.GetKeyDown(KeyCode.L))    // 右のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(i_moveSpeed, 0.0f);
    i_degSquare = 270;
}
else if (Input.GetKeyDown(KeyCode.J))    // 下のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(0.0f, -i_moveSpeed);
    i_degSquare = 180;
}
else if (Input.GetKeyDown(KeyCode.K))    // 上のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(0.0f, i_moveSpeed);
    i_degSquare = 0;
}
else 
{
    // キーが押されていないときは、以降の処理は行わない
    return;
}


// Square vs Cargo
Vector2 posSqr = Square.GetPosition();
i_pushCargoIndex = CollisionTest_vs_Cargo(posSqr, i_moveVtr);


図18 Code3 実行結果
移動キーのいずれかが押されると59行目以降の処理に進むが、そこで行っているのはSquareを移動させた場合にCargoと衝突するかの判定である。これは先ほど解説した部分である。CollisionTest_vs_Cargo(..) にSquareの現在位置と移動量を表すベクトルをセットして、移動先のセルにCargoがあるかを調べている。Cargoがあればインスタンス変数 i_pushCargoIndex に、そのCargoのインデックスがセットされる。
1行目のif文(セル移動処理)にも処理が1つ追加されている。それは11行目のif文である。もし i_pushCargoIndex が $0$以上ならばそれはSquareがCargoを押しながら移動することを意味する。したがって、セル移動処理では20フレームかけてセル1つ分の移動を行うが、その20フレームの間に11行目のif文においてSquareと同じ量の移動をCargoに対しても毎フレーム実行しているのである (毎フレーム現在位置から i_moveVtr だけ移動する)。


# Code4
Squareの移動中にBlockに衝突するとそれ以上先へは進めない。すなわち、移動キーを押しても進む方向にBlockがあればセル移動処理は行われないのであった。
ここでは、セル移動処理が行われなくなる他の場合について見ていく。それは次の2つの場合である。

Cargoを押しながら移動している間に
    -->  CargoがBlockに衝突する場合
    -->  Cargoが別のCargoに衝突する場合

図19 CargoがBlockに衝突する場合(上へは進めない)
図20 Cargoが別のCargoに衝突する場合(左へは進めない)

以下のコードはSquareがCargoを押しながら移動している間に、CargoがBlockまたは別のCargoに衝突した際、次のフレームからのセル移動処理をキャンセルさせる部分のみを実装したものである。
bool CANCEL_MOVE = false;

if(i_pushCargoIndex < 0)
{
    // Squareが単独で移動
    // ..    
    // ..
}
else
{    // Cargoを押しながら移動

    Vector2 posCrg = Cargo[i_pushCargoIndex].GetPosition();

    // Cargo vs Block
    CANCEL_MOVE = CollisionTest_vs_Block(posCrg, i_moveVtr);

    if (!CANCEL_MOVE)
    {
        // Cargo vs Cargo
        int collIdx = CollisionTest_vs_Cargo(posCrg, i_moveVtr, i_pushCargoIndex);
        if (collIdx >= 0)
        { 
            CANCEL_MOVE = true; 
        }
    }
}


Cargoを押しながら移動する場合はインスタンス変数i_pushCargoIndexの値は$0$以上である。したがって、押しながら移動する場合は上記の9行目以降の else文に入る。そこでは押しているCargoの現在位置を取得し(12行目)、押しているCargoの移動先のセルにBlockがあるかどうかを調べる(15行目)。もし、押しているCargoの先にBlockがあればそれ以上は進めないので移動をキャンセルするためにローカル変数CANCEL_MOVEtrueをセットする。
押しているCargoの先にBlockがない場合、さらに続けて Cargoが別のCargoと衝突するかを調べる必要がある(17行目のif文)。もし、別のCargoと衝突する(押しているCargoの移動先のセルに別のCargoがある)とわかれば、ここでローカル変数CANCEL_MOVEtrueにする。
Blockまたは別のCargoに衝突するかどうかを調べるために上で使われたメソッド CollisionTest_vs_Block(..)CollisionTest_vs_Cargo(..) がここでも使われている。第2引数の移動量を表すベクトルはSquareの場合と同じであるが、第1引数にはCargoの現在位置をセットしなければならない。Cargoの現在位置はそれら2つのメソッドの中で、Cargoが今から進もうとしているセルのロケーションを算出するために使われる。

なお、20行目の CollisionTest_vs_Cargo(..) の第3引数にはSquareの場合は何もセットしなかったが、ここでは押しているCargoのインデックスi_pushCargoIndexがセットされている。
これは CollisionTest_vs_Cargo(..) の24行目のfor文内において自分自身との衝突判定を回避するためである。このfor文では、移動先のセルにステージ上のCargoのうちどれか1つが置かれているかを調べている。Squareの場合はすべてのCargoを調べる必要があるが、押しているCargoの場合は「移動先のセルに自分自身が置かれているか」を調べる必要はないので26行目のif文でインデックスが自身のインデックスである場合は判定処理を飛ばしているわけである。
[CollisionTest_vs_Cargo(..)]
..
..
    for(int i=0; i<Cargo.Length; i++)
    {
        if(i == crg_idx) { continue; }

        CellLoc crgLoc = GetCellLocation(Cargo[i].GetPosition());
        if (nextLoc.row == crgLoc.row && nextLoc.col == crgLoc.col)
        {
            return i;
        }
    }
..

まとめると、次の3つのケースのいずれかの場合には移動キーを押してもSquareの移動は発生しない(セル移動処理がキャンセルされる)。

Squareが単独で移動している間に
        (1)  SquareがBlockに衝突する場合
SquareがCargoを押しながら移動している間に
        (2)  CargoがBlockに衝突する場合
        (3)  Cargoが別のCargoに衝突する場合

今までに述べたことをすべて実装したプログラムを以下に示す。
[Code4]  (実行結果 図21)
if (i_MOVE) // セル移動処理
{
    i_moveCount++;
    Vector2 newSqrPos = Square.GetPosition() + i_moveVtr;

    THMatrix3x3 R = TH2DMath.GetRotation3x3(i_degSquare);
    THMatrix3x3 T = TH2DMath.GetTranslation3x3(newSqrPos);
    THMatrix3x3 M = T * R;
    Square.SetMatrix(M);

    if (i_pushCargoIndex >= 0)
    {
        Vector2 newCrgPos = Cargo[i_pushCargoIndex].GetPosition() + i_moveVtr;
        Cargo[i_pushCargoIndex].SetPosition(newCrgPos);
    }

    if (i_moveCount == c_maxMoveCount)
    {   // あるセルに到達
        i_MOVE = false;
    }

    return;
}


if (Input.GetKeyDown(KeyCode.H))    // 左のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(-i_moveSpeed, 0.0f);
    i_degSquare = 90;
}
else if (Input.GetKeyDown(KeyCode.L))    // 右のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(i_moveSpeed, 0.0f);
    i_degSquare = 270;
}
else if (Input.GetKeyDown(KeyCode.J))    // 下のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(0.0f, -i_moveSpeed);
    i_degSquare = 180;
}
else if (Input.GetKeyDown(KeyCode.K))    // 上のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(0.0f, i_moveSpeed);
    i_degSquare = 0;
}
else 
{
    // キーが押されていないときは、以降の処理は行わない
    return;
}


// Square vs Cargo
Vector2 posSqr = Square.GetPosition();
i_pushCargoIndex = CollisionTest_vs_Cargo(posSqr, i_moveVtr);

bool CANCEL_MOVE = false;
if(i_pushCargoIndex < 0) // Squareが単独で移動
{
    // (1) Square vs Block
    CANCEL_MOVE = CollisionTest_vs_Block(posSqr, i_moveVtr);
}
else // SquareがCargoを押しながら移動
{
    Vector2 posCrg = Cargo[i_pushCargoIndex].GetPosition();

    // (2) Cargo vs Block
    CANCEL_MOVE = CollisionTest_vs_Block(posCrg, i_moveVtr);

    if (!CANCEL_MOVE)
    {
        // (3) Cargo vs Cargo
        int collIdx = CollisionTest_vs_Cargo(posCrg, i_moveVtr, i_pushCargoIndex);
        if (collIdx >= 0)
        { 
            CANCEL_MOVE = true; 
        }
    }
}


if (CANCEL_MOVE)
{
    i_MOVE = false;
    THMatrix3x3 R = TH2DMath.GetRotation3x3(i_degSquare);
    THMatrix3x3 T = TH2DMath.GetTranslation3x3(posSqr);
    THMatrix3x3 M = T * R;
    Square.SetMatrix(M);
}


図21 Code4 実行結果
58行目まではセル移動処理とキー操作の部分であり、ここまではCode3と同じである。
いずれかの移動キーが押されれば59行目以降に処理が進むが、まず Squareの進もうとしているセルにCargoがあるかどうかを調べる(62~63行目)。66行目から87行目は先ほど述べたセル移動処理をキャンセルする3つのケースのいずれかに該当するかを調べる部分である。この部分で CANCEL_MOVEtrueにならなければ次のフレームからセル移動処理が開始される。
キャンセルされる場合は90行目のif文に入り、(進もうとしていた方向に)Squareの向きだけを変化させる処理を行う (位置は変化しない)。


# Code5
図22 Mark
では最後に「倉庫番」のルールを説明する。
今までに使われていたセルは Floor と Block の2種類であったが、この他に図22に示される「Mark」というセルを追加する。Mark は Floor と同じように、その上を移動することのできるセルである。

ユーザーの動かす Square、及びステージ上に用意されている Cargo、Block、Floor、Mark の5種類のオブジェクトを使って、このゲームは次のように行われる。
ゲーム開始時点ではこの5種類のオブジェクトがステージ上に適当に配置されている。そして、ステージには Mark の個数と同じ数の Cargo が用意されている。
このゲームにおいてユーザーが行うべきことは、すべての Cargo をすべての Mark の上まで移動させることである。

図23  ステージ上の3つのCargoを3つのMarkの位置まで移動させる
図24 CargoをMarkの位置まで移動させると水色に変わる

例えば 図23のようなステージの場合、3つのCargoと3つのMarkが用意されているが、3つのCargoを適当に移動させて右図24のように すべてのCargoがすべてのMarkの上に置かれた時点でゲーム完了である (図24に示されるようにCargoをMarkの上に移動させると、Cargoの色が水色に変化する)。

プログラムは次のとおり。
[Code5]  
if (!i_INITIALIZED)
{
    SetupStage(0);

    i_INITIALIZED = true;
} 


if (i_MOVE)  // セル移動処理
{
    i_moveCount++;
    Vector2 newSqrPos = Square.GetPosition() + i_moveVtr;

    THMatrix3x3 R = TH2DMath.GetRotation3x3(i_degSquare);
    THMatrix3x3 T = TH2DMath.GetTranslation3x3(newSqrPos);
    THMatrix3x3 M = T * R;
    Square.SetMatrix(M);

    if (i_pushCargoIndex >= 0)
    {
        Vector2 newCrgPos = Cargo[i_pushCargoIndex].GetPosition() + i_moveVtr;
        Cargo[i_pushCargoIndex].SetPosition(newCrgPos);
    }

    if (i_moveCount == c_maxMoveCount)
    {   // あるセルに到達
        i_MOVE = false;

        // Cargoの色を変更する処理
        if (i_pushCargoIndex >= 0)
        {
            CellLoc loc = GetCellLocation(Cargo[i_pushCargoIndex].GetPosition());

            if (i_cellInfo[loc.row, loc.col] == 2) // CargoがMarkに移動した場合
            {    
                if(i_CargoState[i_pushCargoIndex] == c_GREEN)
                {
                    i_CargoState[i_pushCargoIndex] = c_BLUE;
                    SetCargoColor(i_pushCargoIndex, c_BLUE);        
                }
            }
            else // CargoがFloorに移動した場合
            {
                if(i_CargoState[i_pushCargoIndex] == c_BLUE)
                {
                    i_CargoState[i_pushCargoIndex] = c_GREEN;
                    SetCargoColor(i_pushCargoIndex, c_GREEN);        
                }
            }
        }
    }

    return;
}


if (Input.GetKeyDown(KeyCode.H))    // 左のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(-i_moveSpeed, 0.0f);
    i_degSquare = 90;
}
else if (Input.GetKeyDown(KeyCode.L))    // 右のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(i_moveSpeed, 0.0f);
    i_degSquare = 270;
}
else if (Input.GetKeyDown(KeyCode.J))    // 下のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(0.0f, -i_moveSpeed);
    i_degSquare = 180;
}
else if (Input.GetKeyDown(KeyCode.K))    // 上のセルへ進む
{
    i_MOVE = true;
    i_moveCount = 0;
    i_moveVtr = new Vector2(0.0f, i_moveSpeed);
    i_degSquare = 0;
}
else 
{
    // キーが押されていないときは、以降の処理は行わない
    return;
}


// Square vs Cargo
Vector2 posSqr = Square.GetPosition();
i_pushCargoIndex = CollisionTest_vs_Cargo(posSqr, i_moveVtr);

bool CANCEL_MOVE = false;
if(i_pushCargoIndex < 0)  // Squareが単独で移動
{
    // Square vs Block
    CANCEL_MOVE = CollisionTest_vs_Block(posSqr, i_moveVtr);
}
else  // SquareがCargoを押しながら移動
{
    Vector2 posCrg = Cargo[i_pushCargoIndex].GetPosition();

    // Cargo vs Block
    CANCEL_MOVE = CollisionTest_vs_Block(posCrg, i_moveVtr);

    if (!CANCEL_MOVE)
    {
        // Cargo vs Cargo
        int collIdx = CollisionTest_vs_Cargo(posCrg, i_moveVtr, i_pushCargoIndex);
        if (collIdx >= 0)
        { 
            CANCEL_MOVE = true; 
        }
    }
}


if (CANCEL_MOVE)
{
    i_MOVE = false;
    THMatrix3x3 R = TH2DMath.GetRotation3x3(i_degSquare);
    THMatrix3x3 T = TH2DMath.GetTranslation3x3(posSqr);
    THMatrix3x3 M = T * R;
    Square.SetMatrix(M);
}


Code4からは2つの処理が追加されている (1~6行目 及び 29~50行目)。
第1に冒頭のif文においてステージの初期化を行っている。SetupStage(..) はステージの初期化を担当するメソッドで、引数にはステージ番号を指定する。ここでは 4つのステージが用意されており、引数に指定するステージ番号は $0$ から $3$ である。プログラムにおいては $0$ がセットされているが、この場合には上図23の ステージ0 が用意される。
どのステージにおいても用意されているCargo及びMarkの個数は3つずつであり、3つのCargoを3つのMarkまで移動させればゲーム完了である (どのステージも難しいものではない)。

Code4から追加されている第2の処理は、ゲーム実行中のCargoの色の変更処理である。
先程の図24に見られるように、CargoがMarkに移動するとCargoの色は緑色から水色に変化する。この処理はセル移動処理(1行目のif文)内において行われるが、それは30行目から50行目であり、この部分がCode4から追加された部分である。
Cargoの色の変更はCargoが新しいセルに移動した時点で発生するので、移動が完了したフレームにおいてのみ色変更が必要かを調べ、必要であれば処理を行えばよい。このセル移動処理のif文は20フレームかけて次のセルまでの移動を行うものであるが、移動が終わったフレームでは i_moveCount の値が$20$になっているので、そのフレームにおいて 25行目のif文に入ることになるが、このとき「Cargoを押しながら移動」であった場合 さらに 30行目のif文へ進みそこでCargoの色変更処理が行われる。
具体的には、Cargoの色変更は以下のように行われる。
    --> CargoがMarkに移動した場合
        --> その時点でのCargoの色が緑であれば水色にする。
    --> CargoがFloorに移動した場合
        --> その時点でのCargoの色が水色であれば緑にする。

実際、プログラムの34行目から49行目は上記の場合分けをそのまま実装しただけであり、それはコードを見れば明らかであろう。
32行目においてまずCargoの現在のロケーションを取得する (ここで取得されるロケーションは今Cargoが移動した新しいセルのロケーションである)。34行目以下では、その新しいセルがMarkなのかFloorなのかを調べ、上に示す条件に一致した場合にCargoの色を緑あるいは水色に変更する。
38行目や46行目で使われている i_CargoState は現在のCargoの色を保持するインスタンス変数である。これは bool 型の配列であり、要素数はCargoの数に等しい。各要素は各Cargoが現在何色なのかを保持している。ここにセットされる値は c_GREEN または c_BLUE であり、ゲーム開始時点ではすべてのCargoは緑なので各要素の初期値は c_GREEN である。
39行目や47行目の SetCargoColor(..) は、第1引数に指定されるインデックスのCargoを第2引数で指定される色に変更するための補助的なメソッドである。色の変更が起こり得るCargoは、Squareが今押しているCargoであるから このメソッドの第1引数には i_pushCargoIndex がセットされている。

今回のプログラムではキー操作のメソッドがすべて GetKeyDown(..) であるため、Squareを移動させるためにはキーを1回1回押さなければならない。これはゲームの進行をいささか面倒にしてしまう。
しかし、この点については簡単に解決できる。プログラム中の GetKeyDown(..) を、キーの長押しに対応する GetKey(..) に変更すればよいだけである。このように変更すれば、ある方向にSquareを移動させる際にはその方向のキーを押していればよい (1回1回キーを押す必要はない)。

なお、本節で作成した倉庫番のプログラムはいくつかの変更をするだけで、簡単に「迷路ゲーム」に変えることができる。実際 2-27節では迷路内の移動を実装するが、そこで使われるプログラムは本節のCode2と同じものである。


最後にもう一点。オリジナルステージの作成について簡単に触れておく。
もし 読者がオリジナルステージを作成する場合には、プログラム冒頭の初期化ブロックにおいて以下の3つの設定を行えばよい。
    (1) ステージのセル情報
    (2) ゲーム開始時点でのSquareの位置
    (3) ゲーム開始時点でのCargoの位置

具体的には以下のように記述すればよい。
if (!i_INITIALIZED)
{
    // ステージのセル情報
    int[,] cellInfo = new int[,]    // Stage Cell : 9 x 9
    {
        {0, 0, 0, 0,  0,  0, 0, 0, 0},
        {0, 2, 2, 2,  1,  2, 2, 2, 0},
        {0, 1, 1, 1,  1,  1, 1, 1, 0},
        {0, 1, 1, 1,  1,  1, 1, 1, 0},
        {0, 1, 1, 1,  1,  1, 1, 1, 0},
        {0, 1, 1, 1,  1,  1, 1, 1, 0},
        {0, 1, 1, 1,  1,  1, 1, 1, 0},
        {0, 1, 1, 1,  1,  1, 1, 1, 0},
        {0, 0, 0, 0,  0,  0, 0, 0, 0}
    };
    SetStageData(cellInfo);

    // ゲーム開始時点でのSquareの位置
    SetSquareStartLocation(new CellLoc(7, 1), 1);  // 第2引数 : 0 上, 1 右, 2 下, 3 左

    // ゲーム開始時点でのCargoの位置
    CellLoc[] locs = {  new CellLoc(4, 1), new CellLoc(4, 2), new CellLoc(4, 3),
                        new CellLoc(4, 5), new CellLoc(4, 6), new CellLoc(4, 7)
                        };
    AddCargo(locs);

    i_INITIALIZED = true;
}


図25 上のプログラムによるステージ設定
初期化ブロック内で使われているメソッド SetStageData(..)SetSquareStartLocation(..)AddCargo(..) は本節限定の補助的メソッドである。
本節中でも述べたが、ステージは$9$行$9$列、$81$個のセルで構成される。ステージのセル情報はint型の2次元配列に定義するが、$0$ がBlock、$1$ がFloor、$2$ がMarkを表す。
2次元配列にステージのセル情報を定義した後は、プログラム16行目のように その配列を SetStageData(..) の引数にセットして実行すれば、2次元配列に定義した通りのステージが表示される。上記の設定では、ステージの外側はBlockで囲まれ、ステージの1行目に6個のMarkが配置されることになる。
ゲーム開始時点でのSquareの位置は、SetSquareStartLocation(..) を使用する。このメソッドの第1引数は開始時点でのSquareのロケーション(何行何列目に置くか)を CellLoc型で指定し、第2引数は開始時点でのSquareの向きを int型で指定する (向きは $0$ が上、$1$ が右、$2$ が下、$3$ が左を表す)。上のプログラムの設定では、Squareはゲーム開始時点において $7$行$1$列目に置かれ、そのときの向きは右を向いている。
ゲーム開始時点でのCargoの位置は AddCargo(..) に、そのステージで使用する全てのCargoの開始位置を CellLoc型の配列としてセットすればよい (22~25行目)。ここでの設定では、6個のCargoをステージ内の4行目に配置している。今回はMarkの数が6個であるから、Cargoの数も同じく6にしなければならない。
この初期化によって、プログラム開始時点では図25のように表示される。

(注意 : Mark 及び Cargo の個数の上限は 6 である。また、ステージの初期化のために使われる上記の SetStageData(..)AddCargo(..) はエラー処理を含んでいない。例えば、ステージのセル情報を定義する2次元配列が $9\times9$でない場合や、MarkとCargoの個数が一致しない場合、開始時点でのSquareやCargoのロケーションがステージの外側である場合、あるいはMark、Cargoの個数が6を上回るといった場合のエラー処理は行われない)














© 2020-2024 Redpoll's 60 (All rights reserved)