Redpoll's 60
 Home / 3Dプログラミング入門 / 第2章 $§$2-12
第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-12 指定方向へのオブジェクトの移動 5


前節では、図2のオブジェクト Shellをヘリコプターから発射するプログラムを作成したが、本節では Shellを発射するオブジェクトとして、2-9節で使用した戦車(図1)を用いる。本節のプログラムの内容は 2-9節、及び 前節のものとほとんど同じであるので、解説については必要な部分に制限して進めていく。

図1 戦車
図2 Shell 初期状態


# Code1
図1の戦車は次の2つのオブジェクト Body、Barrelを一体化したものであった。

図3 Body 初期状態
図4 Barrel 初期状態

ここでは、まず キー操作によって Barrelを回転させ、Barrelの回転中に Shellが常に Barrelの先端に置かれるようなプログラムを作成する。
キー操作は以下のとおり。
D  :  Barrelが反時計周りに回転(回転は$4$°ずつ)。
F  :  Barrelが時計周りに回転。

[Beta1]  (実行結果 図5)
if (Input.GetKey(KeyCode.D))
{
    i_degBarrel += 4;
}
else if (Input.GetKey(KeyCode.F))
{
    i_degBarrel -= 4;
}

THMatrix3x3 rotBarrel = TH2DMath.GetRotation3x3(i_degBarrel);
THMatrix3x3 localBarrel = rotBarrel;

THMatrix3x3 localBody = THMatrix3x3.identity;

THMatrix3x3 worldBarrel = localBody * localBarrel;
THMatrix3x3 worldBody = localBody;

Barrel.SetMatrix(worldBarrel);
Body.SetMatrix(worldBody);

// Shell
Vector2 direBarrel = rotBarrel * new Vector3(0, 1, 1);
Vector2 startPos = 1.52f * direBarrel;
OneShell.SetPosition(startPos);

(本節においては最後のプログラム以外のプログラムでは Shellは「OneShell」という名前で使われている)

  • 図5 Beta1 実行結果
  • 図6 戦車が移動していない状態での Shellの発射開始位置 (原点から1.52だけ離れている)
  • 図7 Barrelを45度回転させたときの様子

プログラム中で使用されている変数 i_degBarrel は Barrelの回転角度を表すインスタンス変数である。
実行結果(図5)に見られるように、回転中の Barrelの先端に常に Shellが置かれるようになる (Barrelの先端が戦車の場合における、Shellの発射開始位置である)。図3、図4に示されるように、Body、Barrelは初期状態では原点に位置し、その向きはy軸プラス方向を向いている。Barrelが回転していない状態で、Shellを Barrelの先端に移動させたときの様子が図6であるが、このとき、Shellは原点からy軸プラス方向に $1.52$だけ移動している 。
Barrelを回転させたときに Shellが Barrelの先端に置かれるようにするためには、図6の状態から Barrelと同じだけ Shellを回転させればよい。例えば、図7は Barrelを$45$°回転させたときの様子であるが、Shellも図6の位置から$45$°回転しているため回転後においても Shellは Barrelの先端に置かれている。
プログラム22行目の direBarrel は、その時点での Barrelの向きを表す Vector2型変数であり、23行目の startPos は、その時点での Barrelの先端の位置座標、すなわち、Shellの置かれる位置(発射開始位置)を表すVector2型変数である。


では、Aキーを押すことで、Shellが Barrelの向いている方向へ発射されるプログラムを作成する。ここでの発射は単発の発射であり、発射するためにはAキーを1回1回押さなければならない。

[Code1]  (実行結果 図8)
if (Input.GetKey(KeyCode.D))
{
    i_degBarrel += 4;
}
else if (Input.GetKey(KeyCode.F))
{
    i_degBarrel -= 4;
}

THMatrix3x3 rotBarrel = TH2DMath.GetRotation3x3(i_degBarrel);
THMatrix3x3 localBarrel = rotBarrel;

THMatrix3x3 localBody = THMatrix3x3.identity;

THMatrix3x3 worldBarrel = localBody * localBarrel;
THMatrix3x3 worldBody = localBody;

Barrel.SetMatrix(worldBarrel);
Body.SetMatrix(worldBody);

