準備
Hello, World の最後で、キューブを積み上げて、「Hello, World」を表示するというスクリプトを示したが、
各キューブは静止したままで、動きがなく、Unity アプリとしてはちょっと寂しい状況だった。
なので本稿では、キューブにスクリプトをアタッチし、キューブの位置を動的に変更する(いわゆる、キューブを移動させる)スクリプトを書いてみよう。
そのための準備として、まずは下図のようにシーンに床と移動させるキューブを配置する。
移動するキューブだけだと、移動していることがわかりづらいので、比較対象としての床も配置しておこう、というわけだ。
キューブは GameObject > 3D Object > Cube メニューを実行して生成する。
キューブを生成したら、インスペクタでy 座標を 0.5 に設定しておこう。
床を y = 0 の位置に生成するので、キューブがめり込まなくするためだ(キューブ座標はキューブ中心点の座標)。
床もキューブで生成するので、同様に Cube を生成し、F2 を押すか、ヒエラルキービューで右ボタンクリック>Rename を選び「Floor」という名前にしておこう。 これは、わかりやすくするためだけで、現状では必ずしもリネームする必要は無い。
後は、インスペクタで Scale を変更することで、床らしい形状にしておこう。
次に、Assets > Create > C# Script でスクリプトを作成し、MoveCube とかにリネームしよう。
そして、先に作ったキューブにドロップし、スクリプトをキューブにアタッチしよう。
Hello, World で作ったスクリプトはカメラにアタッチしたが、今回はキューブをスクリプトで操作するので、キューブにアタッチする。
Unity では、オブジェクトを操作したい場合は、スクリプトを生成し、それのオブジェクトにアタッチする。
全体をコントロールするスクリプトを作り、それが全てのオブジェクトをコントロールするような方式も不可能ではないが、
各オブジェクトごとにスクリプトを持つ方が、オブジェクトを部品化できる優位点があるとことだ。
これで、準備は完了だ。次の章で、実際にキューブを移動させるスクリプトを作成しよう。
単純な移動
下図(クリックすると、ようつべの動画に飛びます)のように、キューブを x 軸方向に単純に移動するようにしてみよう。
そのためのコードは単純で、キューブにアタッチしたスクリプトに以下のように記述する。
void Update() { transform.position += new Vector3(0.1f, 0f, 0f); }
Update() は毎フレーム(通常は 60FPS または 30FPS)コールされる関数だ。
その中で、キューブオブジェクトの座標を表す transform.position を更新する。
position の型は3次元座標を表す Vector3 なので、new Vector3(0.1f, 0f, 0f) を足し込んでいる。
※ Vector3 は3次元座標を保持するクラス。x, y, z メンバ変数を持つ。
毎フレームごとに、キューブの座標が更新されるので、プレイすると、キューブの x 座標だけが増加していき、 画面の左下方向にひゅーんと移動する、というわけだ。
演習問題:
- 上記スクリプトを作成・オブジェクトにアタッチし、実際に動作させてみなさい。
- 足し込むベクターを (1f, 0f, 0f) に変更すると何が起こるかを予想し、実際に試してみなさい。
- キューブを y 軸方向に移動するスクリプトを書きなさい。
void Update() { transform.position += new Vector3(0f, 0.1f, 0f); }
一定速度の移動
実は Update() は必ずしも正確なタイミングで呼ばれるとは限らない。
他の処理に時間を要すると、その分呼ばれる頻度が落ちることがあるからだ。
なので、オブジェクトを正確に一定速度で移動させるには、経過時間を考慮して座標値を更新しないといけない。
そのためには、下記のように Time.deltaTime を使う。
void Update() { transform.position += new Vector3(2f*Time.deltaTime, 0f, 0f); }
Time.deltaTime には、以前に Update() が呼ばれてからの経過秒数が float 型で入っている。
例えば20ミリ秒経過していれば、0.02 が入っている。
なので、それに秒速をかけて、位置に足し込めば、一定速で移動するようになるというわけだ。
一般的に、x軸方向の秒速を speed とすれば、new Vector3(speed * Time.deltaTime, 0f, 0f) を足し込めばよいことになる。
演習問題:
- キューブを、上(y軸)方向に初速 5/秒 で打ち上げ、下方向に毎秒1の加速度を加え、キューブを放物線運動させなさい。
float m_speed = 5.0f; // y軸方向初速 void Update() { transform.position += new Vector3(0f, m_speed * Time.deltaTime, 0f); m_speed -= 1*Time.deltaTime; // 下方向加速度を速度に反映 }
往復運動
これまでのコードではキューブが画面外に飛び出してしまった。今度はキューブが往復運動し、いつまでも画面内にとどまるようにしてみよう。
そのためにはまず、キューブが x 座標のプラス方向に向かって移動しているのか、マイナス方向に移動しているのかを示すフラグを導入する。
public class MoveCube : MonoBehaviour { bool m_xPlus = true; // x 軸プラス方向に移動中か? ..... }
次に、Update() では、そのフラグを参照し、x 座標を増加または減少させる。
あとは、折り返したい位置に来たら、フラグを変更するだけだ。
コードは以下のように書ける。
void Update () { if( m_xPlus ) { transform.position += new Vector3(2f*Time.deltaTime, 0f, 0f); if( transform.position.x >= 4 ) m_xPlus = false; } else { transform.position -= new Vector3(2f*Time.deltaTime, 0f, 0f); if( transform.position.x <= -4 ) m_xPlus = true; } }
上記コードは分かりやすく書くことを重視しているので、ちょっと冗長かもしれないが、これは単なる例なので細かいツッコミはご勘弁ねがいたい。
演習問題:
- キューブが (-4, 0.5, -4) → (4, 0.5, -4) → (4, 0.5, 4) → (-4, 0.5, 4) → (-4, 0.5, -4) のように周回するようにしなさい。
enum Dir { XPLUS = 0, ZPLUS, XMINUS, ZMINUS, }; Dir m_dir = Dir.XPLUS; void Start() { transform.position = new Vector3(-4f, 0.5f, -4f); } void Update() { switch( m_dir ) { case Dir.XPLUS: transform.position += new Vector3(2f*Time.deltaTime, 0f, 0f); if( transform.position.x >= 4f ) { m_dir = Dir.ZPLUS; } break; case Dir.ZPLUS: transform.position += new Vector3(0f, 0f, 2f*Time.deltaTime); if( transform.position.z >= 4f ) { m_dir = Dir.XMINUS; } break; case Dir.XMINUS: transform.position += new Vector3(-2f*Time.deltaTime, 0f, 0f); if( transform.position.x <= -4f ) { m_dir = Dir.ZMINUS; } break; case Dir.ZMINUS: transform.position += new Vector3(0f, 0f, -2f*Time.deltaTime); if( transform.position.z <= -4f ) { m_dir = Dir.XPLUS; } break; } }
パラメータ指定による移動
前章のようなコーディングでは、座標値などがハードコーディングされているために、オブジェクトの移動コースを容易に変更することができない。
なので本章では、座標をデータ化し、位置を線形補完により指定する書き方を紹介する。
※「線形補間」とは、2つの座標 p1. p2 の間の直線上の点を生成するものだ
float m_progress = 0f; // 進捗 [0, 1) int m_ix = 0; // 現データインデックス Vector3[] m_data = {new Vector3(-4f, 0.5f, -4f), new Vector3( 4f, 0.5f, -4f), new Vector3( 4f, 0.5f, 4f), new Vector3(-4f, 0.5f, 4f), new Vector3(-4f, 0.5f, -4f)};
まずは、上記のようにメンバ変数を追加する。
m_data は3次元座標を表す Vector3 のデータ配列だ。最初と最後の座標が同じなのは、ループしたとき、ちゃんと繋がるようにするためだ。
(現状の)C# では、C++ のように Vector3[] m_data = { {-4f, 0.5f, -4f), ... }; と、コンストラクタの指定を省略できず、
上記のように 必ず new Vector3() を記述しなくてはいけないようだ。C++er にはちょっとまどろっこしい仕様だ。
m_ix はオブジェクトの位置が、どの位置にいるかを示すものだ。オブジェクトは m_data[m_ix], m_data[m_ix+1] 間にあるものとする。
m_progress はオブジェクトが m_data[m_ix], m_data[m_ix+1] 間のどの位置にあるかを示すものだ。0 ならば m_data[m_ix] に、 1 ならば m_data[m_ix+1] にオブジェクトがあるとする。その間は線形に補完されるものとする。
void Start () { transform.position = m_data[m_ix]; }
初期化では、上記のようにオブジェクトの位置を m_data[m_ix] に設定する。
m_ix は 0 に初期化されているので、m_data[0] の座標、すなわち (-4f, 0.5f, -4f) が設定される。
void Update () { m_progress += 0.5f * Time.deltaTime; if( m_progress >= 1.0f ) { m_progress = 0f; if( ++m_ix >= m_data.Length - 1) m_ix = 0; } transform.position = Vector3.Lerp(m_data[m_ix], m_data[m_ix+1], m_progress); }
Update() での処理は上記のように記述できる。
まずは、m_progress += 0.5f * Time.deltaTime; を時間間隔に比例して増加させる。0.5f を乗じているので、m_progress は2秒間で 1.0f になることになる。
m_progress は [0, 1) なので、1.0f に達した場合は m_progress を 0 にリセットし、座標データインデックスの m_ix をインクリメントする。
m_ix が最後のデータに達した場合は、m_ix をリセットする。
最後に Vector3.Lerp(座標1, 座標2, t) を使って、座標1, 2 間を線形補間する。線形補間を自前で書くのはちょっとばかしやっかいだが、 Unity には Vector3.Lerp() という便利な関数が用意されているので、それを使うだけだ。
演習問題:
- 座標データを増やしたり、中身を変更したりすると、キューブがそれにしたがって移動することを確認しなさい。
- ここに示したコードでは、各座標間の距離が近くても遠くても2秒で移動してしまい、速度が一定ではない。
例えば、Vector3[] m_data の4行目を new Vector3(-10f, 0.5f, 10f), に変え、
1行目と2行目の間に new Vector3( 0f, 0.5f, -4f), を追加した場合でも、
移動速度が一定になるよう、コードを修正しなさい。
円運動
最後に、キューブをxz平面で円運動させてみよう。
コードは以下のようになる。
void Update () { var x = 5 * Mathf.Sin(Time.time); var z = 5 * Mathf.Cos(Time.time); transform.position = new Vector3(x, 0.5f, z); }
オブジェクトを一定速度で移動させる章で出てきた Time.deltaTime は以前に Update() がコールされてからの秒数を返したが、
Time.time は float 型で、ゲームがスタートしてからの経過秒数を返します。
また、Mathf.Sin(), Mathf.Cos() は Unity が用意している float 型の三角関数だ。
なので、Mathf.Sin(Time.time)、Mathf.Cos(Time.time) により、約6.28秒周期の円運動座標を生成することができるのだ。
演習問題:
- xz平面ではなく、xy 平面で円運動するようにコードを修正しなさい。
- キューブが1回転する時間が1秒になるようコードを修正しなさい。
- Time.time を使用せず、Time.deltaTime を使ってキューブが円運動するようコードを修正しなさい。
void Update () { var x = 5 * Mathf.Sin(Time.time); var y = 5 * Mathf.Cos(Time.time); transform.position = new Vector3(x, y, 0f); }
void Update () { const float PAI = 3.1415926535f; var x = 5 * Mathf.Sin(Time.time*2*PAI); var y = 5 * Mathf.Cos(Time.time*2*PAI); transform.position = new Vector3(x, y, 0f); }
float m_theta = 0f; void Update () { m_theta += Time.deltaTime; var x = 5 * Mathf.Sin(m_theta); var y = 5 * Mathf.Cos(m_theta); transform.position = new Vector3(x, y, 0f); }
まとめ
- スクリプトをオブジェクトにアタッチすることで、オブジェクトを操作できるぞ。
- Update() で、オブジェクトの transform.position を設定することで、オブジェクトの位置を変更することで、アニメーションできるぞ。
- 負荷によらずオブジェクトを一定速度で移動させたい場合は Time.deltaTime を使うといいぞ。
- 座標値を線形補間したい場合は Vector3.Lerp(座標1, 座標2, t) を使うといいぞ。