Redpoll's 60
 Home / 3Dプログラミング入門 / 第2章 $§$2-11
第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-11 指定方向へのオブジェクトの移動 4 (連射プログラムの実装)


2-9節、2-10節では2D形状の戦車、及び ヘリコプター(図1)を使用して、キー操作によるオブジェクトの運動について学習した。本節と次節では、これらのオブジェクトから弾を発射する問題について考える。

図2の原点に置かれている小さい円形オブジェクトは、弾を表すオブジェクトShellである。本節では、この Shellを用いて、まずは ヘリコプターからの Shellの発射について考える。

図1 ヘリコプター
図2 Shell 初期状態

# Code1
図1のヘリコプターは、以下の2つのオブジェクト Body、Rotorが一体化したものであるが、最初のプログラムでは、Bodyの初期状態の向きであるy軸プラス方向へ Shellを発射する。

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

発射のタイミングはキー操作によるものとし、具体的には、Aキーが押されるごとに Shellが Body先端の指定位置からy軸プラス方向に毎フレーム一定速度で移動を行うようにする (ここでは Aキーの長押しには対応していない。Shellを発射するためにはAキーを1回1回押す必要がある)。

[Code1]  (実行結果 図6)
THMatrix3x3 rotRotor = TH2DMath.GetRotation3x3(45);
THMatrix3x3 traRotor = TH2DMath.GetTranslation3x3(c_attachPos_Rotor);
THMatrix3x3 localRotor = traRotor * rotRotor;

THMatrix3x3 localBody = THMatrix3x3.identity;

THMatrix3x3 worldRotor = localBody * localRotor;
THMatrix3x3 worldBody = localBody;

Rotor.SetMatrix(worldRotor);
Body.SetMatrix(worldBody);

if (Input.GetKeyDown(KeyCode.A))
{
    Vector2 startPos = new Vector2(0.0f, 1.25f);
    OneShell.SetPosition(startPos);
}
else 
{
    Vector2 newShellPos = OneShell.GetPosition() + 0.25f * Vector2.up;
    OneShell.SetPosition(newShellPos);
}

1行目から11行目までは Body、Rotorの一体化の記述である。ここでは、1行目に示されるように Rotorの回転角度は毎フレーム$45$°であるため、常に$45$°回転した状態で表示される。Bodyは初期状態から動かさないので、identity行列が実行されている。
13行目以降が Shellの発射部分である。ただし、このプログラムでは図2のオブジェクト Shellを「OneShell」という名前で使っているので注意
13行目の ifブロックはAキーが押されたときの処理であり、18行目の elseブロックは押されていないときの処理である。ifブロックで行っていることは、Shellを (0.0, 1.25) だけ移動させるという処理である (Shellの初期状態での位置は原点なので、この移動によって (0.0, 1.25)に移動することになる)。図5は、(0.0, 1.25)に置かれた Shellである (ちょうど、Bodyの先端に置かれることになる)。つまり、Aキーを押すたびに図5に示される位置に Shellは移動することになる。
18行目の elseブロックはAキーが押されていない状態での処理であるが、ここでは Shellをy軸方向に毎フレーム $0.25$だけ移動させる処理を行っている。20行目では、GetPosition()によってShellの現在位置を取得し、その位置からy軸方向に $0.25$だけ進んだ位置を計算している。Vector2.upは、Unityに用意されている Vector2構造体のプロパティでy軸プラス方向である $(0, 1)$ を表している。
Aキーを押したとき以外は、18行目の elseブロックに入るので このプログラム実行中はほとんど常にこの elseブロックの処理が行われることになる。例えば、Shellが表示領域の外側へ移動しても、Aキーを押さない限り この elseブロックに処理が移るので、Shellは表示領域の外側で毎フレーム y軸方向に$0.25$ずつ移動し続ける。もちろん、その間ずっと Shellの描画は発生しない。また、Shellの発射開始位置からほとんど進んでいない状態で Aキーを押すと発射開始位置に戻るので、例えば Aキーを連打すると、Shellが途中で消えて開始位置に戻るということが繰り返される。

図5 Shell 発射開始位置 (原点からy軸プラス方向に1.25だけ離れている)
図6 Code1 実行結果

16行目、21行目では Shellを移動させるために SetPosition(..) というカスタムライブラリーのメソッドが使われている。今までのプログラムでは、オブジェクトの位置を移動させる際には次のように平行移動行列を使用していた。
// Objを(x, y)だけ平行移動させる
THMatrix3x3 T = TH2DMath.GetTranslation3x3(x, y);
Obj.SetMatrix(T);

上のコードはObjを (x, y) だけ平行移動させる処理である (正確には初期状態の位置から(x, y)だけの平行移動)。SetPosition()も平行移動行列と役割は同じである。SetPosition()を使用する場合は次のようにすればよい。
// Objを(x, y)だけ平行移動させる
Obj.SetPosition(x, y);

SetPosition(float x, float y)
  :  オブジェクトに対して初期状態の位置から (x, y) だけの平行移動を実行する。

あるいは、引数を Vector2型として指定することもできる。
SetPosition(Vector2 pos)
  :  オブジェクトに対して初期状態の位置から pos だけの平行移動を実行する。

