このエントリーをはてなブックマークに追加

 

Unity C# Script プログラミング 入門
ゲームオブジェクトを移動してみよう
Copyright (C) 2015 by Nobuhide Tsuda

 

※ イラスト提供:かわいいフリー素材集「いらすとや」様

準備

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 座標だけが増加していき、 画面の左下方向にひゅーんと移動する、というわけだ。

演習問題:

  1. 上記スクリプトを作成・オブジェクトにアタッチし、実際に動作させてみなさい。
  2. 足し込むベクターを (1f, 0f, 0f) に変更すると何が起こるかを予想し、実際に試してみなさい。
  3. キューブを y 軸方向に移動するスクリプトを書きなさい。
  4.     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) を足し込めばよいことになる。

演習問題:

  1. キューブを、上(y軸)方向に初速 5/秒 で打ち上げ、下方向に毎秒1の加速度を加え、キューブを放物線運動させなさい。
  2.     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;
        }
    }

上記コードは分かりやすく書くことを重視しているので、ちょっと冗長かもしれないが、これは単なる例なので細かいツッコミはご勘弁ねがいたい。

演習問題:

  1. キューブが (-4, 0.5, -4) → (4, 0.5, -4) → (4, 0.5, 4) → (-4, 0.5, 4) → (-4, 0.5, -4) のように周回するようにしなさい。
  2.     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() という便利な関数が用意されているので、それを使うだけだ。

演習問題:

  1. 座標データを増やしたり、中身を変更したりすると、キューブがそれにしたがって移動することを確認しなさい。
  2. ここに示したコードでは、各座標間の距離が近くても遠くても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秒周期の円運動座標を生成することができるのだ。

演習問題:

  1. xz平面ではなく、xy 平面で円運動するようにコードを修正しなさい。
  2.     void Update () {
            var x = 5 * Mathf.Sin(Time.time);
            var y = 5 * Mathf.Cos(Time.time);
            transform.position = new Vector3(x, y, 0f);
        }
    
  3. キューブが1回転する時間が1秒になるようコードを修正しなさい。
  4.     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);
        }
    
  5. Time.time を使用せず、Time.deltaTime を使ってキューブが円運動するようコードを修正しなさい。
  6.     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) を使うといいぞ。