Live2dのモデルアニメーションをコードのみで制御する

Live2dの公式ではMecanimでのアニメーションの解説は充実しているが、個人的にMecanimはポンコツだと思うので、どうしてもスクリプトでの制御のみにしたい。ただLive2dは独自進化が激しく非常にクセがあり苦労したのでその過程の知見を羅列しておく。

### コンポーネントを自動でアタッチ
基本的にときどきバグるのでLive2dのモデルを度々入れ直すことになる。このときインスペクターで手動でAnimationClipやfadeMotionListをセットしているとおそろしく時間がかかる。なのでEditorScriptでモデルを入れたときに可能な限り自動でアタッチされるようにしておく。
まずLive2dのフォルダをAssets->Resourceに移動しておく、写真の例だとHukidahi,HukidashiL,Yuu_Lieフォルダの中身がモデル関連のスクリプトになっている。

![スクリーンショット 2023-12-08 10.02.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117011/b46b4dbc-b29a-4eb0-a01d-63c109fc1d53.png)

移動させるとAssets直下にいれてくれとエラーが出る時があるが、unityの再起動でなおる。

以下はヒエラルキーにlive2dモデルを置いた直後に必要なコンポーネントがアタッチされるスクリプトのサンプル。これはProjectのEditorフォルダーに入れておくと発動する。
```Live2DModelEditorScript.cs
using Live2D.Cubism.Framework.Motion;
using Live2D.Cubism.Framework.MotionFade;
using UnityEngine;
using UnityEditor;

[InitializeOnLoad]
public class Live2DModelEditorScript
{
    static Live2DModelEditorScript()
    {
        EditorApplication.hierarchyChanged += OnHierarchyChanged;
    }
    
    private static void OnHierarchyChanged()
    {
        var puppetCtr = PuppetScreenManager.Instance.live2dCtr;

        //CubismMotionControllerをアタッチしてプロパティにLive2D/Live2D.fadeMotionListをセット
        foreach (GameObject obj in Object.FindObjectsOfType<GameObject>())
        {
            // Live2Dモデルであるかを判断（例：名前に特定の文字列が含まれているか）
            if (obj.name.Contains("L2d") )
            {
                if (obj.GetComponent<CubismMotionController>() == null)
                {
                    var motionCtr = obj.AddComponent<CubismMotionController>();
                     // コールバックに必要なのでレイヤーを増やしておく
                     motionCtr.LayerCount = 5;
                }

                var fadeMotionList = Resources.Load<CubismFadeMotionList>("Live2D/Live2D.fadeMotionList");
                obj.GetComponent<CubismFadeController>().CubismFadeMotionList = fadeMotionList;
                if (obj.name.Contains("Yuu_Lie"))
                {
                    puppetCtr.yuu_Lie = obj;
                }

                if (obj.name.Contains("Hukidashi"))
                {
                    puppetCtr.hukidahi = obj;
                }
            }
        }
// ProjectからAnimationClipを拾ってきてセットしたいプロパティにセット
        
        puppetCtr.Hukidashi_Fuwa_Clip = Resources.Load<AnimationClip>("Live2D/Hukidahi/Hukidashi_Fuwa_Clip");
        puppetCtr.Yuu_Lie_Akubi_Clip = Resources.Load<AnimationClip>("Live2D/Yuu_Lie/Yuu_Lie_Akubi_Clip");
        puppetCtr.Yuu_Lie_Foot_Clip = Resources.Load<AnimationClip>("Live2D/Yuu_Lie/Yuu_Lie_Foot_Clip");
        puppetCtr.Yuu_Lie_Idle_Clip = Resources.Load<AnimationClip>("Live2D/Yuu_Lie/Yuu_Lie_Idle_Clip");
        puppetCtr.Yuu_Lie_Page_Clip = Resources.Load<AnimationClip>("Live2D/Yuu_Lie/Yuu_Lie_Page_Clip");
        puppetCtr.Yuu_Lie_TalkAfter_Clip = Resources.Load<AnimationClip>("Live2D/Yuu_Lie/Yuu_Lie_TalkAfter_Clip");
        puppetCtr.Yuu_Lie_TalkBefore_Clip = Resources.Load<AnimationClip>("Live2D/Yuu_Lie/Yuu_Lie_TalkBefore_Clip");
        puppetCtr.Yuu_Lie_TalkMIddleL_Clip = Resources.Load<AnimationClip>("Live2D/Yuu_Lie/Yuu_Lie_TalkMiddleL_Clip");
        puppetCtr.Yuu_Lie_TalkMiddleS_Clip = Resources.Load<AnimationClip>("Live2D/Yuu_Lie/Yuu_Lie_TalkMiddleS_Clip");

    }
}
```
ただしCubismHarmonicMotionControllerは毎回手動でアタッチしないと動作しなかった。