図2に示されるように、Shellは初期状態で原点に置かれている。したがって、Shellに対して SetPosition(3, 4) を実行すると Shellの位置は $(3, 4)$ に移るが、これは Shellの初期状態の位置である原点から $(3, 4)$ だけ平行移動した結果である。もし、Shellの初期状態での位置が $(2, 2)$ である場合は、SetPosition(3, 4) を実行すると Shellの位置は $(5, 6)$ に移る ($(3, 4)$に移るわけではない)。SetPosition(x, y)は正確には (x, y) だけの平行移動であることに注意 (UnityのC#スクリプトの標準機能では、Obj.transform.localPositionObj.transform.positionVector3型の値をセットすることでオブジェクトを移動させるが、この処理も正確には初期状態の位置からの平行移動である)。


# Code2
次に、オブジェクトの向きを変えてオブジェクトの向いている方向に Shellを発射するプログラムを作成する。この例の場合では、オブジェクトの向きは前節と同じく、Bodyの向きを意味し、初期状態では図3に示されるようにy軸プラス方向を向いている。

例えば、Bodyを$75$°回転させた状態で Shellを発射することを考えよう。

図7 Bodyを75°回転させた状態
図8 Bodyの先端にある、Shellの発射開始位置は原点から1.25だけ離れている

図7は Bodyを$75$°回転させた状態であるが、図中の緑色の矢印は この状態における Bodyの向きを表している。したがって、オブジェクトの向いている方向に Shellを発射することは、緑色の矢印の方向に Shellを発射することと同じである。Shellを発射するために必要な情報は Body先端の Shellの位置(発射開始位置)、及び この状態での Bodyの向きであるが、それらは次のようにして求められる。
Bodyの向き、すなわち、緑色の矢印の向きについては簡単である (これは前節までにも何度か求めている)。Bodyの初期状態での向きは $(0, 1)$ であるが、それを $75$°回転させた値であるから、$75$°の回転を表す回転行列を $R_{75}$ とし、回転後の向きを $direBody$ とすれば、\[ R_{75}\begin{pmatrix}0 \\1 \\1\end{pmatrix}= direBody\]と計算される。ここでは同次座標で計算しているが、$direBody$をx座標、y座標のみの2次元ベクトルとして考えれば、$direBody$は $(0, 1)$ を$75$°回転させた方向を表す単位ベクトルである。
Shellの発射開始位置は、Bodyが回転をしていない状態では図5に示される位置であった。この位置の座標は $(0.0,\ 1.25)$ であるが、つまり、この位置は原点から 発射方向であるy軸方向に $1.25$離れているわけである。Bodyを回転させた場合でも同様に、発射開始位置は原点から 発射方向に $1.25$離れた位置となる (図8)。発射方向とは上で求めた緑色の矢印の方向(Bodyの向き)、すなわち、$direBody$のことであるから、発射開始位置を $startPos$ とすると、それは次のように計算される (下の計算における $direBody$は $(0, 1)$ を$75$°回転させた2次元の単位ベクトルとする)。\[ 1.25 * direBody = startPos \]では、実際に Bodyをキー操作によって回転させ、回転後の Bodyの向いている方向に Shellを発射するプログラムを作成する (このプログラムでも、Shellは「OneShell」という名前で使われている)。

使用するキーは以下のとおり。
H  :  Bodyの向きを反時計周りに回転 (回転は$1$°ずつ)。
L  :  Bodyの向きを時計周りに回転。
A  :  Shellの発射 (長押しには対応していない)。

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

THMatrix3x3 rotRotor = TH2DMath.GetRotation3x3(45);
THMatrix3x3 traRotor = TH2DMath.GetTranslation3x3(c_attachPos_Rotor);
THMatrix3x3 localRotor = traRotor * rotRotor;

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

THMatrix3x3 worldRotor = localBody * localRotor;
THMatrix3x3 worldBody = localBody;

Rotor.SetMatrix(worldRotor);
Body.SetMatrix(worldBody);

Vector2 direBody = rotBody * new Vector3(0, 1, 1);
if (Input.GetKeyDown(KeyCode.A))
{
    Vector2 startPos = 1.25f * direBody;
    OneShell.SetPosition(startPos);
    
    OneShell.direction = direBody;
}
else
{
    Vector2 newShellPos = OneShell.GetPosition() + 0.25f * OneShell.direction;
    OneShell.SetPosition(newShellPos);
}

1行目から21行目は、Bodyと Rotorを一体化して回転させる処理であり、この部分は前節の Code3とほとんど同じである (前節の Code3では Rotorは常に回転していたが、ここでは、Rotorは$45$°回転した状態で静止している)。23行目は、上記の $direBody$ の計算であり、上記の計算と内容は全く同じである。
図9 Code2 実行結果
24行目以降が、Shellの発射部分のコードであるが、この部分に関しても上の Code1とほとんど同じである。異なる点は以下の2点である。Code1では、Aキーが押されたときには常に Shellは (0.0, 1.25) だけの移動をしたが、ここでは、その時点での Bodyの向きである direBody の方向に $1.25$だけの移動をすることになる (26行目から27行目の処理)。これは、結果的に Shellが原点から Bodyの向きに $1.25$離れた位置へ移動することになるが、その位置が発射開始位置となるのである。もう1つの違いは、Code1では Aキーが押されていない状態では、Shellは常にy軸プラス方向に一定速度で進んでいったが、ここでは、発射時点での Bodyの向きに進んでいくことである。Aキーが押されたときに、24行目の ifブロックで Shellを発射開始位置に移動させるが、その後 29行目において OneShell.direction というプロパティに direBody の値がセットされている。
本節で使われているオブジェクト Shell は、カスタムライブラリの THShell2D クラスのインスタンスであり、THShell2Dクラスには、Shellの発射に関連したプロパティがいくつか用意されている。direction もそのうちの1つであり、以下のような役割を持っている。

direction
  :  THShell2Dクラスの Vector2型プロパティ。このプロパティは Shellの進行方向を表している (Shellの進行方向はそのShellが発射された方向のことである)。

この例では Shellが発射される際には、その時点での Bodyの向きに発射されるので 29行目の OneShell.direction には発射時点での Bodyの向きがセットされる。そして、次のフレームからそのShellは OneShell.direction の方向に向かって進んでいく。
Aキーが押されていない状態では31行目の elseブロックに入るが、ここでは OneShell.directionの方向に一定速度(毎フレーム $0.25$ずつ)で進ませる処理が行われる (33行目から34行目の処理)。再度Aキーが押されるまで、この elseブロックに入り続けるので、上でも述べたが Shellが表示領域の外側に移動して描画されなくなっても、Shellは同じ方向に進み続ける (進行中において Shellの進行方向は変化しない)。


# Code3
今までのプログラムでは、ヘリコプターは原点から移動することはなかったが、ここでは、キー操作によってヘリコプターの移動を可能とし、さらに、移動中に Shellの発射を行うプログラムを作成する。プログラムの内容的には、Code2に対して毎フレーム平行移動が加わるだけである。

前節の Code4では、一体化した Bodyと Rotorの連続的な移動を扱った。次のプログラムは前節の Code4の内容をより単純にして、Bodyの向きに、Bodyのみが移動を行うようにしたものである (ただし、このプログラムには移動/停止用のキー操作はないので、Bodyは実行中 常に動き続ける)。

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

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

Vector2 curP = Body.GetPosition();
Vector2 newP = curP + 0.125f * direBody;
THMatrix3x3 traBody = TH2DMath.GetTranslation3x3(newP);
THMatrix3x3 M = traBody * rotBody;

Body.SetMatrix(M);

  • 図10 Beta3 実行結果
  • 図11 Beta3 コマ送り表示
  • 図12

図10は、このプログラムの実行結果である。1フレームあたりの回転角度の変化量、及び 1フレームあたりの移動距離を前節のものよりも大きな値にしているので実行結果に見られるように やや速い運動になっている。図11は 実行結果の最初の20フレーム程をコマ送りで表示したものであり、図12は 最初のいくつかのフレームについて、Bodyに実行される変換行列の実行過程をアニメーションとして表したものである。
2D空間(XY平面)上を Bodyが移動する場合には、毎フレーム 初期状態の位置から、各フレームで計算された移動先の位置へ移動することになる。「計算された移動先の位置」とは上のプログラムにおける14行目の newP のことである。実際には、Bodyは初期状態の位置である原点から毎フレーム newP だけの平行移動を行うので、結果的に移動先の位置も newP になるのである (図12において白い文字で traBody と表示されている間に、Bodyの移動が行われているが、この移動が「newPだけの平行移動」である。原点から移動しているので移動先の位置も newPとなる)。そして、移動した時点でフレーム描画が発生し、描画されたフレームを連続的に表示すれば図10のような結果になるわけである。
ここでの Bodyの場合は、自身の先端の向きに進んでいくので、各フレームでは計算された位置に移動する前に、まず Bodyの向きを回転させ、Bodyが指定の向きに回転した状態で計算された位置への移動を行い、その時点でフレーム描画が発生する。つまり、毎フレーム Bodyに対しては初期状態から、まず 指定の方向への回転を実行し(プログラムにおける変換行列rotBody)、計算された位置への平行移動を実行する(traBody)。そして、それらの変換の実行結果が毎フレーム描画されるわけである。しかし、こういったことは本章において何度も繰り返し説明してきたことであるので、この点については問題はないであろう。
上に述べたことから、2D空間上を移動している Bodyの、ある時点における進行方向、つまり、その時点での Bodyの向きは、初期状態において Bodyを回転させた際の向きに等しいということがわかる。実際、ある時点における Bodyの位置と向きが次の図13のような状態であったとしよう。この状態になる過程は、上で述べたように、まず 初期状態の Bodyを回転させ、指定の向きに回転させた状態で、移動先の位置への平行移動を行うのである (図12参照)。図14は 平行移動を行う前の Bodyの状態、つまり、初期状態から回転だけを実行した状態である。図13と図14の違いは、Bodyの位置だけであり、Bodyの向き(緑色の矢印の方向)は両方とも同じである。図13の Bodyは、図14の状態から ただ位置を移動しただけであり、向いている方向は何も変化していない。つまり、「Bodyのある時点での進行方向は、初期状態において Bodyを回転させた際の向きに等しい」のである。

図13
図14

では、ここで Shellの発射について話を戻そう。Shellの発射のために必要な情報は、その時点での Bodyの向きと、Shellの発射開始位置である。今、Bodyが図15の状態において Shellを発射するとしよう。このときの、Bodyの向き、及び Shellの発射開始位置について考える。

図15
図16

Bodyの向きについては上の議論から明らかであり、初期状態において Bodyを回転させた方向である。上のプログラムBeta3でいえば、初期状態の向き $(0, 1)$ から角度i_degBodyだけ回転させた方向のことであり、その方向は11行目の direBody にセットされる。
Shellの発射開始位置を求めることも同様に簡単である。Code2では Bodyを回転させ、その時点での Bodyの向いている方向へ Shellを発射するプログラムを作成したが、そこでは Bodyの位置は原点から動くことはなかった。今回作成するプログラムは、Bodyの向いている方向へ Shellを発射する点では同じであるが、発射する位置が原点ではなく、その時点での Bodyの位置からの発射になるという点が異なる。「その時点での Bodyの位置」とは、Beta3でいえば 毎フレーム14行目で計算される newP のことである。Beta3では、Bodyは毎フレーム原点から newP だけの平行移動を行うが (それが平行移動行列traBodyの内容である)、図16の青色の矢印は、この newP だけの平行移動を表している。図16は 回転実行後の Bodyに対して、この newPだけの平行移動を実行した様子である。この平行移動によって Bodyは原点から newPだけ離れた位置に移動するが、この位置から Shellを発射するためには、Shellの発射開始位置も Bodyと同様に newPだけ平行移動させればよい。

プログラム実行中に使用するキーは以下のとおり (Aキー以外は前節の Code4と同じである)。
H  :  Bodyの向きを反時計周りに回転 (回転は$1$°ずつ)。
L  :  Bodyの向きを時計周りに回転。
S  :  オブジェクトの移動/停止用スイッチ。静止中に押すと、Bodyの向いている方向へ(direBodyの方向へ)一定速度で移動を開始する。再度キーを押すまでこの移動は続く。
A  :  Shellの発射 (長押しには対応していない)。

ヘリコプターの移動中に Shellを発射するプログラムは、最終的には次のようなものになる (このプログラムでも Shellは OneShellという名前で使われている)。
[Code3]  (実行結果 図17)
if (Input.GetKey(KeyCode.H))
{
    i_degBody += 1;
}
else if (Input.GetKey(KeyCode.L))
{
    i_degBody -= 1;
}

if (Input.GetKeyDown(KeyCode.S))   
{
    i_MOVE = !i_MOVE;
}

// localRotor
i_degRotor += 12;
THMatrix3x3 rotRotor = TH2DMath.GetRotation3x3(i_degRotor);
THMatrix3x3 traRotor = TH2DMath.GetTranslation3x3(c_attachPos_Rotor);
THMatrix3x3 localRotor = traRotor * rotRotor;

// 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.075f * direBody) : curP;
THMatrix3x3 traBody = TH2DMath.GetTranslation3x3(newP);
THMatrix3x3 localBody = traBody * rotBody;

// world matrix
THMatrix3x3 worldRotor = localBody * localRotor;
THMatrix3x3 worldBody = localBody;

Rotor.SetMatrix(worldRotor);
Body.SetMatrix(worldBody);

// Shell
if (Input.GetKeyDown(KeyCode.A))
{
    Vector2 startPos = newP + 1.25f * direBody;
    OneShell.SetPosition(startPos);
    
    OneShell.direction = direBody;
}
else
{
    Vector2 newShellPos = OneShell.GetPosition() + 0.25f * OneShell.direction;
    OneShell.SetPosition(newShellPos);
}

1行目から35行目までは前節の Code4と同じである。この部分は、キー操作によるヘリコプターの移動処理にあたる。38行目以降は1箇所を除き、本節の Code2の24行目以降と同じである。この if/elseブロックは Shellの発射処理の記述である。
Code2と異なる点は、40行目の Shellの発射開始位置の計算において、ここでは newP を加算している点である。この newP の意味や加算の理由については上で解説したとおりであるが、以下にそのことについて、先程の図16を用いて補足をしよう。図16の原点付近には半透明のヘリコプターと その先端に同じく半透明の Shellが描かれている。この半透明の Shellの位置の座標は、プログラム40行目の 1.25 * direBody であり、つまり、newPが加算される前の値である。図中の上側に半透明でない通常色のヘリコプターと Shellが描かれているが、この通常色の Shellの位置の座標が newPを加算した値、すなわち 1.25 * direBody + newP である。
Code2では、Bodyは原点から動くことはなかったので、Shellを発射する際の開始位置は、原点から Bodyの向きに$1.25$だけ離れた位置、すなわち、1.25 * direBody であったが、Code3では、Bodyは毎フレーム 原点から newPだけの平行移動を行うので、Shellを発射する際の開始位置も、「原点から Bodyの向きに$1.25$だけ離れた位置」ではなく、そこからさらに newPだけ離れた位置、すなわち、1.25 * direBody + newP にする必要がある。

図16
図17 Code3 実行結果

45行目の elseブロックは Code2と同じである。このブロックには Aキーが押されていない状態のときに常に入り続けるが、その際には Shellが発射されたときの方向(OneShell.direction)に毎フレーム一定速度で進ませる処理が行われる。再度Aキーが押されるまで Shellは OneShell.direction の方向に進み続けることになる。


# Code4
今までの実装においては、Shellを発射するためには Aキーを1回1回押さなければならなかった。今回は、Aキーを押し続けることで Shellが連続的に発射されるようにしてみよう。いわゆる、連射の実装である。
Shellを連射する場合には、必要となる Shellの数も、今までのように1つではなく複数個の Shellが必要になり、その複数の Shellは全て同じ方向に進むとは限らないので、それぞれの Shellの進行方向を保持する変数も必要になる。また、複数個の Shellといっても、その数は有限であり、キーを押し続ける限り Shellが連射され続けるようにするためには、その有限個の Shellを使い回す必要も生じてくる。
では実際に、これらの問題について1つずつ考えていこう。

上でも少し触れたが、本節で使われているオブジェクト Shell はカスタムライブラリの THShell2D クラスのインスタンスである。Shellの連射を実装するために、THShell2Dクラスには active という bool型のプロパティが用意されている。

active
  :  THShell2Dクラスの bool型プロパティ。Shellが発射され、画面内を進行している間はこのプロパティの値が true となっている。画面の外に移動した時点でこのプロパティの値は false となる。

bool型プロパティ active の詳細ついては後の段階で説明する。

今回のプログラムでは Shellは配列として使用される。今までのプログラムでは Shellは「OneShell」という名前で使われていたが、ここからは複数のShellを使うので、配列としてShell[0]、Shell[1]、Shell[2] ... の形で使われる。
では具体的に、Shellの2つのプロパティ(activedirection)を使用した簡単なプログラムを以下に示す。
for(int i=0; i < Shell.Length; i++)
{
    if (Shell[i].active)
    {
        Vector2 newShellPos = Shell[i].GetPosition() + 0.45f * Shell[i].direction;
        if( Mathf.Abs(newShellPos.x) > 6 || Mathf.Abs(newShellPos.y) > 6    )
        {
            Shell[i].active = false;
        }

        Shell[i].SetPosition(newShellPos);
    }
}

例えば、Shellの要素数が4であったとしよう、そのときは最大で4つの Shellが同時に運動することになるが、このプログラム実行中のある時点において4つの Shellが図18に示される位置にあったとする。緑色の矢印は、それぞれの Shellの進行方向を表している。

図18
図19

3行目の ifブロックは、毎フレーム Shell[i]を緑色の矢印の方向へ一定距離だけ進ませる処理を行う部分であるが、その判定条件では Shell[i].active という先程の bool型プロパティが使われている。この Shell[i].active は、Shell[i]が表示領域内に存在するかどうかを意味する。つまり、画面の中にあるのであれば active の値は true であり、現在の位置から緑色の矢印の方向へ一定距離だけ移動させる処理が、この ifブロック内で行われる。表示領域に存在しない(画面上に描かれていない) Shellの場合は、この ifブロックに入らないため、何も処理は行われない (表示領域外側のある位置に留まり続ける)。
5行目の Shell[i].direction は、この緑色の矢印の方向を表している (正規化されているので単位ベクトルである)。図中の Shellに振られた番号はプログラム中の Shell配列のインデックスであり、図中の 0 は Shell[0]、1 は Shell[1] という意味である。図18の状態からさらに時間が経過したときに図19の状態になったとしよう。図19の時点では表示されている Shellは、Shell[1]、Shell[2]、Shell[3]の3つであり、Shell[0]は表示領域の外側へ移動してしまっている。
ここでの表示領域とは、図に示されるように x軸、y軸ともに $-6$から $6$の範囲である。この領域が、Shell(及び 全てのオブジェクト)の表示される領域であり、この領域の外側に移動してしまうと、もちろん オブジェクトは表示されない (描画対象ではなくなる)。プログラム6行目の ifブロックの条件判定は、この表示領域に関する判定である。5行目では、Shellが次に移動する位置 newShellPos が計算されるが、6行目の ifブロックの条件判定では、その newShellPosが表示領域の外側かどうかを判定しているのである。Mathf.Abs(..)は Unityに用意されている関数で、引数に指定した値の絶対値を返す。例えば、$x$が $-6$から $6$の間に含まれていない場合を判定するためには、if(Mathf.Abs(x) > 6) とすればよい (これは if(x < -6 || 6 < x) と同じ意味である)。6行目の条件判定では、次に移動する位置 newShellPosが表示領域の外側であれば true となり、この ifブロックに入るが、そこでの処理は Shell[i].activefalse にするだけである。これは、Shell[i]が表示領域内に存在しないことを示すことになる。したがって、次回以降のフレームにおいては Shell[i]は、3行目の ifブロックに入ることはないので、それ以降のフレームでは、Shell[i]は最後に移動した位置(表示領域外側の位置)に留まり続けることになる。

ただし、上のプログラムでは連射をしたとしても、それはすぐ終わってしまう。上のプログラムにおいては Shell配列の要素数は4つであったが、その4つすべての Shellが表示領域の外側に移動した時点で、画面には Shellが1つも存在しないことになり、それは以降のフレームにおいても同じ状態が続く。キーを押している間ずっと連射し続けるためには、有限個の Shellの使い回しが必要になってくるわけである。
以下では、そのことについて見ていこう。

上のプログラムは Shellが発射された後の処理、すなわち、Shellの進行中(移動中)の処理についての記述であり、キーが押された時、あるいは、押され続けているときの処理に関する記述は省かれていた。次のプログラムは、キー操作も含めた連射のプログラムであり、有限個の Shellを使い回すことによって、キーを押し続けている間ずっと Shellが連射される処理を実装したものである。

i_frameCount++;

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

Vector2 direBody = new Vector2(0.616f, 0.788f);  // 単位ベクトル
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) > 6 || Mathf.Abs(newShellPos.y) > 6    )
        {
            Shell[i].active = false;
        }

        Shell[i].SetPosition(newShellPos);
    }
    else
    {
        if (bShoot)
        {
            Shell[i].active = true;
            Shell[i].direction = direBody;

            Vector2 startPos = 1.25f * direBody + newP;
            Shell[i].SetPosition(startPos);

            bShoot = false;
        }
    }
}

