#なんか、TilemapでTileを消したいなぁ……(なお、公式バグらしいです。)
Tilemapと衝突した際に、そのTileを取得、消して、Particleを出した試行錯誤の記事。
たまに2個消える
Rigidbody2Dをぶつけたいのにアタッチ、なんか好きな手段で動かしてみてください。
今回は、ProjectSettingsから、Physics2DのXYの数値を0にして、上下左右にキーで動かしましたが、サンプルコードからは取り除いています。最後に記載している最終的なコードには載せています。
##目次
##結論
安定性に少し欠けるが、とりあえずTilemapのTileを、場所を指定してスクリプトから消すことができました。
Unity2018.3以降では確認済みですが、基本大丈夫なんじゃないでしょうか。
varでもいいけど全部変数は明示してます。
また、サンプルソースはだんだん長くなっていく感じです。
スクリプトは、ぶつかるものにアタッチしてください。説明のため(言い訳)に神クラス風味になっています。
##概要(?)
###躓いたポイント
- Tilemapの文献が少ない
- あっても英語
- SetTile使うと、2回目にエディタがぶっ壊れて落ちた(最重要)
###なんでやろうと思ったか
「ステージを壊して進むような演出がしたい!!」
→
「Tilemapはmapが作りやすいけど、動的なのやるときにMapを複数作るのめんどい……」
「エディタで普通に配置するほどの労力をかけたくない……」
「よもやCSVでインスタンシエイトして配置するわけにもいかない……」
⇒「なんとかしてTilemapをスクリプトから消すぞオレはこの野郎」
###どうやってやったか
- いろいろ苦労した
- OnCollisionEnter2Dを使う
- あたった場所の座標を取る
- 存在するTileすべてを取得する
- 存在するTileの中で、ぶつかった場所に一番近いTileの座標を計算する
- 消す(完成)
- おまけでParticleを出す
##実際の処理
###いろいろ苦労する
文献が少ない気がする。ググラビティの欠如かも。公式も英語だし。
最終的にはRider神の神託に頼りつつ総当たりで検証。なんとかうまくいった。
少しでも参考になったら。
###OnCollisionEnter2Dを使う
当たり判定なので、当然これを使う。
using System.Collections.Generic;
using UnityEngine;
public class DeleteTileTest : MonoBehaviour
{
private void OnCollisionEnter2D(Collision2D ot)
{
//ここに処理
}
}
###あたった場所の座標を取る
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すべてを取得する
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の座標を計算する
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;
}
}
}
}
###~~(存在を)~~消す(完成)
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でも落ちます。落ちます。
それをふせぐためのfalse→trueの反復横跳びです。これをすると、Colliderの情報あたりがリフレッシュされるのか、いい感じに動きます。
Tileを消して、新たにColliderの形を決めてほしいから、という意図が主でしたが、結果うまくいきました。
###おまけでParticleを出す
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でチョット調整しています。
##一応、最終的な私のスクリプト
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);
}
}
}
##あとがき
初学者ですんごく大変だったので、初めて記事として残してみました。
「この処理になんの意味があるんですか?」といわれたら「なんか楽しくなっちゃったからやりました!!!!反省はしません!!!!」と答えます。
以上、ご参考頂けたら幸いです。