### PlayAnimationメソッドについて
スクリプトからアニメーション再生する場合このメソッドを使うが３つめの引数は以下のように強制力を指定するもので再生中のアニメから次のアニメにうつる場合は３のPriorityForce = 3を使用しないと再生されない。

```live2dAnimation.cs
_motionController_Lie.PlayAnimation(Yuu_Lie_TalkMIddleL_Clip, 0, 3, false);

public class CubismMotionPriority
    {
        public const int PriorityNone = 0;
        public const int PriorityIdle = 1;
        public const int PriorityNormal = 2;
        public const int PriorityForce = 3;
    }
```

### フェードについて
再生中のアニメから次のアニメへ移行するときになめらかにさせるフェードはスクリプトからは設定できない。これはLive2dのフェードイン・アウトの設定から行う。アニメーションのタイムラインの上の右クリックのメニューから設定。

![スクリーンショット 2023-12-05 21.57.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117011/7184d507-2798-76fb-f05a-a1c8a4115c5a.png)


Live2dパラメーター全体を選択してから

![スクリーンショット 2023-12-05 22.00.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117011/4ef5013d-b319-65fc-ed63-227e4ebe6cd1.png)


３つ目の「選択範囲内のパラメータ」を選択して適当な数値を入れてOK

![スクリーンショット 2023-12-05 21.59.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117011/a6100533-4510-4d3e-8ff2-f53679026898.png)

いろいろ試してみたが上記の設定以外だとカックンと遷移してしまう。秒数は0.8である必要はない。
上記の理由からループアニメーションもある程度秒数が無いとフェードの余裕時間がなくなるので元から何回かループを繰り返すようにしておいた方がいい。（意味わかる？）

### Dotweenとの連携で連続してアニメーションを再生させる
Live2d以外のオブジェクトのアニメーションにはDotweenを多用しているので、それらと連携させるにはDotweenで制御する必要がある。以下は今のところこれでいけそうというレベルのサンプルコードです。
```Live2dDotweenSample.cs
public void PlayClips(Action onComplete)
    {
        Sequence seq = DOTween.Sequence ();
        _motionController_Lie = yuu_Lie.GetComponent<CubismMotionController>();
     　　　//    最初のクリップ開始
        _motionController_Lie.PlayAnimation(Yuu_Lie_Akubi_Clip, 0, 3, false);
   　　　　　//    クリップ終了待ち
        seq.AppendInterval(Yuu_Lie_Akubi_Clip.length)
            .AppendCallback(() =>
            {
　　　　　　　　　　　//    ２つ目のクリップ開始
                _motionController_Lie.PlayAnimation(Yuu_Lie_Foot_Clip, 0, 3, false);
            })
     //    クリップ終了待ち       
            .AppendInterval(Yuu_Lie_Foot_Clip.length)
            .AppendCallback(() =>
            {
     // ２つのクリップ完了後にすること
                onComplete();
            });
    }
    
//以下は３つのメソッドでセット
    private Sequence currentSequence;
    /// <summary>
    /// クリップをランダムに組み合わせて、指定時間の間再生し続ける
    /// </summary>
    public void SetAutoSequenceAnim()
    {
        currentSequence = DOTween.Sequence().SetId("AutoAnim");
        var clipLIst = GeneratePlayAnimationList(180, new List<AnimationClip>() { Yuu_Lie_Akubi_Clip, Yuu_Lie_Foot_Clip, Yuu_Lie_Idle_Clip });
        AddToSequence(currentSequence, null,clipLIst, 0);
        
        
    }
    /// <summary>
    /// 指定時間と組み合わせるクリップのリストから組み合わせを生成
    /// </summary>
    /// <param name="durationInSeconds">継続時間</param>
    /// <param name="availableClips">クリップたち</param>
    /// <returns></returns>
    public List<AnimationClip> GeneratePlayAnimationList(float durationInSeconds, List<AnimationClip> availableClips)
    {
        var sequence = new List<AnimationClip>();
        AnimationClip lastClip = null;
        float totalDuration = 0.0f;

        while (totalDuration < durationInSeconds)
        {
            AnimationClip selectedClip;
            do {
                selectedClip = availableClips[Random.Range(0, availableClips.Count)];
            } while (selectedClip == lastClip && availableClips.Count > 1);

            sequence.Add(selectedClip);
            totalDuration += selectedClip.length;

            lastClip = selectedClip;
        }

        return sequence;
    }
    /// <summary>
    /// 指定したアニメリストを再生し続ける。ForやForeachではうまく動作しなかったのでこんなやりかたしている
    /// </summary>
    /// <param name="sequence">DotweenのSequence</param>
    /// <param name="beforeClip">1つ前のクリップ</param>
    /// <param name="clipList">組み合わせクリップのリスト</param>
    /// <param name="count">このメソッドがループした回数</param>
    private void AddToSequence(Sequence sequence, AnimationClip beforeClip, List<AnimationClip> clipList, int count)
    {
        
        if (count >= clipList.Count) // 基本ケース: すべてのクリップを処理したら終了
        {
            sequence.Play();
            return;
        }
        
        AnimationClip currentClip = clipList[count];
        float duration = beforeClip == null ? 0 : beforeClip.length;
        
        sequence.AppendInterval(duration).AppendCallback(() => {
            _motionController_Lie.PlayAnimation(currentClip, 0, 3, false);
        });
        
        AddToSequence(sequence, currentClip, clipList, count + 1); 
    }
```