以下の解説においては bool型プロパティ activetrueの Shellについては、便宜上「activeなShell」という表現を用いる。例えば、「Shell[3].activetrueである」ことと、「Shell[3]が activeなShellである」ことは同じ意味である。

1行目の i_frameCount はプログラム実行開始からのフレーム数をカウントする int型のインスタンス変数である。4行目の ifブロックが Shell連射のためのキー操作の部分である。ここでは、Aキーが押され続けていれば、この ifブロックに入ることになる。6行目の if判定で使われている Body.lastShootFrame は、Bodyが最後にShellを発射したときのフレーム番号を表す int型のプロパティである。
本節以降のプログラムでは、Shellを発射するオブジェクトや、Shellとの衝突判定の対象となるオブジェクトが使われるが、それらのオブジェクトはみなカスタムライブラリの THAlphaObject2D クラスのインスタンスである。ここで使われているヘリコプターは Body、Rotor の2つのオブジェクトによって構成されるが、Shellを発射するオブジェクトは Bodyであり、Bodyは THAlphaObject2Dクラスのインスタンスである。THAlphaObject2Dクラスは THObject2Dクラスを継承するクラスで、Shellの発射や衝突判定に必要ないくつかのプロパティを持っている。
たとえば、今回は Shellの連射を実装するにあたって THAlphaObject2Dクラスの int型プロパティ lastShootFrame が使われている。

