【unity】2Dで糸、ケーブルそして紐をやる

2Dのunityで、有料アセットを使わずに糸をやろうとしてハマり倒したので情報まとめておきます。

# やり方

要するに、小さいオブジェクトをいっぱいjointで繋げて長い糸状にする感じです。
と書くと単純ですが、普通にやるとjointがノビノビ大暴れで大変なことになるので。
細かい調整が必要です。

## 糸部品プレハブを作成

### オプジェクト作成

空のオブジェクトを追加し、CablePartとでも名付けておきます。
- RigidBody2D
- HingeJoint2D
をアタッチします。

HingeJoint2Dの「Auto Configure Connected Anchor」のチェックを外しておきます。
![CablePart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515993/2d32404b-dfb1-bb72-981f-1da7c68b0b59.png)

:::note info
もし糸に当たり判定が必要なら、Collider2Dもアタッチします。
その場合、除外タグを設定して糸部品同士が衝突しないようにした方が良いでしょう。
（もつれるような糸の動きを敢えてやりたいなら、その限りではないです）
:::



### CablePartスクリプトを作成してアタッチ

スクリプト「CablePart.cs」を新規作成します。

```C#:CablePart.cs
using UnityEngine;

public class CablePart : MonoBehaviour
{
    public float Weight;
    public float Size;

    void Start()
    {
        //重さの設定
        gameObject.GetComponent<Rigidbody2D>().mass = Weight;
        //サイズ設定
        gameObject.transform.localScale = new Vector2(Size, Size);
        //HingeJoint2Dの設定
        gameObject.GetComponent<HingeJoint2D>().autoConfigureConnectedAnchor = false;
        gameObject.GetComponent<HingeJoint2D>().anchor = new Vector2(Size / 4, Size / 4);
        //JOINTの暴れ対策
        gameObject.GetComponent<Rigidbody2D>().inertia = 0.1f;
    }
}
```

作成したスクリプトをCablePartオブジェクトにアタッチします。
Weight、Sizeのパラメータは0のままでいいです。

![スクリーンショット 2025-01-12 10.48.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515993/e1ec407e-f25c-1485-7e4d-029572b119eb.png)


### CablePartオブジェクトをプレハブ化

作成したCablePartオブジェクトをプレハブにしておきます。
![CablePartプレハブ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515993/aab3d908-9c4d-8737-9b10-c2f5b711b326.png)

プレハブ化したら、ヒエラルキー上のCablePartオブジェクトは消していいです。

## 糸表示用のマテリアルを作成

プロジェクトに新規マテリアルを作成し、CableMaterialとでも名付けておきます。
シェーダはStanderd Unlitに設定して、
糸を描画したい色を設定します。

![スクリーンショット 2025-01-12 10.41.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515993/a8a4f978-30c4-1e58-ce1f-aec9a8213a33.png)

:::note info
マテリアルにあんまり詳しくないので上記のように適当なものを記載していますが、
詳しい方は良きようにした方が良いでしょう。
:::


## 糸を制御するオブジェクトを作成

### 新規オプジェクト作成

空のオブジェクトを追加し、CableObjectとでも名付けておきます。

LineRendererをアタッチして、Materialsの要素0に先ほど作成したCableMaterialを設定しておきます。

![スクリーンショット 2025-01-12 10.52.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515993/41b89539-29a3-6703-67f3-d440f3865929.png)


### CableObjectスクリプトを作成してアタッチ

スクリプト「CableObject.cs」を新規作成します。

```C#:CableObject.cs
using System.Collections.Generic;
using UnityEngine;

public class CableObject : MonoBehaviour
{
    //部品のプレハブ
    [SerializeField] GameObject CablePartPrefab = null;
    //接続先
    [SerializeField] GameObject StartObj = null;
    [SerializeField] GameObject EndObj = null;
    //糸一単位の重さ
    [SerializeField] float CablePartWeight = 1f;
    //糸一単位のサイズ（きめ細かさ）
    [SerializeField] float CablePartSize = 0.1f;
    //糸の太さ
    [SerializeField] float CableWidth = 0.05f;
    //糸の長さ
    [SerializeField] int CableLength = 100;
    //糸の最大長さ（無限に糸を伸ばせないようにする）
    const int MAXLENGTH = 1000;

    List<GameObject> vertices = new List<GameObject>();
    LineRenderer lineRender;

    void Start()
    {
        lineRender = GetComponent<LineRenderer>();
        lineRender.positionCount = vertices.Count;
        lineRender.startWidth = CableWidth;
        lineRender.endWidth = CableWidth;

        //開始時点で糸を出す
        for (int i = 0; i < CableLength; i++)
        {
            addLine();
        }
    }

    void Update()
    {
        int idx = 0;
        lineRender.positionCount = vertices.Count;
        //糸の描画
        foreach (GameObject v in vertices)
        {
            lineRender.SetPosition(idx, v.transform.position);
            idx++;
        }
    }

    //糸を1単位出す
    public void addLine()
    {
        if (vertices.Count > MAXLENGTH) return;

        //CablePartの新規オブジェクト作成
        GameObject newCablePart = (GameObject)Instantiate(
            CablePartPrefab,
            transform.position,
            Quaternion.identity,
            this.gameObject.transform);
        newCablePart.name = "CablePart_" + (vertices.Count + 1).ToString();

        CablePart cablePartScript = newCablePart.GetComponent<CablePart>();
        cablePartScript.Weight = CablePartWeight;
        cablePartScript.Size = CablePartSize;

        vertices.Add(newCablePart);
        //先端オブジェクトにくっつける
        StartObj.GetComponent<HingeJoint2D>().connectedBody
            = newCablePart.gameObject.GetComponent<Rigidbody2D>();

        Vector2 startPos = StartObj.gameObject.transform.position;
        newCablePart.transform.position
            = new Vector2(startPos.x,
            startPos.y);
        if (transform.childCount > 1)
        {
            newCablePart.gameObject.GetComponent<HingeJoint2D>().connectedBody
                = vertices[vertices.Count - 2].GetComponent<Rigidbody2D>();
        }
        else
        {
            newCablePart.GetComponent<HingeJoint2D>().connectedBody
                = EndObj.GetComponent<Rigidbody2D>();
        }
    }

    //糸を1単位巻き取る
    public void Reel()
    {
        if (vertices.Count <= 1) return;

        Destroy(vertices[vertices.Count - 1]);
        vertices.RemoveAt(vertices.Count - 1);
        GameObject obj = vertices[vertices.Count - 1];

        //先端オブジェクトにくっつけなおす
        StartObj.GetComponent<HingeJoint2D>().connectedBody
            = obj.gameObject.GetComponent<Rigidbody2D>();

        Vector2 startPos = StartObj.gameObject.transform.position;

        obj.transform.position
            = new Vector2(startPos.x,
            startPos.y);
    }
}
```

