Unity Spine CreateRuntimeInstance について

[PONOS Advent Calendar 2022](https://qiita.com/advent-calendar/2022/ponos)の23日目の記事です。
昨日は@ackylaさんの「NESエミュレータを作る-キャラクターROM見てみる編」でした。

# はじめに
Unityの2Dゲームアニメーションによく使われているSpineですが、
本日はSpineのランタイム生成や注意してほしい箇所をご紹介したいと思います。
まず、開発環境から設定しましょう。


### テスト環境 
- Unity 2021.3
- Unity Memory Profiler
- spine-unity 4.1
    - http://ja.esotericsoftware.com/spine-unity
    - http://ja.esotericsoftware.com/spine-unity-download

# Spineの構造
まずはUnityのサンプルプロジェクトを作成してspine-unity sdkをインポートしましょう！
![spine.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2156003/c7a398c6-c6cf-217e-3f72-a026fa106264.png)

- Skeleton Data Asset
- Skeleton Animation
- Skeleton Renderer

ご覧の通りSpineはSkeletion Animationコンポーネントが核となっており、
Skeleton Data Assetデータから様々なレンダリングまで行われています。
今回はSpineのGameObject Cloneではなくてランタイム生成について触れていきます。

### ランタイム生成メソッド
```c#
SkeletonDataAsset.cs
public static SkeletonDataAsset CreateRuntimeInstance (
                                TextAsset skeletonDataFile, 
                                AtlasAssetBase[] atlasAssets,
                                bool initialize,
                                float scale = 0.01f)
--------------------------------------------------------------------
SkeletonAnimation.cs
public static SkeletonAnimation NewSkeletonAnimationGameObject (
                                SkeletonDataAsset skeletonDataAsset,
                    			bool quiet = false)
```
- 上記のメソッド2つで簡単に作られます。


ランタイムで生成するSkeleton Data AssetやAtlas AssetsのInspectorサンプルは以下となります。
SpineRuntimeLoader GameObjectがデータ参照している形です。
![spine-inspector.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2156003/e2514b61-9869-fca1-819e-9142634f3207.png)


### SpineRuntimeLoader
```c# 
using UnityEngine;
using Spine.Unity;

public class SpineRuntimeLoader : MonoBehaviour
{
    [SerializeField] SkeletonDataAsset skeletonDataAsset = default;
    [SerializeField] AtlasAssetBase[] atlasAssets = default;

    void CreateSpineInstance()
    {
        var newSkeletonDataAsset = SkeletonDataAsset.CreateRuntimeInstance(skeletonDataAsset.skeletonJSON, atlasAssets, true);
        var newInstance = SkeletonAnimation.NewSkeletonAnimationGameObject(newSkeletonDataAsset);
    }
}
```
- コードでは参照しているSkeletonDataAssetとAtlasAssetからCreateRuntimeInstanceを作成し
- SkeletonAnimationコンポーネントがついたUnity GameObjectを生成します。
![spine-create.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2156003/f4ee49cc-557d-0ba7-3699-41730586f2f4.gif)
- ランタイム中に簡単にSpineの作成ができました。
 
# CreateRuntimeInstance使用時の注意点
ゲームの開発においてオブジェクトプールは利用しつつも、
毎回スケルトンデータインスタンスを生成するとどうなるのでしょうか？
メモリープロファイリングしてみたら驚きの結果が！？


### テストコード
```c#
using UnityEngine;
using Spine.Unity;

public class SpineRuntimeLoader : MonoBehaviour
{
    [SerializeField] SkeletonDataAsset skeletonDataAsset = default;
    [SerializeField] AtlasAssetBase[] atlasAssets = default;
    List<SkeletonAnimation> skeletonInstances = new List<SkeletonAnimation>();

    void CreateSpineInstance()
    {
        var newSkeletonDataAsset = SkeletonDataAsset.CreateRuntimeInstance(skeletonDataAsset.skeletonJSON, atlasAssets, true);
        var newInstance = SkeletonAnimation.NewSkeletonAnimationGameObject(newSkeletonDataAsset);
        skeletonInstances.Add(newInstance);
    }

    void Update()
    {
        if (Input.GetKeyUp(KeyCode.Alpha1))
        {
            for (int i = 0; i < 1000; i++)
            {
                CreateSpineInstance();
            }
        }
        if (Input.GetKeyUp(KeyCode.Alpha2))
        {
            for (int i = 0; i < skeletonInstances.Count; i++)
            {   
                Destroy(skeletonInstances[i].gameObject);
                skeletonInstances[i] = null;
            }
            skeletonInstances.Clear();
        }

    }
}
```
- キー1を押して1000体のオブジェクトを作り、キー2を押して破棄するシンプルなコードです。
　
## Memory Profile
メモリープロファイラーのインポートはこちらをご参考ください。 
- https://docs.unity3d.com/Packages/com.unity.memoryprofiler@1.0/manual/index.html


まず何もない状態でのSnapShotです。特に気になるものはありませんね。
![spine-empty.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2156003/5da454be-da9c-2a53-b8dd-a038303eb123.png)

キー1を押して1000体のインスタンスを生成した状態です。
先とは違ってメモリー使用量が増えているのがわかります。
![spine-1000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2156003/c762b1d7-e407-0bb5-87ae-23b6ed6378c8.png)

今度はキー2で生成した1000体のインスタンスを削除した状態です。
なんと謎のデータが残っている！
待って？Spineとかついてるのが気になりますね…
![spine-destroy-marker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2156003/9ae1b731-0645-3437-5475-89534eadab00.png)

### 原因はランタイム生成したスケルトンデータ
Unity GameObjectは破棄したともCreateRuntimeInstanceから作られたSkeletonDataAssetの参照が解放されてなかったみたいですね…
破棄する際にはSkeletonDataAssetまできちんと解放しましょう！

### 修正コード
```c#
    for (int i = 0; i < skeletonInstances.Count; i++)
    {
        var skeletonAnimation = skeletonInstances[i];

        // スケルトンアニメーションが参照しているスケルトンデータアセットも解放しよう！
        skeletonAnimation.skeletonDataAsset.Clear();
        skeletonAnimation.skeletonDataAsset = null;
            
        Destroy(skeletonInstances[i].gameObject);
        skeletonInstances[i] = null;
    }
    skeletonInstances.Clear();
```

### 結果
同じく1000体のインスタンスを生成した後、破棄した結果スクショです。
Spineがついてるブロックはないですね！これでスッキリ。
![spine-clear-destroy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2156003/0f355144-a6f5-8c0f-65a1-49ed2b5f2802.png)


# 最後に
Spineのランタイム生成から破棄まで触れてみました。
ランタイム生成は一見便利ではありますが、使い回しの際にはロードしたデータをちゃんと解放してあげましょう。
長時間のプレイでは小さくても使用済みメモリーの積み重ねでアプリが落ちてしまうかもしれません！

明日は@ANIZA_15さんの記事です！