[unity]Transform 徹底マスター

ヒエラルキー上に置いた GameObject は必ずこの Transform が追加されます。
どちらも座標を扱うコンポーネントですが、使い方に触れつつ、その違いについても紹介していきます。

Transform

3次元のオブジェクトを移動・回転・拡大縮小するためのコンポーネントです。
インスペクターと同じ事をプログラムでも行うことができます。

移動

localPosition を変更することで移動させます。
動画を再現するサンプルコードを、以下に示します。Cube にアタッチしてください。

using System.Collections;
using UnityEngine;

public class Sample : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(move());
    }

    IEnumerator move()
    {
        float move = 0;

        while (true)
        {
            move += 0.0075f;

            float sin = Mathf.Sin(move) * 2.5f;

            transform.localPosition = new Vector3(sin, 0, 0);

            yield return null;
        }
    }
}

✖ transform.localPosition.x は直接変更できない

コードで、例えば transform.localPosition.x = sin のような設定ができない、という事です。
必ず new Vector3(x, y, z) で全ての値を更新する必要があります。
このため、現在の位置から 0.1 ずつ X移動させたい場合のコードは次のようになります。

    Vector3 vec = transform.localPosition;
    vec.x += 0.1f;
    transform.localPosition = vec;

これは移動だけではなく、Vector2・Vector3 を使うものすべてに必要な所作です。

回転

localEulerAngles を変更することで回転させます。0~360 度で値を表現します。

localRotation というメンバーもありますが、こちらはインスペクターとは異なり、Quaternion 型で扱いづらい変数ですので、ここでは説明しません。

動画と同じ動作をするコードを以下に示します。コルーチンのみ紹介するので、先ほどのコードに追記してください。

    IEnumerator rotate()
    {
        float rot = 0;

        while (true)
        {
            rot += 0.5f;
            if (rot >= 360)
            {
                rot -= 360;
            }

            transform.localEularAngles = new Vector3(rot, rot, 0);

            yield return null;
        }
    }

localEularAngles は、インスペクタの Rotation と同じ値になるわけではありません。

拡大・縮小

localScale を変更することで拡大・縮小させます。1.0 が元のスケールサイズです。
動画と同じ動作をするコードを以下に示します。コルーチンのみ紹介するので、先ほどのコードに追記してください。

    IEnumerator scale()
    {
        float scale = 0;

        while (true)
        {
            scale += 0.0075f;

            float sin = Mathf.Sin(scale) * 1.5f;

            transform.localScale = new Vector3(sin, sin, sin);

            yield return null;
        }
    }

親子関係

ヒエラルキーではこのように、オブジェクトに階層をもたせることが出来ます。
親を Parent、子を Child と呼び、メソッドもそれらの名前で用意されています。

子階層は必ず親階層の影響を受けます。
例えばスケールをそれぞれ GameObject1=2、GameObject2=3、GameObject3=4 とした場合、Cube のスケールは 2 x 3 x 4 = 24 倍になります。

SetParent

親をセットする時に用いるメソッドです。
Instantiate の第2引数でも親オブジェクトを決めることが出来るので、多くはそれで事足りるかもしれません。

1階層下の子供リスト

構文は少し特殊です。ちょっと C# の理に詳しい人ほど「?」となりそう。

    foreach(var t in transform)  
    {
        Debug.Log(t.name);  
    }

全階層で、特定のコンポーネントがアタッチされた子供リスト

例えば Sample コンポーネントをアタッチしたオブジェクトを全て参照する時は GetComponentsInChildren を使います。
本当に全て取得する場合は、必ず存在する Transform をアタッチしたオブジェクトにするといいでしょう。

このメソッドの場合、自分がコンポーネントをアタッチしていると、自分自身も含まれてしまうことに注意してください。

    var children = transform.gameObject.GetComponentsInChildren<Sample>().ToList();
    // 親は省く
    children.Remove(transform);

    foreach(var t in children)  
    {
        Debug.Log(t.name);  
    }

ワンポイント

扱う上で、いくつかのポイントを紹介します。

localPosition と position の違い

「親子関係」で説明したように、子オブジェクトは親の影響を受けます。
position 受けた親の影響も加算された数字です。

例を示します。

▼ GameObject1 (X:5 Y:0 Z:0)
 ▼ GameObject2 (X:0 Y:5 Z:0)
  ▼ GameObject3 (X:0 Y:0 Z:5)
   ▼ Cube (X: -2.5~2.5 の間を移動)

