【Unity】Terrainに生やした木や草を動的に差し替える方法

下記画像のようにTerrain経由で生成した木や草オブジェクトをリアルタイムで差し替える方法の紹介です。
紅葉など季節の移り変わりなどに利用できると思います。
今回は「夏の風景から秋の風景に変更する処理」を例に説明します。
|夏（差し替え前）|秋（差し替え後）|
|---|---|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/938939/53c33d00-48b6-d9c3-571a-9c483df64f6d.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/938939/bdd866c1-00b2-0d75-9706-253bf80f5b4e.png)|

# 木オブジェクトの差し替え
## 夏用と秋用の木を用意
夏用の木と秋用の紅葉した木のプレハブを準備し、Terrainのインスペクタ > 樹木ペイントタブに移動し、準備した2種類の木を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/938939/a01ecacd-5be5-ea2e-2ba6-bf6ea36554f0.png)

## 木を差し替えるスクリプトの作成
最下部にある関数`ChangeTreeColor`に差し替え処理を記載しています。
```SeasonVisualizar.cs
/// <summary>
/// 季節毎の風景を演出する機能を実装する
/// </summary>
public class SeasonVisualizar : MonoBehaviour
{
    /// <summary>
    /// 変更対象のTerrain
    /// </summary>
    [SerializeField] private Terrain terrain;
    /// <summary>
    /// シーン上に配置してある木のリスト
    /// </summary>
    private TreeInstance[] currentTreeList ;
    /// <summary>
    /// 季節用の定数（ 0 = 春, 1 = 秋）
    /// </summary>
    private const int SPRING = 0, FALL = 1;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        // マウスクリックで木を紅葉したものに差し替える
        if (Input.GetMouseButtonDown(0)) {
            ChangeTreeColor(season: FALL);
        }
    }

    /// <summary>
    /// 木のモデルを入れ替え、風景を変える
    /// </summary>
    /// <param name="season">変更予定の季節番号</param>
    public void ChangeTreeColor(int season) {
        currentTreeList = new TreeInstance[terrain.terrainData.treeInstances.Length];
        System.Array.Copy(terrain.terrainData.treeInstances, currentTreeList,
                          terrain.terrainData.treeInstances.Length);

        for (int i = 0; i < terrain.terrainData.treeInstanceCount; i++) {
            currentTreeList[i].prototypeIndex = season;
        }
        terrain.terrainData.treeInstances = currentTreeList;
    }
}
```
関数`ChangeTreeColor`について
- `terrain.terrainData.treeInstances`はTerrain上に配置されたそれぞれの木の情報が`TreeInstance`の配列として格納されています。
- `TreeInstance`はprototypeIndexには下記画像のように、Terrainに設定済みの木オブジェクトの内、参照している木のインデックスが格納されています。
  - 今回は夏用(prototypeIndex: 0)から参照先を秋用(prototypeIndex: 1)へ変更するようにしています。
 ![名称未設定ファイル-ページ2.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/938939/ac2dae71-cd32-2de3-1555-1d7185672746.png)
- `treeIntances`のプロパティを直接変更しても変化しなかったため、現在のtreeIntancesのコピーを作成→コピーの値を変更→変更済みコピー配列でオリジナル配列を上書きという形をとっています。

# 草オブジェクトの差し替え
## 夏用と秋用の草を準備
木と同様に夏、秋用の草プレハブを用意します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/938939/319555c1-a1bc-8a5b-b714-3984ce4143b6.png)