// Shell
Vector2 direBarrel = rotBarrel * new Vector3(0, 1, 1);
if (Input.GetKeyDown(KeyCode.A))
{
    Vector2 startPos = 1.52f * direBarrel;
    OneShell.SetPosition(startPos);
    
    OneShell.direction = direBarrel;
}
else
{
    Vector2 newShellPos = OneShell.GetPosition() + 0.25f * OneShell.direction;
    OneShell.SetPosition(newShellPos);
}

図8 Code1 実行結果
19行目までは Beta1と同じである。それ以降の Shellの発射部分に関しては前節の Shellの単発発射処理のコードとほとんど同じである (例えば前節のCode2)。前節では、その時点での Bodyの向きに Shellが発射されたが、今回はその時点での Barrelの向きに発射されるのでその点で若干の違いがある。23行目の ifブロックは Shellを発射開始位置へ移動させる処理であり、Aキーが押されるたびにこの ifブロックに入るので、ShellがどこにいてもAキーが押されれば Shellは発射開始位置(その時点での Barrelの先端)へ移動することになる。28行目の OneShell.direction は前節でも解説したが、Shellが発射された方向を保持する Vector2型のプロパティである。
30行目の elseブロックは、毎フレーム Shellを一定距離ずつ進行させる部分であり、進行方向は OneShell.directionの表す方向、すなわち Shellの発射されたときの方向である。Aキーが押されなければ、この30行目の elseブロックに毎フレーム処理が移るため、Shellは表示領域の外側に移動しても指定の方向へ移動し続ける (計算が行われるだけで描画されることはない)。

# Code2
続いて、Barrelの回転だけでなく Bodyの回転も含めた場合において、Shellの発射開始位置を求めてみよう。

図9 Barrelを110°回転させたときの様子
図10 図9の状態からBodyを-50°回転させたときの様子

以下では便宜上、y軸プラス方向から$N$°回転させた方向を「$N$°の方向」と表現する。

今、2つのオブジェクトを一体化して ShellをBarrel先端の発射開始位置に置いた状態(図6)から Barrelを$110$°回転させたとしよう。Barrelを回転させる前の状態(図6)では、Shellは原点からy軸プラス方向に$1.52$だけ離れた位置 $(0.0,\ 1.52)$ に置かれていた。Shellをその位置から Barrelとともに$110$°回転させると、回転後においても Shellは Barrelの先端に位置し、原点からの距離は$1.52$のままである。図9は、このときの様子を示している (図中の赤い点線は Bodyの向きを表すものである)。
この状態において、さらに Bodyを $-50$°回転させたときの様子が図10である。Bodyを回転させると、Bodyにアタッチされている子オブジェクトの Barrelも Bodyと同じだけ回転することになる。したがって、ここでは Barrelも Bodyとともに $-50$°回転するが、同じように Shellも回転させれば、今回の回転後においても Shellは Barrelの先端に位置することになる (原点からの距離も変わらず $1.52$のままである)。
Bodyに$-50$°の回転を行う前の Body向きは、図9に示されるようにy軸プラス方向である。$-50$°の回転を実行した後は、図10に示されるように Bodyの向きを表す赤い点線は(y軸プラス方向から)$-50$°の方向を向いている。一方で、Barrelの向きは$60$°の方向を向く結果になった。
したがって、Bodyを回転させた後の Barrel先端の Shell発射開始位置は、$60$°の方向に原点から$1.52$だけ離れた位置ということになる。

さらに、もう1つ例を見てみよう。

図11 Barrelを-90°回転させたときの様子
図12 図11の状態からBodyを45°回転させたときの様子

図11は、Barrelのみを$-90$°回転させたときの様子である。図12は、図11の状態から Bodyを$45$°回転させたときの様子である。Bodyを回転させるときには、Barrelも一体となって回転するため Barrelの向きも$45$°回転し、結果的には(y軸プラス方向から)$-45$°の方向を向くことになる。したがって、Bodyを回転させた後の Barrel先端の Shell発射開始位置は、この場合においては$-45$°の方向に原点から$1.52$だけ離れた位置ということになる。

Barrelの回転角度を i_degBarrel、Bodyの回転角度を i_degBody とすれば、上の例から Barrel、Bodyの両方が回転している場合の最終的な Barrelの(y軸プラス方向を基準とした)回転角度は (i_degBarrel+i_degBody) であり、そのときの Barrel先端の Shell発射開始位置は角度(i_degBarrel+i_degBody)の方向に原点から$1.52$だけ離れた位置になるということがわかる。