lastShootFrame
  :  THAlphaObject2Dクラスの int型プロパティ。オブジェクトがShellを連射している際に、そのオブジェクトが最後に Shellを発射したときのフレーム番号を保持しておくために、このプロパティが使われる。

つまり、6行目の判定の意味は、Bodyが最後に Shellを発射してから 3フレーム以上経過しているかどうかをチェックしているのである。Aキーを押し続けていても毎フレーム Shellが発射されるわけではなく、一定の距離を置いて発射されるように最短でも3フレーム間隔にしているのである。もちろん、この間隔は任意に設定できるが、1フレームごとに発射すると、Shellと Shellとの間隔が狭くなりすぎるため、ここでは少なくとも3フレーム経過してからの発射としている。もし、最後に Shellを発射してから3フレーム以上経過していれば、ローカル変数 bShoottrueがセットされるが、bShoottrueがセットされた場合、そのフレームにおいて Shellが発射されることになる (正確には、14行目のfor文ループ中に最初に見つかったactiveでないShellを発射開始位置に移動させる)。
14行目の for文は、ifブロック(16行目)については先程のプログラムと同じく Shellの進行中の処理である。26行目の elseブロックは、今回追加された部分であり、このブロックに Shellの発射処理が記述されている。
例えば、使用している Shellの数が 8 であったとしよう (つまり、Shell[0]から Shell[7]までを使用する)。この設定の下で、Aキーを押し続けて7個の Shellを連射したとする。そのときの様子を示したのが図20である。
図20 Shellを7個連射したときの様子
先程の図と同じように各Shellに振られている数字は、Shell配列のインデックスを表しており、図20は Shell[0]から Shell[6]の7個の Shellが進行している様子である。表示領域内にある Shell、すなわち、描画されている Shellは activeな Shellであるから、ここでは Shell[0]から Shell[6]は activeな Shellであり、そしてそれは Shell[0]から Shell[6]のプロパティ active の値が trueであることと同じことである。
ここで、さらに Aキーを押し続け、あるフレームでもう1つの Shellを発射することになったとする。そのとき、まず 6行目の ifブロック内でローカル変数bShoottrueになる。 Shell配列の要素数が8なので、プログラム中の14行目の for文は8回繰り返されるが、i==0 から i==6 のときは先程も述べたように、Shell[i].activeの値が trueなので、16行目の ifブロックに入り、次の移動先への移動処理を行う。図20の時点では、Shell[7]は描画されていない、すなわち、Shell[7]は表示領域の外側にあり、Shell[7].activeの値は falseである。したがって、for文において i==7 のときには26行目の elseブロックに処理が移る。今回のフレームでは bShoottrueになっているので、ここでは Shell[7]を発射開始位置に移動させる処理が行われる。
図21  8個目のShellを発射したときの様子
33行目から34行目が移動処理の記述であるが、33行目で使われている変数 newP はCode3と同じく、Bodyの新しい移動先の位置を表す変数である。この処理によって、Shell[7]は図21に示されるように、Body先端の発射開始位置に移動することになる。30行目で、activetrueがセットされるので、次のフレームからは Shell[7]も16行目の ifブロックに入ることになり、毎フレーム指定方向に少しずつ進んでいく。31行目では各Shellの Vector2型プロパティ direction に、direBody をセットしているが、このプログラムでは direBodyの値は毎フレーム (0.616, 0.788) である (13行目 ; direBodyは単位ベクトルである)。directionは、各Shellの進行方向を表すプロパティで、この設定によって全ての Shellが (0.616, 0.788) の方向に進んでいくことになる。新しくactiveになったShellを発射した後は、36行目においてローカル変数 bShootの値を falseにする (ここで bShootfalseにする処理は重要である。この記述がなければ連射にはならない)。
今、進めている例では Shellの数は8個という設定であったから、図21のような状態は、すべての Shellが発射されており、すべての Shellが表示領域内で進行しているという状態になる。これは、すべての Shellが activeな Shellであるということである。このことが、意味することは、この状態でAキーが押されていても、for文中の26行目の elseブロックには処理が移らないということである。つまり、Aキーが押されていても Shellが発射されることはない。
では、図21の状態から少し時間が経過して図22に示される状態になったとしよう。

