[Unity] TerrainをC#スクリプトを使って掘る

#はじめに

TerrainをC#スクリプトを使って掘る方法が良くわからなかったので調べました。
TerrainからterrainDataを取得し、そこにGetHeightsやSetHeightsDelayLODを使ってアクセスするだけで良いようです。

**※なお、開発環境はUnity 2020.3.8f1を使用したので、Terrain設定やTerrainDataの関数が2019とは異なるのに注意が必要です。**

#Terrainの準備
簡易的に掘りたかったので、Terrainの横幅・奥行きは共に100、高さは2としています。
また、ハイトマップの1ピクセルにアクセスして掘るため、ハイトマップ解像度は129x129、制御テクスチャ解像度は128x128とします。

![terrain1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542131/886420d2-c7db-fadb-b614-6c025b6f94eb.jpeg)

地形レイヤーには、地面の色と、掘った色、二種類のテクスチャを割り当てます。

![terrain3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542131/8d72d3f9-82f6-e7ec-3d4f-97a73b4b7b6f.jpeg)

#スクリプト

StartでTerrainの高さを1mに設定して(値0.5が1mに相当)、地面テクスチャを1番目のレイヤーにリセットしています。
デフォルトは0mなので、高さを上げるのをやっとかないと掘れないのと、実行後にエディタに戻ると掘った結果が残るので最初にリセットしています。

Updateはマウスでクリックした位置にRayを飛ばして、UV座標を取得し、それをTerrainのハイトマップ座標に換算してハイト値を変更するようにしています。

```C#
using System.Linq;
using UnityEngine;

public class TerrainDigging : MonoBehaviour
{
    private Camera mainCamera;

    void Start()
    {
        mainCamera = Camera.main; // カメラを取得
        TerrainData terrainData = gameObject.GetComponent<Terrain>().terrainData; // テレインデータを取得
        // テクスチャをリセット
        var alphamaps = terrainData.GetAlphamaps(0, 0, terrainData.alphamapResolution, terrainData.alphamapResolution);
        for (int x = 0; x < alphamaps.GetLength(0); x++)
        {
            for (int y = 0; y < alphamaps.GetLength(1); y++)
            {
                alphamaps[x, y, 0] = 1f;
                alphamaps[x, y, 1] = 0f;
            }
        }
        terrainData.SetAlphamaps(0, 0, alphamaps);
        // 高さを中間にリセット
        var heights = terrainData.GetHeights(0, 0, terrainData.heightmapResolution, terrainData.heightmapResolution);
        for (int x = 0; x < heights.GetLength(0); x++)
        {
            for (int y = 0; y < heights.GetLength(1); y++)
            {
                heights[x, y] = 0.5f;
            }
        }
        terrainData.SetHeights(0, 0, heights);
    }

    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            var ray = mainCamera.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, Mathf.Infinity))
            {
                TerrainData terrainData = hit.collider.gameObject.GetComponent<Terrain>().terrainData; // テレインデータを取得

                int ax = Mathf.FloorToInt(hit.textureCoord.x * terrainData.alphamapResolution); // アルファマップのX座標
                int ay = Mathf.FloorToInt(hit.textureCoord.y * terrainData.alphamapResolution); // アルファマップのY座標
                var alphamaps = terrainData.GetAlphamaps(ax, ay, 1, 1); // クリック箇所のアルファマップを取得[1x1]
                alphamaps[0, 0, 0] = 0; // 1個目のテクスチャを無効
                alphamaps[0, 0, 1] = 1; // 2個目のテクスチャを有効
                terrainData.SetAlphamaps(ax, ay, alphamaps); // アルファマップに反映

                int hx = Mathf.FloorToInt(hit.textureCoord.x * terrainData.heightmapResolution); // ハイトマップのX座標
                int hy = Mathf.FloorToInt(hit.textureCoord.y * terrainData.heightmapResolution); // ハイトマップのY座標
                var heights = terrainData.GetHeights(hx, hy, 1, 1); // クリック箇所のヘイトマップを取得[1x1]
                heights[0, 0] = 0; // 高さを0にする
                terrainData.SetHeightsDelayLOD(hx, hy, heights); // ハイトマップに反映
            }
        }
    }
}
```

#実行例

スクリプトを配置したTerrainに貼り付けて実行すれば、マウスでクリックした箇所に穴を掘ることができます。
簡易的に凹ませていますのでエッジが汚いですが、まあサンプルなのでこのくらいで良いでしょう。

![terrain2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542131/9e3081b1-ad5a-734e-a47d-316be7bf1d31.jpeg)

#参考リンク

以下のサイトを参考にさせて頂きました。

[【Unity C# スクリプトでTerrain生成 2 】Terrain の高さを設定](https://matsumu.com/2020/05/30/1-183/)
[【Unity】テレインのプレイヤーがいる場所のアルファマップを取得する](https://www.ame-name.com/archives/2531)