前節では、図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」という名前で使われている)
プログラム中で使用されている変数
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 が使われている。