Unityでのオブジェクトの親子関係は非常によく使うので、この話題を扱ったわかりやすいブログ記事など多いのですが、いざやってみるとなかなか混乱することが多いので、自分が理解した範囲でまとめてみます。(いろいろ間違っているかもしれませんので、その際はご指摘ください!)
まず、親子関係にすることのメリットとデメリットです。
■メリット
・親を移動・回転・拡大縮小したときに子も同様に移動・回転・拡大縮小される
・子を移動するときに、親の位置との相対指定で移動ができる
→特に、親が回転している場合でも回転のことを考えなくてよい
■デメリット
・子の移動・回転・拡大縮小が親のScaleの値に影響されるため、直感的な指定ができない
サンプルとして、Scaleが(2,2,2)のCube1と(1,1,1)のCube2を用意しました。
Positionは、Cube1が(0,0,0)、Cube2が(2,0,0)です。
この状態で、HierarchyでCube2をCube1にドラッグ&ドロップすると、Cube2はCube1の子オブジェクトとなります。この時、Scene上では変化はありません。同時に、Cube2のInspectorのScaleの値は(0.5,0.5,0.5)となり、Cube1からの相対サイズになります。
以上から、ワールド座標系の値からローカル座標系の値を求める計算式は下記になります。
子のワールド座標のサイズ = 親のサイズ × 子のローカルサイズ
→ 子のローカルサイズ = 子のワールド座標のサイズ / 親のサイズ
そして、Positionについても相対位置に変化しています。
ここで勘違いしやすいのが、相対位置というのは単に原点が親オブジェクトの位置になるだけではなく、距離についても(サイズと同じく)親オブジェクトのScaleが影響してくるということです。
今回の場合、Cube2のPosition(ローカル座標)は(1,0,0)に更新されました。
見た目(Scene)上は2m離れているのに、数値上は1mしか離れていない、ということになるので混乱しやすい部分です。(位置についても上記の計算式になります)
また、回転についても親のScaleが影響するので、親が立方体でない状態(x,y,zの値が異なる状態)で子を回転させると、平行四辺形のような予想外の形状になると思います。子を回転させる場合は、親が(1,1,1)などの立方体の状態に限る、と覚えておいた方が良さそうです。
ここまでは手動で親子関係を作成しましたが、スクリプトで操作する場合は、
[子オブジェクト].transform.SetParent([親オブジェクトのTransform],worldPositionStays)
という関数を使用します。SetParentの第二引数のworldPositionStaysがポイントとなります。
先ほどHierarchy上で手動で親子関係を作成した場合は、子オブジェクトの見た目は全く変わらずに親子関係ができました。これと同じ操作をするには、worldPositionStaysをtrueにします。
逆にfalseにすると、PostionとScaleは子オブジェクトのものがそのまま引き継がれるのですが、親オブジェクトのScaleによる係数の影響で、実際の位置やサイズは変化します。
worldPositionStaysまとめ
true → ワールド座標上での位置・サイズ・回転は変化しないが、インスペクタの値は親のScaleに応じて変わる
false → インスペクタの値は変化しないが、ワールド座標上での位置・サイズ・回転は親のScaleに応じて変わる
なお、worldPositionStaysはInstantiateの引数としても指定することができるので、インスタンス化と同時に子オブジェクトにする際にも使用できます。
では、実例として、任意に操作可能なオブジェクト(例えばVuforiaなどで発見したARマーカーやVRのハンドコントローラー)の1m横に、別の0.5m四方のオブジェクトを生成する、というのをやってみます。
ARマーカーやハンドコントローラーはRotationが様々で事前に予想することができないため、相対位置指定が必要となります。また、表示したい位置もサイズもワールド座標系で指定されているので、変換が必要になります。
//配置したいオブジェクト public GameObject prefab; //表示したい相対位置(ワールド座標系) Vector3 pos = new Vector3 (1f, 0, 0); //設定したいサイズ(ワールド座標系) Vector3 size = new Vector3 (0.5f, 0.5f, 0.5f); //基準となる親オブジェクト(このスクリプトをアタッチする) var marker = this.gameObject; //配置したいオブジェクトをインスタンス化 var obj = Instantiate(prefab); //親子関係を作る(worldPositionStaysはfalse) obj.transform.SetParent (marker.transform, false); //ワールド座標系→ローカル座標系の係数を作成 Vector3 parentScaleInverse = new Vector3 (1f / marker.transform.localScale.x, 1f / marker.transform.localScale.y, 1f / marker.transform.localScale.z); //サイズと位置を変換して設定(Vector3.Scaleは要素毎に掛け算) obj.transform.localScale = Vector3.Scale (size, parentScaleInverse); obj.transform.localPosition = Vector3.Scale (pos, parentScaleInverse);
SetParent時にworldPositionStays = falseとしているのは、親オブジェクトの回転に子オブジェクトを合わせるためです。ここをtrueとし、子オブジェクトの回転を保ってしまうと、親オブジェクトに追従せずおかしな動きになります。
実行した結果です。
基準となるオブジェクトが回転していても追従しているのがわかります。
実はワールド座標系とローカル座標系を変換する関数はUnityに用意されているのですが、そのまま使うよりも上記のことを理解してから使うとより使いやすいかと思います。