Unity/Tilemapで、スクリプトから一部のtileを当たり判定含めて消してみた

#なんか、TilemapでTileを消したいなぁ……(なお、公式バグらしいです。)
![paaaa.mp4.gif](https://qiita-image-store.s3.amazonaws.com/0/366532/ff29efe1-e63a-ad28-1758-2f29948ea6ce.gif)

Tilemapと衝突した際に、そのTileを取得、消して、Particleを出した試行錯誤の記事。
~~たまに2個消える~~
Rigidbody2Dをぶつけたいのにアタッチ、なんか好きな手段で動かしてみてください。
今回は、ProjectSettingsから、Physics2DのXYの数値を0にして、上下左右にキーで動かしましたが、サンプルコードからは取り除いています。最後に記載している最終的なコードには載せています。

##目次
1. [結論](https://qiita.com/momo79814613/items/57c574629fcaf4d9787f#%E7%B5%90%E8%AB%96)
2. [概要](https://qiita.com/momo79814613/items/57c574629fcaf4d9787f#%E6%A6%82%E8%A6%81)
3. [実際の処理](https://qiita.com/momo79814613/items/57c574629fcaf4d9787f#%E5%AE%9F%E9%9A%9B%E3%81%AE%E5%87%A6%E7%90%86)

##結論
安定性に少し欠けるが、とりあえずTilemapのTileを、場所を指定してスクリプトから消すことができました。
Unity2018.3以降では確認済みですが、基本大丈夫なんじゃないでしょうか。
varでもいいけど全部変数は明示してます。
また、サンプルソースはだんだん長くなっていく感じです。
スクリプトは、ぶつかるものにアタッチしてください。説明のため(言い訳)に神クラス風味になっています。


##概要(?)
###躓いたポイント
1. Tilemapの文献が少ない
2. あっても英語
3. SetTile使うと、2回目にエディタがぶっ壊れて落ちた(最重要)

###なんでやろうと思ったか
「ステージを壊して進むような演出がしたい!!」
→
「Tilemapはmapが作りやすいけど、動的なのやるときにMapを複数作るのめんどい……」
「エディタで普通に配置するほどの労力をかけたくない……」
「よもやCSVでインスタンシエイトして配置するわけにもいかない……」

⇒「なんとかしてTilemapをスクリプトから消すぞオレはこの野郎」

###どうやってやったか
1. いろいろ苦労した
2. OnCollisionEnter2Dを使う
3. あたった場所の座標を取る
4. 存在するTileすべてを取得する
5. 存在するTileの中で、ぶつかった場所に一番近いTileの座標を計算する
6. [消す(完成)](https://qiita.com/momo79814613/items/57c574629fcaf4d9787f#%E5%AD%98%E5%9C%A8%E3%82%92%E6%B6%88%E3%81%99%E5%AE%8C%E6%88%90)
7. おまけでParticleを出す

##実際の処理
###いろいろ苦労する
文献が少ない気がする。ググラビティの欠如かも。公式も英語だし。
最終的にはRider神の神託に頼りつつ総当たりで検証。なんとかうまくいった。
少しでも参考になったら。

###OnCollisionEnter2Dを使う
当たり判定なので、当然これを使う。

```c#:DeleteTileTest.cs
using System.Collections.Generic;
using UnityEngine;

public class DeleteTileTest : MonoBehaviour
{
    private void OnCollisionEnter2D(Collision2D ot)
    {
    //ここに処理
    }
}
```
###あたった場所の座標を取る
```c#:DeleteTileTest.cs
using System.Collections.Generic;
using UnityEngine;

public class DeleteTileTest : MonoBehaviour
{
    private void OnCollisionEnter2D(Collision2D ot)
    {
        //とりあえず変数作って初期化
        Vector3 hitPos = Vector3.zero;
        //あたった場所の座標を取得
        foreach (ContactPoint2D point in ot.contacts)
        {
            hitPos = point.point;
        }
    }

}
```

###存在するTileすべてを取得する
```c#:DeleteTileTest.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class DeleteTileTest : MonoBehaviour
{

    private void OnCollisionEnter2D(Collision2D ot)
    {
        //とりあえず変数作って初期化
        Vector3 hitPos = Vector3.zero;
        //あたった場所の座標を取得
        foreach (ContactPoint2D point in ot.contacts)
        {
            hitPos = point.point;
        }

        BoundsInt.PositionEnumerator position = ot.gameObject.GetComponent<Tilemap>().cellBounds.allPositionsWithin;
        var allPosition = new List<Vector3>();

        foreach (var variable in position)
        {
            if (ot.gameObject.GetComponent<Tilemap>().GetTile(variable) != null)
            {
                allPosition.Add(variable);
            }
        }

    }

}
```

これは、基本的にvar型が楽だと思いますが、説明のために型を指定しています。


```c#

BoundsInt.PositionEnumerator position = ot.gameObject.GetComponent<Tilemap>().cellBounds.allPositionsWithin;
```

あたった相手のTilemapを取得、すべてのPositionを取得します。
ここで注意するのが、Positionは「Gridの中での位置」です。
Tileの左下の部分が取得されます。
もう一つ注意が「Tileを設置してない場所も取得される」ことです。
一度でも設置した範囲は、一旦は長方形の範囲で確保されるようですので、
何もおいていなくても、端っこのTile同士が描く長方形の範囲内はすべて取得されます。

そのため、

```c#
        foreach (var variable in position)
        {
            if (ot.gameObject.GetComponent<Tilemap>().GetTile(variable) != null)
            {
                allPosition.Add(variable);
            }
        }
```
このようにして「ほんとにTileあるの……?」と確認して、あるときだけそのPositionをallPositionに格納しました。ここらへんで時間かかりました。~~というか全部時間かかりました。~~

###存在するTileの中で、ぶつかった場所に一番近いTileの座標を計算する


```c#:DeleteTileTest.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class DeleteTileTest : MonoBehaviour
{

    private void OnCollisionEnter2D(Collision2D ot)
    {
        Vector3 hitPos = Vector3.zero;
        foreach (ContactPoint2D point in ot.contacts)
        {
            hitPos = point.point;
        }

        BoundsInt.PositionEnumerator position = ot.gameObject.GetComponent<Tilemap>().cellBounds.allPositionsWithin;
        var allPosition = new List<Vector3>();
        //一番近い場所を保存したいので変数を宣言
        int minPositionNum = 0;

        foreach (var variable in position)
        {
            if (ot.gameObject.GetComponent<Tilemap>().GetTile(variable) != null)
            {
                allPosition.Add(variable);
            }
        }

        //for文で探査する。でも初期化で0入れてるから1からスタート
        for (int i = 1; i < allPosition.Count; i++)
        {
            //それぞれのあたった場所からの大きさを取得、最小を更新したらminPositionNumを更新する
            if ((hitPos - allPosition[i]).magnitude <
                (hitPos - allPosition[minPositionNum]).magnitude)
            {
                minPositionNum = i;
            }
        }
    }
}
```


###~~(存在を)~~消す(完成)


```c#:DeleteTileTest.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class DeleteTileTest : MonoBehaviour
{

    private void OnCollisionEnter2D(Collision2D ot)
    {
        Vector3 hitPos = Vector3.zero;
        foreach (ContactPoint2D point in ot.contacts)
        {
            hitPos = point.point;
        }

        BoundsInt.PositionEnumerator position = ot.gameObject.GetComponent<Tilemap>().cellBounds.allPositionsWithin;
        var allPosition = new List<Vector3>();
        //一番近い場所を保存したいので変数を宣言
        int minPositionNum = 0;

        foreach (var variable in position)
        {
            if (ot.gameObject.GetComponent<Tilemap>().GetTile(variable) != null)
            {
                allPosition.Add(variable);
            }
        }

        //for文で探査する。でも初期化で0入れてるから1からスタート
        for (int i = 1; i < allPosition.Count; i++)
        {
            //それぞれのあたった場所からの大きさを取得、最小を更新したらminPositionNumを更新する
            if ((hitPos - allPosition[i]).magnitude <
                (hitPos - allPosition[minPositionNum]).magnitude)
            {
                minPositionNum = i;
            }
        }

        //最終的な位置を一旦格納した。RoundToIntは四捨五入とのことです
        Vector3Int finalPosition = Vector3Int.RoundToInt(allPosition[minPosition]);
        

        TileBase tiletmp = ot.gameObject.GetComponent<Tilemap>().GetTile(finalPosition);
        
        if (tiletmp != null)
        {
            Tilemap map = ot.gameObject.GetComponent<Tilemap>();
            TilemapCollider2D tileCol = ot.gameObject.GetComponent<TilemapCollider2D>();

            map.SetTile(finalPosition, null);
            tileCol.enabled = false;
            tileCol.enabled = true;
        }
    }
}
```
Vector3Int自体知りませんでしたが、整数型のVector3みたいです。たぶん。
RoundToIntは、四捨五入で整数にまるめてくれます。
なので、最終的な位置を一旦格納します。

```c#
        Vector3Int finalPosition = Vector3Int.RoundToInt(allPosition[minPosition]);
```

そして、TileBase(Tileとかのおおもとの型?)型の変数に、最終的な位置にあるTileを格納します。
GetTileは、Vector3Intで場所を指定、そのTilemapの中で該当する位置にあるTileを返してくれます。


```c#

        TileBase tiletmp = ot.gameObject.GetComponent<Tilemap>().GetTile(finalPosition);
```


ここまできても疑って「nullなんじゃないか……?」と考えてから処理をしています。
ちゃんと取得できたら

        if (tiletmp != null)
        {
            Tilemap map = ot.gameObject.GetComponent<Tilemap>();
            TilemapCollider2D tileCol = ot.gameObject.GetComponent<TilemapCollider2D>();
            map.SetTile(finalPosition, null);
            tileCol.enabled = false;
            tileCol.enabled = true;
        }

こんな処理をします。
~~もっと上の方でできた気がめっちゃしますが、~~Tilemapに格納、TilemapCollider2Dにもそれぞれ格納します。
そしたら本題!SetTileをします!
これが曲者で、一回目の実行では動くんですが、2回目以降実行すると、メモリが変なことになるのかな、と見えましたが、衝突した瞬間にエディタが落ちるか、可愛い感じで「保存……しとく?」と伺ってきて、選ぶと落ちます。どっちにしろ落ちます。WinでもMacでも落ちます。落ちます。
<img width="474" alt="Otiru.png" src="https://qiita-image-store.s3.amazonaws.com/0/366532/176a2492-f1c9-46a7-c251-6be22d6af6f6.png">
それをふせぐためのfalse→trueの反復横跳びです。これをすると、Colliderの情報あたりがリフレッシュされるのか、いい感じに動きます。
Tileを消して、新たにColliderの形を決めてほしいから、という意図が主でしたが、結果うまくいきました。

###おまけでParticleを出す



```c#:DeleteTileTest.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class DeleteTileTest : MonoBehaviour
{
    //なんかすきなParticleを作ってエディタでアタッチしましょう
    [SerializeField] private ParticleSystem particle;

    private void OnCollisionEnter2D(Collision2D ot)
    {
        Vector3 hitPos = Vector3.zero;
        foreach (ContactPoint2D point in ot.contacts)
        {
            hitPos = point.point;
        }

        BoundsInt.PositionEnumerator position = ot.gameObject.GetComponent<Tilemap>().cellBounds.allPositionsWithin;
        var allPosition = new List<Vector3>();
        int minPositionNum = 0;

        foreach (var variable in position)
        {
            if (ot.gameObject.GetComponent<Tilemap>().GetTile(variable) != null)
            {
                allPosition.Add(variable);
            }
        }

        for (int i = 1; i < allPosition.Count; i++)
        {
            if ((hitPos - allPosition[i]).magnitude <
                (hitPos - allPosition[minPositionNum]).magnitude)
            {
                minPositionNum = i;
            }
        }

        Vector3Int finalPosition = Vector3Int.RoundToInt(allPosition[minPosition]);
        

        TileBase tiletmp = ot.gameObject.GetComponent<Tilemap>().GetTile(finalPosition);
        
        if (tiletmp != null)
        {
            Tilemap map = ot.gameObject.GetComponent<Tilemap>();
            TilemapCollider2D tileCol = ot.gameObject.GetComponent<TilemapCollider2D>();<img width="474" alt="Otiru.png" src="https://qiita-image-store.s3.amazonaws.com/0/366532/27618c7e-e3eb-0483-0e94-c196b313f9bd.png">

            map.SetTile(finalPosition, null);
            tileCol.enabled = false;
            tileCol.enabled = true;

            //わたしは1秒寿命でサイズを変えて、0.85fで消したらいい感じになったのでこんな感じにしました
            Destroy(Instantiate(particle, finalPosition + Vector3.one * 0.5f, Quaternion.identity), 0.85f);
        }
    }
}
```

最終的な位置は、Tileの左下なので、Vector3.one * 0.5fでチョット調整しています。

##一応、最終的な私のスクリプト

```c#:DeleteTileTest.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class DeleteTileTest : MonoBehaviour
{
    [SerializeField] private ParticleSystem particle;


    private void Update()
    {
        transform.position += new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0) * Time.deltaTime;
    }

    private void OnCollisionEnter2D(Collision2D ot)
    {
        var hitPos = Vector3.zero;

        foreach (var point in ot.contacts)
        {
            hitPos = point.point;
        }

        var position = ot.gameObject.GetComponent<Tilemap>().cellBounds.allPositionsWithin;
        var minPosition = 0;
        var allPosition = new List<Vector3>();

        foreach (var variable in position)
        {
            if (ot.gameObject.GetComponent<Tilemap>().GetTile(variable) != null)
            {
                allPosition.Add(variable);
                Debug.Log(variable.ToString());
            }
        }

        for (var i = 1; i < allPosition.Count; i++)
        {
            if ((hitPos - allPosition[i]).magnitude <
                (hitPos - allPosition[minPosition]).magnitude)
            {
                minPosition = i;
            }
        }

        var finalPosition = Vector3Int.RoundToInt(allPosition[minPosition]);
        
        var tiletmp = ot.gameObject.GetComponent<Tilemap>().GetTile(finalPosition);
        
        if (tiletmp != null)
        {
            var map = ot.gameObject.GetComponent<Tilemap>();
            var tileCol = ot.gameObject.GetComponent<TilemapCollider2D>();
            map.SetTile(finalPosition, null);
            tileCol.enabled = false;
            tileCol.enabled = true;
            
            Destroy(Instantiate(particle, finalPosition + Vector3.one * 0.5f, Quaternion.identity), 0.85f);
        }
    }
}
```

##あとがき
初学者ですんごく大変だったので、初めて記事として残してみました。
「この処理になんの意味があるんですか?」といわれたら「なんか楽しくなっちゃったからやりました!!!!反省はしません!!!!」と答えます。
以上、ご参考頂けたら幸いです。