図22 Shell[0]が表示領域の外側へ移動したときの様子
図23 Shell[0]が再び発射されたときの様子

図22の時点では、Shell[0]が表示領域の外側へ移動してしまったため、表示領域内にある Shellは、Shell[1]から Shell[7]までの7個である。19行目の ifブロックは Shellが表示領域の外側に移動したかどうかを判定する部分であり、Shell[0]が表示領域外に移動する際には、この ifブロックにおいて Shell[0].activefalseがセットされる。
この状態でAキーが押された場合、Shell[0].activefalseになっているので、for文において i==0 のときに、26行目の elseブロックに入ることになり、そこでの処理によって Shell[0]が発射開始位置に移動することになる。つまり、図23に示されるように Shell[0]が再び発射されるのである。
さらに時間が経過し、図24の状態になったとしよう。

図24 Shell[1]が表示領域の外側へ移動したときの様子
図25 Shell[1]が再び発射されたときの様子

この時点では、Shell[1]が表示領域の外側へ移動してしまったため、表示領域内には、Shell[0]、及び Shell[2]から Shell[7]の7個の Shellが存在している。この状態でAキーが押されていた場合には次のようになる。Shell[1]が表示領域の外側に移動するときに、やはり、19行目の ifブロックにおいて Shell[1].active には falseがセットされるので、今回は for文において i==1 のときに、26行目の elseブロックに処理が移り、Shell[1]が発射開始位置に移動することになる。そして、図25に示されるように Shell[1]が再び発射されるのである。
つまり、Aキーが押されているときには for文の繰り返し処理の過程で、activeでない Shellのうち、最初に見つかったものを発射させるのである。一度発射させれば、その Shellは activeな Shellとなり、以降のフレームでは16行目の ifブロックに入り、指定の方向に進行し続ける。そして、時間経過とともに表示領域の外側に移動するが、そのときにまた、activeでない Shellになるのである (activeでない Shellは、いわば待機中の Shellであり、発射されるまで表示領域の外側で待機し続けるのである)。
Aキーが押され続けていれば、上に述べてきた処理が繰り返されるわけである。以上が、Shellの使い回しによる連射の具体的な内容である。

上で解説してきた例では、Shellの個数は4個や8個という設定であったが、以下に示す実際のプログラムでは、用意される Shellの個数は 50個である。この数であれば、Aキーを押し続けても連射が途中で途切れるということは起こらない。
Shellの個数が少ない場合には、Aキーを押し続けるとすぐに全てのShellが発射される。そのときには、全てのShellが表領域内で直進をしているという状況が生じやすい。全てのShellが表示領域内にある間(全てのShellがactiveである間)は、Aキーを押しても新たにShellが発生することはないので、連射が途切れた状態になってしまう。

(今回のプログラムにおける 50という個数設定は適当な設定であり、もっと少ない数でも十分である。Shellは8ポリゴンの単色オブジェクトであり、その運動は等速直線運動のみであるから、Shellの数が50個でも100個でもメモリー負荷、処理負荷を懸念する必要はない)


では、まずは発射方向をy軸プラス方向に固定した連射のプログラムを以下に示す。
[Beta4]  (実行結果 図26)
i_frameCount++;

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

// localRotor
THMatrix3x3 rotRotor = TH2DMath.GetRotation3x3(45);
THMatrix3x3 traRotor = TH2DMath.GetTranslation3x3(c_attachPos_Rotor);
THMatrix3x3 localRotor = traRotor * rotRotor;

// localBody
Vector2 direBody = Vector2.up;
Vector2 newP = Vector2.zero;
THMatrix3x3 localBody = THMatrix3x3.identity;

// world matrix
THMatrix3x3 worldRotor = localBody * localRotor;
THMatrix3x3 worldBody = localBody;

Rotor.SetMatrix(worldRotor);
Body.SetMatrix(worldBody);

// Shell
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 = direBody;

            Vector2 startPos = newP + 1.25f * direBody;
            Shell[i].SetPosition(startPos);

            bShoot = false;
        }
    }
}

このプログラムは、Rotorと Bodyを一体化して表示するだけの処理に上の連射のプログラムを追加しただけのものであり、新たに追加される処理はない 。Aキーが押され続けているときの処理である4行目の ifブロック、及び Shell連射用の31行目の for文は、上で解説した連射のプログラムと内容は同じである (上のプログラムでは、表示領域の範囲がx軸、y軸ともに $-6$から $6$の間であったが、ここでは その範囲を多少広く設定してある)。
図26 Beta4 実行結果
また、14行目から28行目では変換行列を求めて、Rotor、Bodyのそれぞれに実行しているが、結果的には、Rotorが Bodyにアタッチされただけの状態で表示され (Rotor、Bodyは実行中は静止したまま)、Aキーを押し続けると Shellが連射されることになる。このプログラムでは、Bodyの向きを変える処理はないため、連射の方向は実行結果に示されるようにy軸プラス方向のみである。
なお、19行目、20行目のローカル変数 direBodynewP にセットされる値は毎フレーム同じであり、わざわざ これらのローカル変数を用意する必要はないが、この2つのローカル変数は後のプログラムの内容と合わせるために、ここでは形式的に使われている。