### アニメーションが完了した時のコールバック
clipには再生中かどうかを判定するプロパティは無いので、レイヤーでのアニメ再生判定を使用してアニメーションのコールバックを作成。レイヤーを複数設定するとアニメ間のフェードがおかしくなるのでレイヤーは0のみ

```PlayComplete.cs
//レイヤーの２番目にアニメを再生
    _motionController_Lie.PlayAnimation(Yuu_Lie_TalkMiddleS_Clip, 0, 3, false);
//コールバックを設定
    StartCoroutine(CheckAnimationEnd(_motionController_Lie, 0, () =>{}));

//レイヤー２番目のアニメを監視して完了後にコールバック発動
    IEnumerator CheckAnimationEnd(CubismMotionController controller ,int layerIndex, Action callback)
    {
        while (controller.IsPlayingAnimation(layerIndex))
        {
            yield return null;
        }
        Debug.LogWarning(layerIndex+"---AnimEnd");
        callback?.Invoke();
    }
```
## アニメーションが完了した時のコールバック拡張メソッド
上記の拡張メソッド
AIに書いてもらいました、これどっかに貼れば使えます。
```MotionControllerExtension.cs
public static class MotionControllerExtension
{
    private static CoroutineRunner instance;

    public static CoroutineRunner Instance
    {
        get
        {
            if (instance == null)
            {
                // Create a new GameObject with the CoroutineRunner Component 
                instance = new GameObject("CoroutineRunner").AddComponent<CoroutineRunner>();
            }
            return instance;
        }
    }
    public static void PlayAnimationOnComplete(this CubismMotionController motionCtr, AnimationClip clip, Action onComplete ,bool isLoop = false)
    {
        motionCtr.PlayAnimation(clip, 0, 3, isLoop);
        CoroutineRunner.Instance.RunCoroutine(CheckAnimationEnd(motionCtr, onComplete));
    }
    static IEnumerator CheckAnimationEnd(CubismMotionController controller , Action callback)
{
    while (controller.IsPlayingAnimation(0))
    {
        yield return null;
    }
    Debug.LogWarning(controller.gameObject.name +"--AnimEnd");
    callback?.Invoke();
}
}
public class CoroutineRunner : MonoBehaviour 
{
    private static CoroutineRunner instance;

    public static CoroutineRunner Instance
    {
        get
        {
            if (instance == null)
            {
                // Create a new GameObject with the CoroutineRunner Component 
                instance = new GameObject("CoroutineRunner").AddComponent<CoroutineRunner>();
            }
            return instance;
        }
    }

    public void RunCoroutine(IEnumerator coroutine)
    {
        Instance.StartCoroutine(coroutine);
    }
}
```
と、最近のアップデート作業においてUnityでツール系作る限界を感じました。ネイティブと厳密に連携させるのは厳しいと思います。なのでアニメーションは[Rive](https://rive.app/)を使用することにしました。この記事はこれでおしまい。