## 草を差し替えるコードの作成
先ほどのスクリプトに草を差し替えるコードを追記します。
```SeasonVisualizar.cs
/// <summary>
/// 季節毎の風景を演出する機能を実装する
/// </summary>
public class SeasonVisualizar : MonoBehaviour
{
    /// <summary>
    /// 変更対象のTerrain
    /// </summary>
    [SerializeField] private Terrain terrain;
    /// <summary>
    /// シーン上に配置してある木のリスト
    /// </summary>
    private TreeInstance[] currentTreeList ;

    /* 追記 */
    /// <summary>
    /// シーン上に配置してある草のリスト
    /// </summary>
    private DetailPrototype[] currentGrass;
    /// <summary>
    /// 季節ごとの草オブジェクト
    /// </summary>
    [SerializeField] private GameObject[] grassVariant;
    /// <summary>
    /// 季節用の定数（ 0 = 春, 1 = 秋）
    /// </summary>
    private const int SPRING = 0, FALL = 1;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0)) {
            ChangeTreeColor(season: FALL);
            /* 追記 */
            ChangeGrassColor(season: FALL);
        }
    }

    /// <summary>
    /// 木のモデルを入れ替え、風景を変える
    /// </summary>
    /// <param name="season">変更予定の季節番号</param>
    public void ChangeTreeColor(int season) {
        currentTreeList = new TreeInstance[terrain.terrainData.treeInstances.Length];
        System.Array.Copy(terrain.terrainData.treeInstances, currentTreeList, terrain.terrainData.treeInstances.Length);
        for (int i = 0; i < terrain.terrainData.treeInstanceCount; i++) {
            currentTreeList[i].prototypeIndex = season;
        }
        terrain.terrainData.treeInstances = currentTreeList;
    }

    /// <summary>
    /// 草オブジェクトを入れ替え、風景を変える
    /// </summary>
    /// <param name="season">変更予定の季節番号</param>
    public void ChangeGrassColor(int season) {
        currentGrass = new DetailPrototype[terrain.terrainData.detailPrototypes.Length];
        System.Array.Copy(terrain.terrainData.detailPrototypes, currentGrass, terrain.terrainData.detailPrototypes.Length);
        for (int i = 0; i < terrain.terrainData.detailPrototypes.Length; i++) {
            currentGrass[i].prototype = grassVariant[season];
        }
        terrain.terrainData.detailPrototypes = currentGrass;
    }
}
```
スクリプト作成後、先ほど準備した草プレハブをこのスクリプトの`GrassVariant`に設定します。（要素0: 夏、要素1: 秋）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/938939/d42864c2-b065-1836-522c-7f57aa368a95.png)

関数`ChangeGrassColor`について
- `Terrain.terrainData.detailPrototypes`にTerrainに配置されたそれぞれの草の情報が`DetailPrototype`の配列として格納されています。
- 草の場合は`DetailPrototype.prototype`へオブジェクトを代入することで草の差し替えが可能になっています。
  - 今回の場合は`DetailPrototype.prototype`へ事前に用意した秋用の草プレハブを代入することでモデルを入れ替えます。
- 木と同様、一度detailPrototypesのコピーを作成し、最終的にコピーでオリジナルを上書きしています。

# 注意点
この方法で差し替えを行うと、Unity実行終了後も差し替え状態が保持されたままになります。
そのままでは気になる場合はもとに戻すような関数を追加するか、差し替え状態を戻すEditor拡張を準備するといいと思います。
下記スクリプトはSeasonVisualizarコンポーネントのインスペクタ上に"Reset"ボタンを追加し、ボタン押下で差し替えをもとに戻すEditor拡張スクリプトです。
```SeasonVisualizarEditor.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

#if UNITY_EDITOR
[CustomEditor(typeof(SeasonVisualizar))]
public class SeasonVisualizarEditor : Editor {
    /// <summary>
    /// 季節用の定数（ 0 = 春, 1 = 秋）
    /// </summary>
    private const int SPRING = 0, FALL = 1;
    public override void OnInspectorGUI() {
        base.OnInspectorGUI();
        SeasonVisualizar sv = target as SeasonVisualizar;

        if (GUILayout.Button("Reset")) {
            sv.ChangeTreeColor(season: SPRING);
            sv.ChangeGrassColor(season: SPRING);
        }
    }
}
#endif
```