続いて、Bodyがキー操作(H、Lキー)によって向きを変えられるようにし、Bodyの向いている方向に Shellを連射する処理を実装する (Bodyは回転することはできるが、位置は初期状態の位置から動かさない)。
[Code4]  (実行結果 図27)
i_frameCount++;

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

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

// localRotor
THMatrix3x3 rotRotor = TH2DMath.GetRotation3x3(45);
THMatrix3x3 traRotor = TH2DMath.GetTranslation3x3(c_attachPos_Rotor);
THMatrix3x3 localRotor = traRotor * rotRotor;

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

// world matrix
THMatrix3x3 worldRotor = localBody * localRotor;
THMatrix3x3 worldBody = localBody;

Rotor.SetMatrix(worldRotor);
Body.SetMatrix(worldBody);

// Shell
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 = direBody;
            
            Vector2 startPos = newP + 1.25f * direBody;
            Shell[i].SetPosition(startPos);
            
            bShoot = false;
        }
    }
}

図27 Code4 実行結果
このプログラムは Beta4とほとんど同じであるが、Bodyの向きを回転させる処理が追加されている。具体的には、 3行目から10行目は Bodyの向きを回転させるためのキー操作の処理であり、28行目において算出される回転行列 rotBodyは、実質的に Bodyに実行される変換行列である。また、Beta4ではローカル変数 direBodyの値は毎フレーム y軸プラス方向を表していたが、ここでは、29行目において Bodyの向きと同じ向きがセットされている。Shellが発射される際には、55行目の ifブロックに入るが、このブロックで使用されている direBodyの値は、その時点での Bodyの向きである必要があるが、このプログラムにおいてもその点は問題ないわけである。なお、今回もヘリコプターの移動は発生しないので、30行目のローカル変数 newPは前回と同じく常に $(0, 0)$ であり、ここでも形式的な使用である。
実行結果(図27)に示されるように、Bodyの回転を実行中にAキーを押し続けると、その時点での Bodyの向いている方向に Shellが連射されることになる。


# Code5
Code3ではキー操作によってヘリコプターを移動させ、移動中に(あるいは 移動した位置において)Shellの発射を行うプログラムを作成したが、そのプログラムにおける Shellの発射はキーを1回1回押さなければならないものであった。ここでは、その処理を変更し ヘリコプターの移動中に(あるいは 移動した位置において)キーを押し続けることでShellを連射できるようにする。

このプログラムは、今までのプログラムのまとめのようなものである。
[Code5]  (実行結果 図28)
i_frameCount++;

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

if (Input.GetKeyDown(KeyCode.S))   
{
    i_MOVE = !i_MOVE;
}

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

// localRotor
i_degRotor += 12;
THMatrix3x3 rotRotor = TH2DMath.GetRotation3x3(i_degRotor);
THMatrix3x3 traRotor = TH2DMath.GetTranslation3x3(c_attachPos_Rotor);
THMatrix3x3 localRotor = traRotor * rotRotor;

// 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.075f * direBody : curP;
THMatrix3x3 traBody = TH2DMath.GetTranslation3x3(newP);
THMatrix3x3 localBody = traBody * rotBody;

// world matrix
THMatrix3x3 worldRotor = localBody * localRotor;
THMatrix3x3 worldBody = localBody;

Rotor.SetMatrix(worldRotor);
Body.SetMatrix(worldBody);

// Shell
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 = direBody;
            
            Vector2 startPos = newP + 1.25f * direBody;
            Shell[i].SetPosition(startPos);
            
            bShoot = false;
        }
    }
}

図28 Code5 実行結果
プログラムの内容自体は、Code3の内容を単発の発射から連射処理に変更しただけのものであり、新しい処理はない。実行結果に示されるように、ヘリコプターの移動中に、あるいは移動した位置でAキーを押し続けると、その時点でのヘリコプターの向き(Bodyの向き)に Shellが連射される。












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