Unityで親子関係にないGameObject同士を親子みたいに移動・回転させる

# やりたいこと

こんな感じで、親子関係にないGameObject同士を、親子みたいに動かしたかった。
![virtual_child.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222054/de8164bf-7f0b-bc0f-a333-acc120d96181.gif)
VRコンテンツ開発中に、手でつかむものをControllerのGameObjectの子にしたくなかったのがきっかけ。

# スクリプト

```CS:VirtualChildBehaviour
public class VirtualChildBehaviour : MonoBehaviour
{
    // 手のローカル座標を用いた、ターゲットへの相対位置
    private Vector3 _relavivePosition;
    // virtualParentのローカル座標を用いた、virtualParent.rotation -> virtualChild.rotationへの回転軸
    private Vector3 _parentLocalAxis;
    // virtualParent.rotation -> virtualChild.rotationへの回転量
    private float _rotationAngle;

    public Transform VirtualParent { get; private set; } = null;

    public Transform VirtualChild => transform;

    public bool ExistVirtualParent => VirtualParent != null;

    /// <summary>
    /// 親子関係のような振る舞いをさせたいオブジェクトを登録する
    /// </summary>
    /// <param name="virtualParent"></param>
    /// <returns></returns>
    public void RegisterParent(Transform virtualParent)
    {
        if (ExistVirtualParent)
        {
            Debug.LogWarning($"[VirtualChildBehaviour] '{VirtualChild.name}'はすでに'{VirtualParent.name}'の子供として登録されています。");
            return;
        }

        VirtualParent = virtualParent;

        // VirtualParentのローカル座標系で表した場合のターゲットオブジェクトの位置
        _relavivePosition = VirtualParent.InverseTransformPoint(VirtualChild.position);

        // 登録直後の状態のVirtualParentのrotationをVirtualChildのrotationに一致させるための回転行列を求める。
        // 求めたい回転行列をAとし, VirtualChild, VirtualParentのrotationをC,Pとすると、C = A * Pであり、
        // 両辺からPの逆行列（Inverse(P)）をかけると A = C * Inverse(P)
        var rotationMatrix = VirtualChild.rotation * Quaternion.Inverse(VirtualParent.rotation);

        // rotationMatrixはグローバル座標を使って表現されているため、virtualParentが少しでも回転した後は使えないが、
        // Quaternionから回転量（angle）と回転軸（axis）を抽出すると、angleはそのまま使える
        rotationMatrix.ToAngleAxis(out _rotationAngle, out Vector3 grobalAxis);

        // axisはVirtualParentのローカル座標で表現しておき、
        // 使うタイミングでVirtualParentのrotationをもとにグローバル座標へ変換し直す
        _parentLocalAxis = VirtualParent.InverseTransformVector(grobalAxis);
    }

    /// <summary>
    /// バーチャルな親子関係を破棄する
    /// </summary>
    /// <returns></returns>
    public void UnregisterParent()
    {
        VirtualParent = null;
    }

    private void LateUpdate()
    {
        if (ExistVirtualParent == false) return;

        // ローカル座標 -> グローバル座標へ変換
        var position = VirtualParent.TransformPoint(_relavivePosition);
        var grobalAxis = VirtualParent.TransformVector(_parentLocalAxis);

        // (回転行列) * 親のrotation
        var rotation = Quaternion.AngleAxis(_rotationAngle, grobalAxis) * VirtualParent.rotation;

        VirtualChild.SetPositionAndRotation(position, rotation);
    }
}
```

```CS:VirtualChildBehaviourSample
[RequireComponent(typeof(VirtualChildBehaviour))]
public class VirtualChildBehaviourSample : MonoBehaviour
{
    private VirtualChildBehaviour _virtualChild;
    public Transform virtualParent;

    private void Awake()
    {
        _virtualChild = GetComponent<VirtualChildBehaviour>();
        _virtualChild.RegisterParent(virtualParent);
    }
}
```
Childのように動かしたいGameObjectの方にVirtualChildBehaviourSample、VirtualChildBehaviourをアタッチして、インスペクターからvirtualParentに親にしたいGameObjectを登録すればOK。

Scaleの変化には未対応。

# あとがき

めちゃくちゃ混乱して、これだけで2日くらいかかってしまった。
圧倒的数学力不足。

# 参考

- [【Unity道場 博多スペシャル 2017】クォータニオン完全マスター](https://www.youtube.com/watch?v=uKWLPU8gfIY)
- [TransformクラスのTransform**** / InverseTransform****系メソッドのメモ](http://edom18.hateblo.jp/entry/2018/04/26/214315)