前節ではキー操作によるオブジェクトの移動を扱った。そこで対象となったオブジェクトの形状は円であり、オブジェクトの外観も1色のみで構成されていた。円という形状は特殊な形状で、何度回転させても外観上の変化がない。そういった理由で前節のオブジェクトPointについては、向き、すなわちオブジェクトのOrientationを一切設定しなかった。本節では、まず始めに長方形オブジェクトの運動を扱う。長方形を移動させる場合には円と違って、オブジェクトの向きを設定しなければ不自然な場合が発生するが、以下の最初のプログラムにおいて、その具体的な例をまずは見てみよう。
# Code1
図1は、Rectという長方形オブジェクトの初期状態である。
図1 Rect 初期状態
図2 Code1 実行結果 まず、このオブジェクトを$360$°任意の方向に移動させるプログラムである。ここでもまた前節で使われた進行線を、オブジェクトの移動方向を示すために用いている。プログラムは前節のCode7を1箇所のみ変更してそのまま使用している。変更点は移動させるオブジェクトが Pointから Rectに変わるので、Code7で使われていた変数 Pointをここでは Rectという変数名に変更している。
以下に、操作方法とプログラムを示す。
H : 進行線が反時計周りに回転(回転は$2$°ずつ)。
L : 進行線が時計周りに回転。
S : Rectを進行線の方向に一定の距離だけ移動させる(長押しに対応する)。
[Code1] (実行結果 図2)
Vector2 curPos = Rect.GetPosition();
if (Input.GetKey(KeyCode.H))
{
i_forwardDegree += 2;
}
else if (Input.GetKey(KeyCode.L))
{
i_forwardDegree -= 2;
}
THMatrix3x3 R = TH2DMath.GetRotation3x3(i_forwardDegree);
Vector2 forwardDir = R * new Vector3(0, 1, 1);
float d = 0.05f;
Vector2 newPos = (Input.GetKey(KeyCode.S)) ? curPos + d * forwardDir : curPos;
THMatrix3x3 T = TH2DMath.GetTranslation3x3(newPos);
Rect.SetMatrix(T);
プログラム中でつかわれている
curPos 、
i_forwardDegree 、
forwardDir などは、前節のものと使用目的は同じである。すなわち、
curPos は Rectの現在位置を表し、
i_forwardDegree は進行線の初期の方向であるy軸プラス方向(単位ベクトルで表せば$(0, 1)$)からの回転角度で、
forwardDir は進行線の現在の方向を表している。進行線の現在の方向は、初期の方向であるy軸プラス方向から角度
i_forwardDegree だけ回転させた方向であり、13行目の計算でそれを求めている。
R は角度
i_forwardDegree だけ回転させる回転行列であり、
(0, 1, 1) は$(0, 1)$を同次座標で表したものである。
18行目で算出される平行移動行列
T は、
newPos だけの平行移動を行うものであるが、Rectの場合は初期状態の位置が原点であるので、この
T を実行することにより結果的には
newPos に移動することになる (座標で表せば
(newPos.x, newPos.y) へ移動することになる)。Rectが初期状態の位置である原点から、毎フレーム計算された位置(
newPos )への移動を行うことで実行結果(図2)に見られるように、$360$°任意の方向に移動することになるのである。
このプログラムは、Rectをキー操作によって$360$°任意の方向に移動させるということに関しては、特に問題点はない。しかし、円形オブジェクトを移動させる場合と違って、長方形オブジェクトの移動の場合には、その進行方向に自身の先端 (Rectの場合は、青色の小さい領域がある側)が向いている方が、より自然な印象を与える。そこで、Rectに対して向きの設定をする必要が生じるわけである。
# Code2
図3 Code2 実行結果 Code1では Rectの進行方向は、HやLキーによって$360$°変えることができ、その進行方向は進行線という細長い線によって示されていた。
今回は Rectの先端(青色の小さい領域がある側)が常に進行線の方向と一致するようにプログラムを書き改める。ただし、このプログラムでは Rectの移動は扱わないので、使用するキーはHとLのみである。
実装は簡単である。Code1では、HやLキーで
i_forwardDegree が更新され、
i_forwardDegree だけ回転させる行列
R が12行目で算出された。この
R は進行線の初期の方向であるy軸プラス方向を、現在の方向へ回転させる行列である。Rectの先端は初期状態では進行線と同じくy軸プラス方向を向いているので、回転行列
R を初期状態のRectに実行すれば進行線と同じ方向を向くことになる。
[Code2] (実行結果 図3)
if (Input.GetKey(KeyCode.H))
{
i_forwardDegree += 2;
}
else if (Input.GetKey(KeyCode.L))
{
i_forwardDegree -= 2;
}
THMatrix3x3 R = TH2DMath.GetRotation3x3(i_forwardDegree);
Vector2 forwardDir = R * new Vector3(0, 1, 1);
Rect.SetMatrix(R);
11行目の
forwardDir は Rectの現在の向きを表すローカル変数であるが、ここでは計算結果がセットされるだけで それ以降の使用はない。
# Code3
図4 Code3 実行結果 Code2ではRectの向きを$360$°回転できるようにした。ここではRectの先端の方向に、Rectが移動するプログラムを作成しよう。
実行手順は次の通り。
(1) Rectの先端を進ませたい方向に回転させる。
(2) Rectの先端の方向に一定距離進ませる。
操作方法は以前のものと変わるところはない (ここからは進行線は使われないのでHやLキーを押して回転するのは Rectのみである)。
H : Rectが反時計周りに回転(回転は$2$°ずつ)。
L : Rectが時計周りに回転。
S : Rectを進行線の方向に一定の距離だけ移動させる(長押しに対応する)。
[Code3] (実行結果 図4)
Vector2 curPos = Rect.GetPosition();
if (Input.GetKey(KeyCode.H))
{
i_forwardDegree += 2;
}
else if (Input.GetKey(KeyCode.L))
{
i_forwardDegree -= 2;
}
THMatrix3x3 R = TH2DMath.GetRotation3x3(i_forwardDegree);
Vector2 forwardDir = R * new Vector3(0, 1, 1);
float d = 0.05f;
Vector2 newPos = (Input.GetKey(KeyCode.S)) ? curPos + d * forwardDir : curPos;
THMatrix3x3 T = TH2DMath.GetTranslation3x3(newPos);
THMatrix3x3 M = T * R;
Rect.SetMatrix(M);
このプログラムはCode1とほとんど同じである。1箇所異なる点は、Rectに実行する変換行列がこのプログラムでは19行目の
M = T * R; になっていることである。この変換行列
M は、まず 初期状態の Rectを
i_forwardDegree だけ回転させ(
R )、次に
newPos だけの平行移動を行う(
T ) という2つの変換を1つにまとめたものである。
図5
図6 コマ送り表示 図5は実行結果(図4)の最初の方のいくつかのフレームにおいて、オブジェクトに実行される変換をアニメーションとして表したものである。図5において白い文字で「R」と表示されている状態は回転行列
R が実行されている過程を意味し、「T」と表示されている状態は平行移動行列
T が実行されている過程を意味している。
指定の向きに回転を行ってから、計算された位置への移動 という処理を毎フレーム実行していくことで実行結果(図4あるいは図6)に見られるように、Rectが自身の先端の向きに進んでいくことになるのである。
図6は実行結果(図4)の最初の方の20フレーム程をコマ送りで表示したものである。
ここでは、キー操作によってオブジェクトを運動させたが、プログラムにおける運動の実装手順は、2-4節(自転と公転)におけるCode2(あるいは Code3)の場合と本質的には変わらない。2-4節 Code2においても、オブジェクトDiskが初期状態での向きから回転し、次に初期状態での位置から現在の位置へ移動したのであった。つまり、毎フレーム初期状態の Diskに対して変換が実行されていた。上記図5でも、毎フレーム初期状態の Rectに対して回転、平行移動という順で変換が実行されているのである。
# Code4
ここから先の節の後半部分では、一体化した2つのオブジェクトをキー操作によって運動させる問題について扱う。
次の3つの図は、左からBody、Barrel、そしてその2つのオブジェクトを一体化した2D形状の戦車である。
まずは Body、Barrelを一体化して戦車を構成するプログラムから始めよう。
この場合の一体化では、Barrelを Bodyにアタッチする。つまり、Bodyが親オブジェクトであり、Barrelが子オブジェクトである。通常、アタッチの際には子オブジェクトを親オブジェクトの指定位置(アタッチポジション)に移動させるが、今回の例では、子オブジェクトの初期状態での位置が既にアタッチポジションなので、アタッチのために子オブジェクトを移動させる必要がない。したがって一体化のプログラムは次のようなものになる。
[Code4] (実行結果 図9)
THMatrix3x3 localBarrel = THMatrix3x3.identity;
THMatrix3x3 localBody = THMatrix3x3.identity;
THMatrix3x3 worldBarrel = localBody * localBarrel;
THMatrix3x3 worldBody = localBody;
Barrel.SetMatrix(worldBarrel);
Body.SetMatrix(worldBody);
プログラム中の行列
local### 、
world### は、ここではすべて identity行列である。今回の処理だけを考えれば、こういった記述は冗長であり必要ではない (Barrelと Bodyに、直接 identity行列をセットしても同じ結果になる)。2-5節から2-7節で見たように、これはオブジェクトを一体化して運動させるための記述であるので、ここではそれを明示的に示したが、もちろん形式的な意味しか持たない。
今回はアタッチのための移動が発生しないので、
localBarrel には identity行列がセットされているが、もしアタッチポジションが $(a, b)$ ならば、
localBarrel には次のようにセットすればよい。
THMatrix3x3 localBarrel = TH2DMath.GetTranslation3x3(a, b);
# Code5
図10 Code5 実行結果 Code4では一体化しただけでオブジェクトを動かさなかったので、親である Body、子である Barellの両方には identity行列が実行されていた。今回のプログラムでは2つのオブジェクトを一体化して、Code3(Rectの移動プログラム)のように$360$°任意の方向に向きを変え、Body先端の示す方向に移動できるようにする (Bodyの先端は、図7のBody上側の辺の中央部分)。
プログラムは、Code3のキー操作による Rectの移動プログラムとほとんど同じである。
しかし、主な違いとして 2つのオブジェクトを一体化し、その一体化したオブジェクトを運動させるという点、そして、その移動はキーを押し続けることによって行われるのではなく、キーを1回押せば再度押すまで移動が続くという点、また、使用している変数名が異なっているという点が挙げられる (変数名が違うだけで役割は同じ)。
使用するキーは次のとおり。
H : Bodyが反時計周りに回転 (回転は$2$°ずつ)。
L : Bodyが時計周りに回転。
S : Bodyの移動/停止用スイッチ。停止中に押すと、Bodyの向いている方向へ一定速度で移動を開始する (移動中に押すと停止する)。
[Code5] (実行結果 図10)
if (Input.GetKey(KeyCode.H))
{
i_degBody += 2;
}
else if (Input.GetKey(KeyCode.L))
{
i_degBody -= 2;
}
if (Input.GetKeyDown(KeyCode.S)) // 移動/停止
{
i_MOVE = !i_MOVE;
}
THMatrix3x3 rotBody = TH2DMath.GetRotation3x3(i_degBody);
Vector2 direBody = rotBody * new Vector3(0, 1, 1);
Vector2 curPos = Body.GetPosition();
Vector2 newPos = (i_MOVE) ? curPos + 0.05f * direBody : curPos;
THMatrix3x3 localBarrel = THMatrix3x3.identity;
THMatrix3x3 traBody = TH2DMath.GetTranslation3x3(newPos);
THMatrix3x3 localBody = traBody * rotBody;
THMatrix3x3 worldBarrel = localBody * localBarrel;
THMatrix3x3 worldBody = localBody;
Barrel.SetMatrix(worldBarrel);
Body.SetMatrix(worldBody);
19行目までは内容的にはCode3と同じである。ただ、前述したように変数名が異なっていることと、ここでは Sキーは移動/停止用のスイッチとしての役割を持っているので、10~13行目にSキー用の
if ブロックが追加されている。この
if ブロックでは、Sキーを押すごとに 移動するか停止するか を表す12行目の
bool 型インスタンス変数
i_MOVE の値が切り替わる。
i_MOVE の値が
true であれば、再度Sキーが押されるまでオブジェクトの移動は続いていく。
プログラム中で使用されている変数
i_degBody は Bodyの回転角度を表すインスタンス変数で、初期状態の方向であるy軸プラス方向から何度回転したかを表している。15行目の
rotBody は角度
i_degBody だけ回転させる回転行列であり、16行目の
direBody は Bodyの現在の方向を表す
Vector2 型のローカル変数である。
direBody の示す方向が Bodyの進む方向である。
21行目以降は 1箇所を除いてCode4と同じである。それは24行目で
localBody へ行列をセットする箇所である。
THMatrix3x3 localBody = traBody * rotBody;
Code4においては
localBody へセットされていた行列は identity行列であった。ここでは上記のように
traBody と
rotBody の積がセットされている。これによって、今回のプログラムでは運動が発生するのである。
21行目以降の処理を解説しよう。
Bodyに実行される行列
worldBody は
localBody のコピーであり、具体的には、まず初期状態から
i_degBody だけ回転させ(
rotBody )、次に初期の位置(原点)から このフレームで計算された位置(
newPos )へ平行移動させる(
traBody ) という2つの変換をまとめたものである。
Barrelに実行される行列
worldBarrel は、まず Barellを Bodyにアタッチし(
localBarrel : 上で述べたように今回のアタッチは何も動かす必要はないのでidentity行列)、そのアタッチ後の位置から Bodyに実行する変換を Barrelに対しても実行する(
localBody )。重要なのは Barrelを Bodyにアタッチしてから、Barrelに対し、Bodyに実行するものと同じ変換行列
localBody を実行していることである。これにより、Barrelはアタッチされたその位置からBodyと同じ運動をすることになり、この例でいえば、Bodyの上に載せられた状態で一体化して運動することになるのである。
ただ、今回のプログラムでは
localBarrel は identity行列なので、
worldBarrel 、
worldBody の表す変換は同じものである。実行結果(図10)を見ればわかるように、それによって Body、Barrelはともに同じ運動をすることになる。
# Code6
Code4、Code5において Barellに実行される行列
worldBarrel は次のようにセットされていた。
THMatrix3x3 worldBarrel = localBody * localBarrel;
Code4、Code5における
localBarrel は identity行列であったので Barrelが独自に運動をすることはなかった。今回は 子オブジェクトBarrelをキー操作によって独自に運動させることについて考えていく。
2-5節から2-7節において、複数のオブジェクトを一体化して運動させる場合に使用した
local### という行列は個々のオブジェクトそれぞれの独自の運動を表していた (子オブジェクトを持つ親オブジェクトの場合は、
local### は親オブジェクトだけでなくアタッチされている子オブジェクトも一体となって運動するのであった)。今回のプログラムでは Barrelに独自の運動をさせるために、
localBarrel にBarrelの独自の運動を定義する。
実際に、キー操作による Barrelの独立した運動を実装しよう。
Code5では、HあるいはLキーによって Bodyと Barrel、すなわち 戦車全体が回転した。ここでは、さらに次のキー操作を追加してBarrelだけが(Bodyとは別に)回転できるようする。
D : Barrelが反時計周りに回転(回転は$4$°ずつ)。
F : Barrelが時計周りに回転。
H及びLキーは今まで通り戦車全体、すなわち Bodyとその上に載っている Barrelの回転を表す(回転角度は1フレームあたり$2$°ずつの回転である)。
また、Barrelの回転のために新しいインスタンス変数を1つ追加する。
i_degBarrel は Barrelの現在の回転角度のことで、初期の方向である
Bodyの先端から 何度回転したかを表す (初期値は$0$)。ここで注意すべきことは、Barrelは初期状態で$(0, 1)$の方向、すなわち y軸プラス方向を向いているが(図8)、
i_degBarrel の表す角度はy軸プラス方向からの回転角度ではなく、Bodyの先端からの回転角度であるということである。
以下の図11は、Bodyが回転していない状態で Barrelを$30$°回転させた図である。そして、隣の図12は、図11の状態から Bodyを$45$°回転させた図である。
図11 Barrelの30°回転
図12 Barrelの30°回転 (Bodyも45°回転している) Bodyの先端とは Bodyの上側の辺の中央部分のことで、上図においては赤い点線上に存在する。したがって、Body先端からの回転角度とは赤い点線からの回転角度に等しい。図12では Bodyが$45$°回転しているので、Barrelはy軸プラス方向から$75$°回転させた方向を向いているが、
i_degBarrel は Bodyの先端からの回転角度なので、図12におけるその値は$30$である。
以下の図13は、Bodyが回転していない状態で Barrelを$60$°回転させた図である。図14は、図13の状態から Bodyを$120$°回転させた図である。図14では Bodyが$120$°回転しているので、Barrelはy軸プラス方向から$180$°回転させた方向を向いているが、
i_degBarrel の値は$60$である。
図13 Barrelの60°回転
図14 Barrelの60°回転(Bodyも120°回転している) [Code6] (実行結果 図15)
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);
図15 Code6 実行結果 今回のプログラムでは移動は扱っていない。Barrelの独自の回転のみが目的である。
10~17行目は、D、Fキーによる Barrelの回転角度
i_degBarrel の更新処理である。19行目で
i_degBarrel だけ回転させる行列
rotBarrel を取得し、これを
localBarrel にそのままコピーしている(20行目)。今回は移動が発生しないので、Bodyに実行する変換も回転だけであり、その回転行列は22~23行目で取得されている。
ここでも重要なのは25行目の Barrelに実行される行列
worldBarrel である。この行列が表す変換は、まず Barrel自身の回転を行い(
localBarrel )、その回転した状態から Bodyに対して実行する回転と同じ回転を実行するのである(
localBody )。こうすることによって、Barrelが Bodyの上で独自に回転することが実現されるのである。
もし、D、Fキーを1度も押さなければ、
localBarrel は何も回転を行わない行列になり、実際に Barrelに実行される回転は
localBody のみとなる。その場合には、Barellと Bodyは同じ回転をすることになるので、Barrelが Bodyの上に固定された状態で回転することになる。
このプログラムでは H、Lキーの
if ブロックと D、Fキーの
if ブロックはつながっていないので、Bodyの回転と Barrelの回転を同時に行うことができる。例えば、Hキーを押した状態で、Fキーを押し続けると Bodyが反時計周りに回転しながら、その上で Barrelが時計周りに回転をすることになる。
仮に D、Fキーを次のように
else if によって、H、Lキーの
if ブロックとつなげたとしよう。
if (Input.GetKey(KeyCode.H))
{
...
}
else if (Input.GetKey(KeyCode.L))
{
...
}
else if (Input.GetKey(KeyCode.D))
{
...
}
else if (Input.GetKey(KeyCode.F))
{
...
}
この場合は、Bodyの回転中に Barrelを回転させることはできない。Barrelを回転させるためには、一旦 Bodyの回転を中断しなければいけない(具体的には、HとDが同時に押されている場合は、順序的に上に位置するHのブロックに入るのでDのブロックには入らない。つまり、Bodyの回転は行われるが Barrelの回転は行われない)。
Bodyと Barrelの回転を同時に行うためには、D、Fキーの
if ブロックは独立させておくことが必要なのである。
# Code7
図16 Code7 実行結果 では最後に、Code6に Bodyの移動処理を追加しよう。この移動処理はすでに、何度か見てきているSキーによる操作である。
最終的なキー操作は以下のようになる。
H : Bodyが反時計周りに回転(回転は$2$°ずつ)。
L : Bodyが時計周りに回転。
D : Barrelが反時計周りに回転(回転は$4$°ずつ)。
F : Barrelが時計周りに回転。
S : Bodyの移動/停止用スイッチ。停止中に押すと、Bodyの向いている方向へ一定速度で移動を開始する (移動中に押すと停止する)。
プログラムは今までのものをまとめただけで、新たな処理はない。
[Code7] (実行結果 図16)
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 curPos = Body.GetPosition();
Vector2 newPos = (i_MOVE) ? curPos + 0.05f * direBody : curPos;
THMatrix3x3 traBody = TH2DMath.GetTranslation3x3(newPos);
THMatrix3x3 localBody = traBody * rotBody;
// world matrix
THMatrix3x3 worldBarrel = localBody * localBarrel;
THMatrix3x3 worldBody = localBody;
Barrel.SetMatrix(worldBarrel);
Body.SetMatrix(worldBody);
図17は、このプログラムの実行結果(図16)の最初のいくつかのフレームにおいて、オブジェクトに実行される変換をアニメーションとして表したものである。
図17
図18 白い文字で「localBarrel」と表示されている状態は変換行列
localBarrel が実行されている過程であり、白い文字で「localBody」と表示されている状態は変換行列
localBody が実行されている過程である。38行目、39行目からわかるように
localBarrel は Barrelのみに実行され、
localBody は Barrelと Bodyの両者に実行される。図17のアニメーションでは「localBarrel」と表示されている状態では Barrelのみが動いており、「localBody」が表示されている状態ではBarrel、Bodyの両方が動いているが、これは38行目、39行目に記述されていることが反映されているのである。
また、「localBody」の表示中その下部に「traBody * rotBody」という文字も表示されているが、これは変換行列
localBody は、回転行列
rotBody と平行移動行列
traBody の2つの変換をまとめたものであることを意味している。「rotBody」が青色で表示されているときは回転行列
rotBody が実行されていることを意味し、「traBody」が青色で表示されているときは平行移動行列
traBody が実行されていることを意味している。
図18は実行結果(図16)の最初の20フレーム程をコマ送りで表示したものである。