以上の結果をふまえて、まずは キー操作によって Barrelと Bodyを回転させ、回転中に Shellが常に Barrelの先端に置かれるようなプログラムを作成する。
使用するキーは以下のとおり。
H  :  Bodyが反時計周りに回転(回転は$2$°ずつ)。
L  :  Bodyが時計周りに回転。
D  :  Barrelが反時計周りに回転(回転は$4$°ずつ)。
F  :  Barrelが時計周りに回転。

[Beta2]  (実行結果 図13)
if (Input.GetKey(KeyCode.H))
{
    i_degBody += 2;
}
else if (Input.GetKey(KeyCode.L))
{
    i_degBody -= 2;
}

if (Input.GetKey(KeyCode.D))
{
    i_degBarrel += 4;
}
else if (Input.GetKey(KeyCode.F))
{
    i_degBarrel -= 4;
}

THMatrix3x3 rotBarrel = TH2DMath.GetRotation3x3(i_degBarrel);
THMatrix3x3 localBarrel = rotBarrel;

THMatrix3x3 rotBody = TH2DMath.GetRotation3x3(i_degBody);
THMatrix3x3 localBody = rotBody;

THMatrix3x3 worldBarrel = localBody * localBarrel;
THMatrix3x3 worldBody = localBody;

Barrel.SetMatrix(worldBarrel);
Body.SetMatrix(worldBody);

// Shell
THMatrix3x3 R2 = TH2DMath.GetRotation3x3(i_degBarrel + i_degBody);
Vector2 direBarrel = R2 * new Vector3(0, 1, 1);
Vector2 startPos = 1.52f * direBarrel;
OneShell.SetPosition(startPos);

このプログラムでは、Bodyの回転も加わるために Bodyの回転角度を表すインスタンス変数 i_degBody が追加されている。
32行目の$3\times3$行列 R2 は、上で述べた 角度(i_degBarrel+i_degBody)だけ回転させる回転行列であり、33行目の direBarrel はy軸プラス方向から角度(i_degBarrel+i_degBody)だけ回転させたときの方向である。この方向に 原点から$1.52$だけ離れた位置が Shell発射開始位置であり、それは34行目において startPosにセットされている。

では、Aキーを押すことで、Shellが Barrelの向いている方向へ発射されるプログラムを以下に示す (ここでも Shellの発射は単発である)。
[Code2]  (実行結果 図14)
if (Input.GetKey(KeyCode.H))
{
    i_degBody += 2;
}
else if (Input.GetKey(KeyCode.L))
{
    i_degBody -= 2;
}

if (Input.GetKey(KeyCode.D))
{
    i_degBarrel += 4;
}
else if (Input.GetKey(KeyCode.F))
{
    i_degBarrel -= 4;
}

THMatrix3x3 rotBarrel = TH2DMath.GetRotation3x3(i_degBarrel);
THMatrix3x3 localBarrel = rotBarrel;

THMatrix3x3 rotBody = TH2DMath.GetRotation3x3(i_degBody);
THMatrix3x3 localBody = rotBody;

THMatrix3x3 worldBarrel = localBody * localBarrel;
THMatrix3x3 worldBody = localBody;

Barrel.SetMatrix(worldBarrel);
Body.SetMatrix(worldBody);

// Shell
THMatrix3x3 R2 = TH2DMath.GetRotation3x3(i_degBarrel + i_degBody);
Vector2 direBarrel = R2 * new Vector3(0, 1, 1);
if (Input.GetKeyDown(KeyCode.A))
{
    Vector2 startPos = 1.52f * direBarrel;
    OneShell.SetPosition(startPos);
    
    OneShell.direction = direBarrel;
}
else
{
    Vector2 newShellPos = OneShell.GetPosition() + 0.25f * OneShell.direction;
    OneShell.SetPosition(newShellPos);
}

このプログラムは33行目までは Beta2と同じであり、34行目以降は Code1の23行目以降と同じである。このプログラムでは、Bodyの回転処理が加わっているため、その分の記述が追加されているだけである。また、Barrelの向きの計算(32、33行目)は、Beta2と同様に Bodyの回転も考慮したものになっている。

図13 Beta2 実行結果
図14 Code2 実行結果


# Code3
では最後に、戦車の移動中、あるいは 移動した地点において Shellを発射するプログラムを作成する。
Barrelは Bodyにアタッチされているので、戦車の移動は実際には Bodyを移動させることによって実現される。今までのプログラムでは Bodyの位置は原点から動くことはなかった。これは言い方を変えれば、毎フレームの移動量が $(0, 0)$ であったのである。プログラム実行中に Bodyを連続的に移動させるためには、毎フレームの Bodyの移動量を変化させればよい。
簡単に言えば、今回作成するプログラムは、今までのプログラムに毎フレーム平行移動が加わっただけのものである。

2-9節 Code7では 一体化した Bodyと Barrelを連続的に移動させるプログラムを作成したが、図15はその実行結果である。図16は、図15の最初のいくつかのフレームにおいて、オブジェクトに実行される変換をアニメーションとして表したものである。

図15  2-9節 Code7 実行結果
図16

図16では、赤いフィルターがかかっている状態から通常色の状態に変化するが、これは通常色の状態においてフレーム描画が発生することを表している。図16に示されるように、オブジェクトが連続的に移動する場合でも、毎フレーム まず始めに初期状態の位置において指定の方向に向きを変化させ、向きを変えた状態で指定の位置に平行移動していることがわかる。この平行移動の量を newP とすれば、Bodyと Barrelが一体化して移動する際には、毎フレーム 初期状態の位置から Bodyと Barrelは newPだけ移動しているのである。したがって、各フレームでの移動後の位置における Barrel先端の位置、すなわち Shellの発射開始位置を求める場合でも、移動前の位置における Barrel先端の位置を求め、その位置を newP だけ移動させればよい。図17に示されているのは、この過程である。図17における青色の矢印が移動量 newP を表しており、緑色の矢印は Barrelの向き、すなわち Shellの発射される方向を表している。
「移動前の位置における Barrel先端の位置」は Code2で解説した方法で求めればよいから、求めた値に newPを加算すれば「移動後の位置における Barrel先端の位置」が得られるわけである。

図17
図18 Beta3 実行結果

次のプログラムは、キー操作によって Bodyの移動を可能にし、Bodyの移動中 常にBarrelの先端にShellが置かれるようにしたものである。
使用するキーは以下のとおり。
H  :  Bodyが反時計周りに回転(回転は$2$°ずつ)。
L  :  Bodyが時計周りに回転。
D  :  Barrelが反時計周りに回転(回転は$4$°ずつ)。
F  :  Barrelが時計周りに回転。
S  :  Bodyの移動/停止用スイッチ。停止中に押すと、Bodyの向いている方向へ一定速度で移動を開始する。再度キーを押すまでこの移動は続く。

[Beta3]  (実行結果 図18)
if (Input.GetKey(KeyCode.H))
{
    i_degBody += 2;
}
else if (Input.GetKey(KeyCode.L))
{
    i_degBody -= 2;
}

if (Input.GetKey(KeyCode.D))
{
    i_degBarrel += 4;
}
else if (Input.GetKey(KeyCode.F))
{
    i_degBarrel -= 4;
}

if (Input.GetKeyDown(KeyCode.S))   // 移動/停止
{
    i_MOVE = !i_MOVE;
}

// localBarrel
THMatrix3x3 rotBarrel = TH2DMath.GetRotation3x3(i_degBarrel);
THMatrix3x3 localBarrel = rotBarrel;

// localBody
THMatrix3x3 rotBody = TH2DMath.GetRotation3x3(i_degBody);
Vector2 direBody = rotBody * new Vector3(0, 1, 1);

Vector2 curP = Body.GetPosition();
Vector2 newP = (i_MOVE) ? curP + 0.05f * direBody : curP;
THMatrix3x3 traBody = TH2DMath.GetTranslation3x3(newP);
THMatrix3x3 localBody = traBody * rotBody;

// world matrix
THMatrix3x3 worldBarrel = localBody * localBarrel;
THMatrix3x3 worldBody = localBody;

Barrel.SetMatrix(worldBarrel);
Body.SetMatrix(worldBody);

// Shell
THMatrix3x3 R2 = TH2DMath.GetRotation3x3(i_degBarrel + i_degBody);
Vector2 direBarrel = R2 * new Vector3(0, 1, 1);
Vector2 startPos = newP + 1.52f * direBarrel;
OneShell.SetPosition(startPos);

このプログラムは42行目までは 2-9節 Code7と同じである。さらに、44行目以降は本節 Beta2の31行目以降と1箇所を除き同じである。Beta2と違い、ここでは毎フレーム平行移動が発生するので47行目では、上で述べた移動ベクトル newP を加算する処理が追加されている。実行結果(図18)を見れば分かるように、戦車の移動中 常に Barrelの先端に Shellが置かれるようになっている。

