Unityプログラム解説 モデルをラグドール化してみた

今回はUnityでVRoidの代4弾として
「UnityでVRoidで作ったラグドール化してみた」
を実装した動画を作ってみました。

今回の動画は撮影自体は2週間前に行ってたんだっけど
編集作業サボって後に回した結果、動画内容を冬月も忘れてたのよね

動画見返しながら動画の編集作業していたんだけど
なーんかよくわかないことを色々していたのでそのへんカットしたら8分と短くなったんだよな

今回も解説していきますが、正直うまく説明できるかわかりません、すみません
冬月なりに解説していいますので、よければ読んでいっていたければと思います

Youtubeでの解説

ラグドール化するの
少しエッチいですね(笑
されてる本人が言うことじゃないとお思いますが

動画内容はエッチな内容は一切ないから安心してね
(編集してる当時はエッチとは一切思ってなかった)

ただ、処理自体はかなりニッチな処理なので
あんまり使いみちはないかもしれないな……

体力0になったときの倒れる時ぐらいしか使いみちがないですもんね……
死体を作る時に便利なんでしょうかね……

プログラム解説していくよ

GitHubはこちら

https://github.com/Syungetu/UnitytoVRMProject

解説はいらん!コードを見ればわかる
といった兄貴姉御は上のリンクで
Unityプロジェクトがダウンロードできますのでご活用ください

スクリプト前のラグドール化の設定部分は
YouTubeで解説してますので、こちらを参考にしてね

ラグドール化処理解説
変数宣言
[Header("オブジェクト設定")]
/// <summary> メインで使っているRigidbody </summary>
public Rigidbody _MainRigidbody;
/// <summary> メインで使っているコライダー </summary>
public BoxCollider _MainBoxCollider;
/// <summary> モデルのボーンルートオブジェクト </summary>
public GameObject _ModelBoonRootObject;
/// <summary> モデルのAnimator </summary>
public Animator _ModelAnimator;

[Header("設定")]
/// <summary> ラグドールフラグ </summary>
public bool _IsRagdoll = false;

/// <summary> ラグドールフラグバッファ </summary>
private bool _BuffIsRagdoll = false;
ラグドール化するにあたって、
ボーン周りの制御も必要になるのでそのあたりを受取る変数を用意しているわ

/// <summary> メインで使っているRigidbody </summary>
public Rigidbody _MainRigidbody;
/// <summary> メインで使っているコライダー </summary>
public BoxCollider _MainBoxCollider;

このあたりは動画でも解説してるが、
もし、ダメージ判定や移動処理をモデルに入れていた場合に処理が必要になる部分だ

モデルをワールドに配置している場合は個々の処理は使いません。
攻撃などの判定でColliderを追加した場合、移動や重力の判定でRigidbodyを追加した場合に使用します

/// <summary> モデルのボーンルートオブジェクト </summary>
public GameObject _ModelBoonRootObject;

前準備でラグドール用のColliderが追加されているので
それを管理するため、ボーンオブジェクトのルートを取得する必要があります。

VRoidで生成したモデルはモデルルートの中にRootというゲームオブジェクトがありますが、それがボーンオブジェクトのルートになるわ

もし、このスクリプトをアタッチするオブジェクトが決まっている場合はここの変数はもっとスマートに変更できるかもしれないな

モデルのルートにアタッチする場合は、その下のRootを選択すればいいので、Start関数などにプログラム側から指定すればいいかと思います


    void Start()
    {
        foreach(Transform child in this.gameObject.transform)
        {
            if(child.gameObject.name == "Root")
            {
                 _ModelBoonRootObject = child.gameObject;
            }
        }
    }

上のような書き方をすれば、VRMのモデル直下にこのスプリクトを置いた場合に自動でGameObjectを読み込みしてくれると思います

/// <summary> モデルのAnimator </summary>
public Animator _ModelAnimator;

で、アニメーションしたままラグドール化するとミミズのようなグネグネ下動きをしてしまうので、ラグドール化した際にアニメーションを停止するためにAnimatorを取得するわ

アニメーションの強さを変化させれば
もがく動きとかも表現できるかもしれないな

処理の中がれとしてはアニメーションを停止させてからラグドール化する感じになります
ラグドール化の有効無効化の振り分け処理

    /// < summary >
    /// 更新処理
    /// < /summary >
    void Update()
    {
        SetChangeRagdoll();
    }

    /// < summary >
    /// ラグドールにするかどうか
    /// < /summary >
    private void SetChangeRagdoll()
    {
        if (_BuffIsRagdoll != _IsRagdoll)
        {
            // 前フレームとフラグの値が変わったら実行させる

            if (_IsRagdoll == true)
            {
                StartCoroutine(SetRagdollAsynchronousProcess());
            }
            else
            {
                StartCoroutine(ReleaseRagdollAsynchronousProcess());
            }

            // バッファに反映
            _BuffIsRagdoll = _IsRagdoll;
        }
    }

    /// < summary >
    /// ラグドール化する(徐々に処理を行わないと当たり判定で吹っ飛ぶので非同期処理とする)
    /// < /summary >
    private IEnumerator SetRagdollAsynchronousProcess()
    {
        if (_MainRigidbody != null)
        {
            // 物理演算をキャンセル
            _MainRigidbody.isKinematic = true;
            // 重力計算をキャンセル
            _MainRigidbody.useGravity = false;

            // 念の為力を0にする
            _MainRigidbody.velocity = Vector3.zero;
            _MainRigidbody.AddForce(0, 0, 0);
        }

        if (_ModelAnimator != null)
        {
            // アニメーションを切らないとビクビクするので切る
            _ModelAnimator.enabled = false;
        }

        if (_MainBoxCollider != null)
        {
            // メインの当たり判定を切っておく(ダメージ用とか)
            _MainBoxCollider.enabled = false;
        }

        // 1フレーム待つ
        yield return null;

        // 各々のラグドール用のコライダーを変化させる
        SetRagdollCollider(_IsRagdoll);

        yield return null;

        // 各々のラグドール用のリジッドボディを有効化する
        SetRagdollRigidbody(_IsRagdoll);

        // リジッドボディのちからを初期化
        SetRagdollRigidbodyForce();
    }

    /// < summary >
    /// ラグドールを解除する(徐々に処理を行わないと当たり判定で吹っ飛ぶので非同期処理とする)
    /// < /summary >
    private IEnumerator ReleaseRagdollAsynchronousProcess()
    {
        // リジッドボディのちからを初期化
        SetRagdollRigidbodyForce();

        yield return null;

        // 各々のラグドール用のコライダーを変化させる
        SetRagdollCollider(_IsRagdoll);

        yield return null;

        // 各々のラグドール用のリジッドボディを無効化する
        SetRagdollRigidbody(_IsRagdoll);

        if (_MainRigidbody != null)
        {
            // 物理演算をキャンセル
            _MainRigidbody.isKinematic = false;
            // 重力計算をキャンセル
            _MainRigidbody.useGravity = true;

            // 念の為力を0にする
            _MainRigidbody.velocity = Vector3.zero;
            _MainRigidbody.AddForce(0, 0, 0);
        }

        if (_ModelAnimator != null)
        {
            // アニメーションを再生する
            _ModelAnimator.enabled = true;
        }

        if(_MainBoxCollider != null)
        {
            // メインの当たり判定を戻しておく
            _MainBoxCollider.enabled = false;
        }

    }

ちょっと処理が長いですが、
ラグドール化の有効化、無効化で別処理で書く必要があったのでこんな感じになりました
    /// < summary >
    /// ラグドールにするかどうか
    /// < /summary >
    private void SetChangeRagdoll()
    {
        if (_BuffIsRagdoll != _IsRagdoll)
        {
            // 前フレームとフラグの値が変わったら実行させる

            if (_IsRagdoll == true)
            {
                StartCoroutine(SetRagdollAsynchronousProcess());
            }
            else
            {
                StartCoroutine(ReleaseRagdollAsynchronousProcess());
            }

            // バッファに反映
            _BuffIsRagdoll = _IsRagdoll;
        }
    }

今回非同期処理で徐々に処理を進める必要があるから
StartCoroutine();
を使って関数を回すようにしているわ

非同期で処理を行うため、Frameをまたいで有効、無効化処理が動くから_BuffIsRagdoll != _IsRagdollで切り替えした最初のFフレームのみ動くようにしているぞ

有効、無効化で処理が走るのでif文で分けた先で処理を分けています
無効化の際も徐々に処理を行うので非同期処理で行っています
ラグドール化処理
    /// < summary >
    /// ラグドール化する(徐々に処理を行わないと当たり判定で吹っ飛ぶので非同期処理とする)
    /// < /summary >
    private IEnumerator SetRagdollAsynchronousProcess()
    {
        if (_MainRigidbody != null)
        {
            // 物理演算をキャンセル
            _MainRigidbody.isKinematic = true;
            // 重力計算をキャンセル
            _MainRigidbody.useGravity = false;

            // 念の為力を0にする
            _MainRigidbody.velocity = Vector3.zero;
            _MainRigidbody.AddForce(0, 0, 0);
        }

        if (_ModelAnimator != null)
        {
            // アニメーションを切らないとビクビクするので切る
            _ModelAnimator.enabled = false;
        }

        if (_MainBoxCollider != null)
        {
            // メインの当たり判定を切っておく(ダメージ用とか)
            _MainBoxCollider.enabled = false;
        }

        // 1フレーム待つ
        yield return null;

        // 各々のラグドール用のコライダーを変化させる
        SetRagdollCollider(_IsRagdoll);

        yield return null;

        // 各々のラグドール用のリジッドボディを有効化する
        SetRagdollRigidbody(_IsRagdoll);

        // リジッドボディのちからを初期化
        SetRagdollRigidbodyForce();
    }

肝心なところは関数化しているのでちょっと分かりづらいと思いますが、3つに処理を区切って行っています

        if (_MainRigidbody != null)
        {
            // 物理演算をキャンセル
            _MainRigidbody.isKinematic = true;
            // 重力計算をキャンセル
            _MainRigidbody.useGravity = false;

            // 念の為力を0にする
            _MainRigidbody.velocity = Vector3.zero;
            _MainRigidbody.AddForce(0, 0, 0);
        }

        if (_ModelAnimator != null)
        {
            // アニメーションを切らないとビクビクするので切る
            _ModelAnimator.enabled = false;
        }

        if (_MainBoxCollider != null)
        {
            // メインの当たり判定を切っておく(ダメージ用とか)
            _MainBoxCollider.enabled = false;
        }

        // 1フレーム待つ
        yield return null;

このあたりの処理がモデルにアタッチさせているRigidbodyやAnimatorを止める処理になるわ
RigidbodyのAddForceを0にしているのは力をなくして吹っ飛ばなくするためよ

yield return null;
は、一旦処理を中断して次フレームに処理を再開する命令になるぞ、ここで一気に処理を行わずにフレームを追って処理を行うことでモデルが吹っ飛ばなくしているな

処理自体は当たり判定のColliderを無効化したり、
アニメーションを無効化したり、Rigidbodyを動かないように設定しています
        // 各々のラグドール用のコライダーを変化させる
        SetRagdollCollider(_IsRagdoll);

        yield return null;

先程の物理演算とかの処理を止めた後、次フレームに入り
今度は個々の処理に入ります、この処理は他でも使うので関数化しています

詳しくは後述すけど、ラグドール化の前準備で追加したColliderを有効化してるわ

        // 各々のラグドール用のリジッドボディを有効化する
        SetRagdollRigidbody(_IsRagdoll);

        // リジッドボディのちからを初期化
        SetRagdollRigidbodyForce();

で、ラグドール用のColliderを有効化してから、ラグドール用の物理演算のRigidbodyを有効化するぞ

このあたりはスクエアオブジェクトを重力落下させる時に使うような処理に似ていますね
ラグドール化の無効化
    /// < summary >
    /// ラグドールを解除する(徐々に処理を行わないと当たり判定で吹っ飛ぶので非同期処理とする)
    /// < /summary >
    private IEnumerator ReleaseRagdollAsynchronousProcess()
    {
        // リジッドボディのちからを初期化
        SetRagdollRigidbodyForce();

        yield return null;

        // 各々のラグドール用のコライダーを変化させる
        SetRagdollCollider(_IsRagdoll);

        yield return null;

        // 各々のラグドール用のリジッドボディを無効化する
        SetRagdollRigidbody(_IsRagdoll);

        if (_MainRigidbody != null)
        {
            // 物理演算をキャンセル
            _MainRigidbody.isKinematic = false;
            // 重力計算をキャンセル
            _MainRigidbody.useGravity = true;

            // 念の為力を0にする
            _MainRigidbody.velocity = Vector3.zero;
            _MainRigidbody.AddForce(0, 0, 0);
        }

        if (_ModelAnimator != null)
        {
            // アニメーションを再生する
            _ModelAnimator.enabled = true;
        }

        if(_MainBoxCollider != null)
        {
            // メインの当たり判定を戻しておく
            _MainBoxCollider.enabled = false;
        }

    }

無効化も、ただ単純に無効化するだけでなく、
順序立てて処理をする必要があります

正直な話を言うと、有効化の逆の手順で無効化にしているわ
ラグドール内のColliderやRigidbodyを無効化した後にモデルのアニメーションやColliderやRigidbodyを有効化する感じね

ただ、モデルのColliderやRigidbodyが有効可になった時に変な力が入って吹っ飛ぶのを防ぐためにAddForceやvelocityの値を0を代入しているぞ

もっとスマートな方法があるとは思うのですが、
冬月は思いつかなかったのでAddForceやvelocityの値を0を代入する方法を使っています

ラグドール用のコライダーを操作する

    /// < summary >
    /// ラグドール用のコライダーを操作する
    /// < /summary >
    /// < param name="value" >< /param >
    private void SetRagdollCollider(bool value)
    {
        Component[] boxColliderComponent = _ModelBoonRootObject.GetComponentsInChildren(typeof(BoxCollider));
        foreach (Component c in boxColliderComponent)
        {
            (c as BoxCollider).enabled = value;
        }

        Component[] sphereColliderComponent = _ModelBoonRootObject.GetComponentsInChildren(typeof(SphereCollider));
        foreach (Component c in sphereColliderComponent)
        {
            (c as SphereCollider).enabled = value;
        }

        Component[] capsuleColliderComponent = _ModelBoonRootObject.GetComponentsInChildren(typeof(CapsuleCollider));
        foreach (Component c in capsuleColliderComponent)
        {
            (c as CapsuleCollider).enabled = value;
        }
    }

先程関数化した中身です
処理としてはボーンオブジェクト内に入っているColliderをすべて取得して有効化無効化すると言った流れになります

ラグドール化する際にColloderの種類が複数使われているのでそれに対応するため、BoxCollider、SphereCollider、CapsuleColliderのすべてのColloderを取得しているわ

_ModelBoonRootObject.GetComponentsInChildren(typeof(BoxCollider));
この処理で、すべての子オブジェクトの指定されたスクリプトを取得できるぞ

typeof(XXXXXX)のXXXXXの部分に任意なクラスを入れればそれのリストが出力されるので、そこに無効化、有効化処理をする感じになります

ラグドール用のリジッドボディを操作する

    /// < summary >
    /// ラグドール用のリジッドボディを操作する
    /// < /summary >
    /// < param name="value" >< /param >
    private void SetRagdollRigidbody(bool value)
    {
        Component[] rigidbodyComponent = _ModelBoonRootObject.GetComponentsInChildren(typeof(Rigidbody));
        foreach (Component c in rigidbodyComponent)
        {
            (c as Rigidbody).isKinematic = !value;
            (c as Rigidbody).useGravity = value;
        }
    }
    /// < summary >
    /// ラグドール用のリジッドボディのちからを初期化する
    /// < /summary >
    /// < param name="value" >< /param >
    private void SetRagdollRigidbodyForce()
    {
        Component[] rigidbodyComponent = _ModelBoonRootObject.GetComponentsInChildren(typeof(Rigidbody));
        foreach (Component c in rigidbodyComponent)
        {
            (c as Rigidbody).velocity = Vector3.zero;
            (c as Rigidbody).AddForce(0,0,0);
        }
    }

ラグドールの接触判定を有効化したら、今度は物理演算を有効化して自由落下させられるようにします

まえのSetRagdollCollider()のときのように、ボーンオブジェクト内のRigidbodyをすべて取得し、それを有効化する感じね

(c as Rigidbody).isKinematic = !value;
が物理演算の反映を飛ばすかどうか(実際には違うけど)

(c as Rigidbody).useGravity = value;
が重力計算を行うかどうかの判定だな

デフォルトではラグドール化する前は
isKinematic = true; と useGravity = false; になっているので
isKinematic = false; と useGravity = true; とするとラグドール化して重落下してモデルが崩れ落ちます

まとめ

これで、ラグドール化できるようになりました。
ちょっと使いみちが限られてしまいますが面白い技術だと思います

爆発に巻き揉まれた時にこの処理を入れると
吹き飛んだときの表現がリアルっぽくなるかな

ただ、このあたりはアニメーション周りも関係有るので
色々流用できる技術ではありそうだな

次回はアニメーションの管理をスクリプトで行う部分を開設予定です

気軽にコメントをどうぞ!

この記事に関すること冬月に聞きたいこと等、小さいことでもコメントしていただける嬉しいです。冬月に直接連絡したい方は下のお問合せフォームをお使いください。(メール送信されます)

内容に問題なければ、下記の「コメントを送信する」ボタンを押してください。※メールアドレスは公開されることは有りません。


reCaptcha の認証期間が終了しました。ページを再読み込みしてください。

Unity | ゲーム製作の関連記事