このような親子関係があった時、CubeX の localPosition は (-2.5, 0, 0) ~ (2.5, 0, 0) の間を行き来しますが、position は (2.5, 5, 5) ~ (7.5, 5, 5) の間を行き来します。

子オブジェクトのみ親も含めた計算値
localPositionposition
localEulerAngleseulerAngles
localScalelossyScale (※)回転しない場合

このような対応表となります。
positioneulerAngles は直接変更することもできます……が、親子関係を維持したまま更新されます。

lossyScale は変更もできず、また回転されたオブジェクトでは値に信頼性がないので、扱いには気をつけてください。

基本的には local で済ませられる方が、想定外の副作用も起こらずスマートにプログラムを組むことができます。position や eulerAngles を使うのは「どうしようもない時だけ」に限定しましょう。

うっかり親なしで position、eulerAngles 触ってたのに、後から親が増えて破綻するシナリオが多い

処理落ちでスピードが変わらないようにするために

先ほど紹介したコードは、描画フレーム数(fps)に依存します。
そのため、オブジェクトが大量に表示されるなど、重い処理が行われた時に動きの速度が変わってしまうという問題を抱えています。

例えば先ほどの例を fps250 と、fps60 でそれぞれ実行した時の速度を比較します。

fps250
fps60

これではゲーム体験が変わってしまいますし、オンラインゲームでは処理落ちした人だけ損をする、という事にもなりかねません。

そうならないために、fps に応じてスケール値の変更スピードを可変にする必要があります。

難しそうに聞こえるかもしれませんが、前述の scale() を 1 行変更するだけです。
Time.deltaTime を掛け合わせるのがポイント。

    IEnumerator scale()
    {
        float scale = 0;

        while (true)
        {
            scale += 2f * Time.deltaTime;

            float sin = Mathf.Sin(scale) * 1.5f;

            transform.localScale = new Vector3(sin, sin, sin);

            yield return null;
        }
    }
fps250
fps60

このようにほとんど違いがなくなりました。
ただし、Time.deltaTime をかければどれだけ処理落ちしてもいい……というわけではありません。
試しに fps15 になった状態も紹介します。

fps15

たしかに速度は変わりませんが、見た目がカクカクしているのがわかると思います。
この状態だとキー入力のレスポンスも悪くなることが多く、ユーザーエクスペリエンスは著しく落ちてしまいます。

参考までに、fps60 が多くのゲームにおいてヌルヌルとした動きを産み出します。
ただし、オンラインゲームやハイポリゴンのゲームは処理を最適化することが難しいので fps30 を基本としていることが多いです。
PC だと fps200 といったスペック任せのフレーム数を出すことができます。対戦ゲームなどで、PC のスペックがいいほど有利なのはこの部分が大きいです。

ゲーム製作において、ターゲット機で常時 fps30 を割っているのであればプログラム技術が足りないか、表示物がリッチすぎるか、システムになんらかの問題を抱えているといっていいでしょう。
PC ではヌルヌルしてたのに Android に持っていた途端話にならないくらいカクカクになる、なんて事は非常によくあるケースなので、早めにターゲット機でのテストをしておくようお勧めします。

それで発売できなくなったゲーム、結構あります

transform はキャッシュしておく

Monobehaviour が属する gameObject は直接 transform と書くことでアクセスできますが、パフォーマンスを気にするのであれば変数をキャッシュしておくと更に高速にアクセスできます。
キャッシュされた t の方が transform の 4~5 割程度の処理速度で済むようです。

「キャッシュ」というと大げさに聞こえますが、ただ単に Awake で変数に transform を格納し、それを使っているだけです。

// transform のキャッシュ
Transform t;

void Awake()
{
    t = transform;
}

void Update()
{
    t.localPosition = new Vector3(0, 0, 0);
}
// 子リストをキャッシュ
List<Transform> children;

void Awake()
{
    // 子供のリストを取得
    children = transform.gameObject.GetComponentsInChildren<Transform>().ToList();
    // - 自分自身
    children.Remove(transform);
}

void Update()
{
    children.ForEach( t => t.localPosition = new Vector3(0, 0, 0) );
}

GetComponent<Transform> は transform にくらべて 1 割増し程度。そこまで変わりませんでした。
むしろ transform が思ったより重い印象。

頻繁に transform を使う場合は速度ロスを防ぐため、なるべくキャッシュを行いましょう。

Camera.main も似たような理由で、キャッシュしておいた方がいいメンバーです。

RectTransform

長くなりましたので、RectTransform については別記事にしました。
よければ合わせてご覧ください。

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA