LoginSignup
19
12

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-03-05

なんか、TilemapでTileを消したいなぁ……(なお、公式バグらしいです。)

paaaa.mp4.gif

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

目次

  1. 結論
  2. 概要
  3. 実際の処理

結論

安定性に少し欠けるが、とりあえず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. 消す(完成)
  7. おまけでParticleを出す

実際の処理

いろいろ苦労する

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

OnCollisionEnter2Dを使う

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

DeleteTileTest.cs
using System.Collections.Generic;
using UnityEngine;

public class DeleteTileTest : MonoBehaviour
{
    private void OnCollisionEnter2D(Collision2D ot)
    {
    //ここに処理
    }
}

あたった場所の座標を取る

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すべてを取得する

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型が楽だと思いますが、説明のために型を指定しています。


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

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

そのため、

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

このようにして「ほんとにTileあるの……?」と確認して、あるときだけそのPositionをallPositionに格納しました。ここらへんで時間かかりました。というか全部時間かかりました。

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

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;
            }
        }
    }
}

(存在を)消す(完成)

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は、四捨五入で整数にまるめてくれます。
なので、最終的な位置を一旦格納します。

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

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


        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でも落ちます。落ちます。
Otiru.png
それをふせぐためのfalse→trueの反復横跳びです。これをすると、Colliderの情報あたりがリフレッシュされるのか、いい感じに動きます。
Tileを消して、新たにColliderの形を決めてほしいから、という意図が主でしたが、結果うまくいきました。

おまけでParticleを出す

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でチョット調整しています。

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

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);
        }
    }
}

あとがき

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

19
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
12