では、戦車の移動中、あるいは 移動した地点において Shellを発射するプログラムの最終的な実装を以下に示す。このプログラムにおいては Shellの発射は単発ではなく、複数個の Shellの使い回しによる連射として実装されている。
使用するキーは Beta3と同じであるが、Shell発射のためのAキーの処理が追加されている。
A  :  Shellの発射 (長押しによって連射になる)。

[Code3]  (実行結果 図19)
i_frameCount++;

if (Input.GetKey(KeyCode.H))
{
    i_degBody += 2;
}
else if (Input.GetKey(KeyCode.L))
{
    i_degBody -= 2;
}

if (Input.GetKey(KeyCode.D))
{
    i_degBarrel += 4;
}
else if (Input.GetKey(KeyCode.F))
{
    i_degBarrel -= 4;
}

if (Input.GetKeyDown(KeyCode.S))   // 移動/停止
{
    i_MOVE = !i_MOVE;
}

bool bShoot = false;
if (Input.GetKey(KeyCode.A))
{
    if (i_frameCount >= Barrel.lastShootFrame + 3)
    {
        bShoot = true;
        Barrel.lastShootFrame= i_frameCount;
    }
}

// localBarrel
THMatrix3x3 rotBarrel = TH2DMath.GetRotation3x3(i_degBarrel);
THMatrix3x3 localBarrel = rotBarrel;

// localBody
THMatrix3x3 rotBody = TH2DMath.GetRotation3x3(i_degBody);
Vector2 direBody = rotBody * new Vector3(0, 1, 1);

Vector2 curP = Body.GetPosition();
Vector2 newP = (i_MOVE) ? curP + 0.05f * direBody : curP;
THMatrix3x3 traBody = TH2DMath.GetTranslation3x3(newP);
THMatrix3x3 localBody = traBody * rotBody;

// world matrix
THMatrix3x3 worldBarrel = localBody * localBarrel;
THMatrix3x3 worldBody = localBody;

Barrel.SetMatrix(worldBarrel);
Body.SetMatrix(worldBody);

// Shell
THMatrix3x3 R2 = TH2DMath.GetRotation3x3(i_degBarrel + i_degBody);
Vector2 direBarrel = R2 * new Vector3(0, 1, 1);
for (int i = 0; i < Shell.Length; i++)
{
    if (Shell[i].active)
    {
        Vector2 newShellPos = Shell[i].GetPosition() + 0.80f * Shell[i].direction;
        if (Mathf.Abs(newShellPos.x) > 20 || Mathf.Abs(newShellPos.y) > 12)
        {
            Shell[i].active = false;
        }
        
        Shell[i].SetPosition(newShellPos);
    }
    else
    {
        if (bShoot)
        {
            Shell[i].active = true;
            Shell[i].direction = direBarrel;
            
            Vector2 startPos = newP + 1.52f * direBarrel;
            Shell[i].SetPosition(startPos);
            
            bShoot = false;
        }
    }
}

図19 Code3 実行結果
現在のフレーム番号を表す1行目のインスタンス変数 i_frameCount の更新と、26行目から34行目のAキー用のイベント処理が追加された点を除けば、このプログラムの54行目までは上のBeta3と同じである。
前節でも述べたが、Shellを発射するオブジェクトはカスタムライブラリの THAlphaObject2Dクラスのインスタンスである。本節では、Shellを発射するオブジェクトは Barrelであるが、Barrelも THAlphaObject2Dクラスのインスタンスである。THAlphaObject2Dクラスには、Shellを連射できるようにするために lastShootFrame という int型のプロパティが用意されている。ここでは 29行目の ifブロックにおいて使われているが、ここでの記述は Barrelが3フレームおきに Shellを連射できるようにするものである (lastShootFrameにはShellを最後に発射したときのフレーム番号がセットされる)。
なお、56行目以降は Shellの連射処理の部分であるが、この部分も前節のCode5の連射処理の部分(Code5の49行目以降)と内容はほとんど同じである。前節のCode5では、Shellの発射される方向は(ヘリコプターの)Bodyの向きであったので、連射処理の部分においては Shellの発射される方向として Bodyの方向を表す direBody が使われていたが、ここでは、Shellの発射される方向は(戦車の)Barrelの向きであるので、このプログラムの連射処理の部分では Barrelの方向を表す direBarrel が使われている。












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