作成したスクリプトをCableObjectにアタッチします。
パラメータ「CablePartPrefab」に、CablePartのプレハブを設定します。

![スクリーンショット 2025-01-12 11.03.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515993/5ce96262-679c-55c6-da8c-5fa10bc3b015.png)


### 糸の先端と終端を設定
CableObjectスクリプトのStartObjに、糸の先端（開始点）に設置するオブジェクトを設定します。
またEndObjに、糸の終端オブジェクトを設定します。

**先端／終端のオブジェクトに、RigidBody2Dのアタッチが必須です。**
**また先端オブジェクトのみ、HingeJoint2Dのアタッチが必須です。**

本項では両端にRigitBody2D「キネマティック」の四角を設定してみます。

![スクリーンショット 2025-01-12 11.47.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515993/ed11d238-5044-939d-f8cf-c6670800aa09.png)

実行すると、先端〜終端の間に「垂れ下がってくねくね動くケーブル」が発生します。

![スクリーンショット 2025-01-12 11.52.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515993/fb2658aa-c56a-63b0-3a20-6c5862e57129.png)

終端オブジェクトのRigidBody2Dを動的に変更すると、
終端を吊ってボヨヨンボヨヨンと動くケーブルになります。

![スクリーンショット 2025-01-12 11.56.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515993/8e5cba6d-433b-cb80-f96f-949e2e8bb055.png)


# CableObjectの細かい設定

とりあえず安定する値をデフォルト値にしていますが、
お好みに合わせて下記を参考に調整してみてください。

## CablePartWeight
糸部品の1単位あたりの重さ。
終端オブジェクトを動的にする場合、その重さと設定に乖離があるとJOINTが暴れやすくなります。
接続するオブジェクトの重さによって調整すると良いです。
なお、乖離がなくても重すぎると暴れます。

* 値が小さい場合：
ふわっとした挙動になる。
小さすぎると変な動きになる。

* 値が大きい場合：
キビキビした挙動になる。
JOINTが暴れやすくなる。

## CablePartSize
糸部品1単位あたりの内部的な大きさ。
設定値は、見てくれの良さと挙動の安定のトレードオフになります。

* 値が小さい場合：
きめ細かい糸のような挙動になる。
伸びやすく、動作が安定しにくい。
描画に必要な糸部品の数が増える（重くなりやすい）。

* 値が大きい場合：
カクカクの鎖のような挙動になる。
伸びにくく、動作の安定が良い。
描画に必要な糸部品の数が少なく済む。

## CableWidth
糸の太さ。
表示上の設定値なので、あんまり挙動には影響しない（はず）。

細い糸にしたければ値を小さく、
ごんぶとうどんにしたければ値を大きくする。

## CableLength
初期状態で糸部品をいくつ作成するかの設定値。
CablePartSize * CableLength が、糸全体の長さになる。
（JOINTの伸びの影響もあるので、実際はいろいろ試しながら適正な値を探す感じになる。）
あまり値が多いと、処理が重くなる。
一応、CONSTで最大の設定値を1000と制限しています。


# 備考

## 糸を出す／巻き取る
元々は魚釣りゲーム用に作っていたので、動的に糸を出したり巻き取ったりできるようにしています。

CableObjectスクリプトのパブリック関数「AddLine」呼び出しで、糸部品1単位分出します。
「Reel」呼び出しで糸部品1単位分巻き取ります。
糸の開始点（先端オブジェクトとの繋ぎ目）から糸出し／巻き取りされるようにしています。


## 糸の伸びについて
unityのJointの宿命だと思うんですが、めちゃくちゃ伸びます。
設定で多少マシにはなります。

魚釣りのゲームを作りたくて試行錯誤した結果ではありますが、
こんなボヨンボヨンの糸じゃ釣りは成立しないので断念しました。
未だ試行錯誤中です。

**unityさん、次verで絶対伸びないJointください！！！**

［先駆者が残してくれた神の記事］
https://blog.oimo.io/2022/08/25/calm-joints-down/
非常に参考にさせていただきました。