Spring Joint 2D で ソフトボディを作る

この記事は、Unityアドベントカレンダー25日目の記事です。

# はじめに
Unity の Spring Joint 2D を利用することで、やわらかい物体(**ソフトボディ**)を作ることができます。
![SpringJoint2Dによるソフトボディ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/199773b3-69ae-9ee4-b1b4-4190530eef77.gif)

# GitHubリポジトリ
https://github.com/rngtm/UnityAdvent2021-Softbody

# ソフトボディの作り方
パーティクル(Rigidbody)をばね(Spring Joint 2D)で接続すると、
ソフトボディのようにふるまうオブジェクトを作ることができます。

![パーティクルを結ぶ@3x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/9eb9ed26-af09-6b96-8a9f-16e02a401d2f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/ca9cfeab-3dca-770a-c44e-fded29292df5.png)

# ソフトボディの実装


<details><summary>ソフトボディの実装コード</summary><div>

ソフトボディのパーティクルを表現するクラス(SoftbodyParticle)を用意します。

```SoftbodyParticle.cs
using UnityEngine;

public class SoftbodyParticle : MonoBehaviour
{
    [SerializeField] private new Rigidbody2D rigidbody;

    public Rigidbody2D Rigidbody => rigidbody;
}

```

次に、ソフトボディ(Softbody2D)を実装します。
SoftbodyParticleを円周上に並べ、SpringJoint2Dで接続しています。

```Softbody2D.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using Unity.VisualScripting;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;

[CustomEditor(typeof(Softbody2D))]
public class Softbody2DEditor : Editor
{
    public override void OnInspectorGUI()
    {
        if (GUILayout.Button("Create"))
        {
            var component = target as Softbody2D;
            if (component != null)
            {
                component.Create();
            }
        }
        
        base.OnInspectorGUI();
    }
}
#endif

public class Softbody2D : MonoBehaviour
{
    [Header("=== Create Settings ===")]
    [SerializeField] private int particleCount = 32;
    [SerializeField] private float softbodyRadius = 4f;
    [SerializeField] private SoftbodyParticle _particlePrefab;
    [SerializeField] private bool aroundConfigureDistance = false;
    [SerializeField] private bool centerConfigureDistance = false;
    
    [Header("=== Runtime Settings ===")]
    [SerializeField] private float aroundFrequency = 25f;
    [SerializeField] private float centerFrequency = 5f;

    [Header("=== Cache ===")]
    [SerializeField] private SoftbodyParticle[] aroundParticles;
    [SerializeField] private SoftbodyParticle centerParticle;
    [SerializeField] private List<SpringJoint2D> aroundJoints = new List<SpringJoint2D>();
    [SerializeField] private List<SpringJoint2D> centerJoints = new List<SpringJoint2D>();

    private void Update()
    {
        foreach (var joint in aroundJoints)
        {
            joint.frequency = aroundFrequency;
        }
        foreach (var joint in centerJoints)
        {
            joint.frequency = centerFrequency;
        }
    }

    private void OnDrawGizmos()
    {
        Gizmos.matrix = Matrix4x4.Translate(new Vector3(0, 0, -4f));
        foreach (var joint in aroundJoints)
        {
            Gizmos.color = Color.red;
            if (joint != null)
                Gizmos.DrawLine(joint.transform.position, joint.connectedBody.position);
        }
        foreach (var joint in centerJoints)
        {
            Gizmos.color = Color.yellow;
            if (joint != null)
                Gizmos.DrawLine(joint.transform.position, joint.connectedBody.position);
        }
    }

    /// <summary>
    /// ソフトボディの作成
    /// </summary>
    public void Create()
    {
        CreateParticles();
        CreateJoint();
    }

    /// <summary>
    /// パーティクルをJointで接続
    /// </summary>
    private void CreateJoint()
    {
        aroundJoints.Clear();
        centerJoints.Clear();
        
        // 外周パーティクル同士を接続
        for (int i = 0; i < aroundParticles.Length; i++)
        {
            var p1 = aroundParticles[i];
            var p2 = aroundParticles[(i + 1) % aroundParticles.Length];

            var joint1 = p1.gameObject.AddComponent<SpringJoint2D>();
            var joint2 = p2.gameObject.AddComponent<SpringJoint2D>();

            joint1.connectedBody = p2.Rigidbody;
            joint2.connectedBody = p1.Rigidbody;

            aroundJoints.Add(joint1);
            aroundJoints.Add(joint2);
        }
        
        // 中心パーティクルと外周パーティクルを接続
        for (int i = 0; i < aroundParticles.Length; i++)
        {
            var p1 = aroundParticles[i];
            var p2 = centerParticle;

            var joint1 = p1.gameObject.AddComponent<SpringJoint2D>();
            var joint2 = p2.gameObject.AddComponent<SpringJoint2D>();

            joint1.connectedBody  = p2.Rigidbody;
            joint2.connectedBody  = p1.Rigidbody;
            
            centerJoints.Add(joint1);
            centerJoints.Add(joint2);
        }
        
        // Jointの設定
        foreach (var joint in aroundJoints)
        {
            joint.autoConfigureDistance = aroundConfigureDistance;
            joint.enableCollision = false;
        }
        foreach (var joint in centerJoints)
        {
            joint.autoConfigureDistance = aroundConfigureDistance;
            joint.enableCollision = false;
        }
    }

    /// <summary>
    /// パーティクル生成
    /// </summary>
    private void CreateParticles()
    {
        if (centerParticle != null)
        {
            DestroyObject(centerParticle.gameObject);
        }
        foreach (var p in aroundParticles)
        {
            DestroyObject(p.gameObject);
        }

        centerParticle = CreateParticle(Vector3.zero);
        aroundParticles = new SoftbodyParticle[particleCount];
        for (int i = 0; i < particleCount; i++)
        {
            float radian = i * Mathf.PI * 2f / (particleCount - 1);
            var position = new Vector3(Mathf.Cos(radian), Mathf.Sin(radian), 0f) * softbodyRadius;
            aroundParticles[i] = CreateParticle(position);
        }
    }

    /// <summary>
    /// パーティクル作成
    /// </summary>
    private SoftbodyParticle CreateParticle(Vector3 position)
    {
        var p = Instantiate(_particlePrefab, transform);
        p.transform.localPosition = position;
        return p;
    }

    static void DestroyObject(GameObject target)
    {
        if (Application.isPlaying)
            Destroy(target);
        else
            DestroyImmediate(target);
    }
}
```
　
</div>

</details>

# ソフトボディの作成
パラメータや、Prefabを以下のように設定した状態で、Createボタンを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/4db99585-89a8-5ef7-8f08-ba38285113be.png)

以下のようなソフトボディが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/11a5fde1-f2e7-e775-4f38-c59448b2dab4.png)

# 結果
![SpringJoint2Dによるソフトボディ2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/511db3c7-4a70-7151-8590-07c8281d765f.gif)