【Unity/2D】タイルマップ上の特定タイルを移動床に変換するエディタ拡張

【Unity】タイルマップ上の特定のタイルを移動床に変換

タイルマップ上の特定のタイルの位置にオブジェクトを生成し、元のタイルを消すエディタ拡張。
おまけで、自作CharacterController2Dと連動する移動床のコードも。

サンプル動画

全体的な流れ。
ちゃんと変換元タイルが削除されているのが確認出来る。

(音なし)

変換元タイル画像

タイルマップto移動床(上下)
タイルマップto移動床(左右)

  • ライセンス: CC0
  • コードの都合上、ファイル名はそのままで使用してください。

使用方法

  1. 「変換元タイル画像」をProjectウィンドウに、ドラッグ&ドロップしてインポート。
    • Pixel Per Unit: 32に設定してApply
  2. 「インポートしたスプライト」を、メインで使っているTile Paletteに、ドラッグ&ドロップ。
  3. 保存フォルダを選びタイルを作成。
  4. タイルマップ上の移動床を出したい位置に「変換元タイル」を書き込む。
    (移動範囲の中心点になる)
  5. ↓のコードをコピペし、各スクリプト&移動床のプレハブを作成しておく。
  6. 対象のTilemapを右クリック -> 「Convert Moving Platform
  7. Tilemap下に移動床が出現するので、インスペクターからサイズや移動距離等を調整。

コード

ConvertMovingPlatformEditor

Tilemapコンポーネントを右クリックした時に、メニューが追加されるエディタ拡張。

  • Editorフォルダ下に、新規スクリプト「ConvertMovingPlatformEditor」を作成し、コードを全文コピペ。
    (Editorフォルダがない場合は新規作成)


using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEditor;

public class ConvertMovingPlatformEditor : EditorWindow
{

//判定用のスプライト名。
	static readonly string HorizontalMovingPlatformName = "horizontal-moving-platform";
	static readonly string VerticalMovingPlatformName = "vertical-moving-platform";
//移動床の出現位置がタイルとズレてしまうのを補正。
	static readonly Vector3 SpawnOffset = new Vector3(0.5f, 0.5f, 0);


	[MenuItem("CONTEXT/Tilemap/Convert Moving Platform")]
	static void ConvertMovingPlatform(MenuCommand command)
	{
		Tilemap tilemap = (Tilemap)command.context;
		TileBase tile;
		Vector3Int checkPosition = Vector3Int.zero;
		MovingPlatform spawnedMovingPlatform;
		GameObject movingPlatformPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Prefabs/MovingPlatform.prefab");


		for (int i = tilemap.origin.x; i < tilemap.size.x; i++) {
			for (int j = tilemap.origin.y; j < tilemap.size.y; j++) {
				checkPosition.Set(i, j, 0);
				tile = tilemap.GetTile(checkPosition);

//GetTileした位置に何もない場合はnullになるので弾く。
				if (tile != null) {
//タイル名は元のスプライトのアセット名になる。
					if (tile.name == HorizontalMovingPlatformName) {
						spawnedMovingPlatform = Instantiate(movingPlatformPrefab, tilemap.transform).GetComponent<MovingPlatform>();
						spawnedMovingPlatform.direction = MovingPlatform.Direction.Horizontal;
						spawnedMovingPlatform.tf.position = tilemap.CellToWorld(checkPosition) + SpawnOffset;

//エディタのVerが2019以下の場合、削除用メソッドがないので、空のタイルで上書き。
						tilemap.SetTile(checkPosition, new Tile());
//エディタのVerが2020以上なら、多分こっちでも消せる。
//                        tilemap.DeleteCells(checkPosition, Vector3Int.one);
					} else if (tile.name == VerticalMovingPlatformName) {
						spawnedMovingPlatform = Instantiate(movingPlatformPrefab, tilemap.transform).GetComponent<MovingPlatform>();
						spawnedMovingPlatform.direction = MovingPlatform.Direction.Vertical;
						spawnedMovingPlatform.tf.position = tilemap.CellToWorld(checkPosition) + SpawnOffset;

//エディタのVerが2019以下の場合、削除用メソッドがないので、空のタイルで上書き。
						tilemap.SetTile(checkPosition, new Tile());
//エディタのVerが2020以上なら多分こっちでも消せる。
//                        tilemap.DeleteCells(checkPosition, Vector3Int.one);
					}
				}

			}
		}
	}
}

MovingPlatform

移動床プレハブのメインスクリプト。

  1. 移動床プレハブの元となる空オブジェクト「MovingPlatform」を作成。
    • Rigidbody2Dコンポーネントをアタッチ。
    • タグとレイヤーをGroundに設定
      (無い場合は追加しつつ)
  2. 子の位置に、新規オブジェクト「PlatformsS」「PlatformsM」「PlatformsL」を作成。
    (Sサイズから順で)
  3. 「PlatformsS」を右クリック -> 2D Object -> Spriteを作成し、「Platform」と命名。
  4. 移動床のスプライトを設定。
  5. Box Collider 2D等で当たり判定を設定。
  6. Duplicateで複製しまくって、各Platformsサイズ毎に任意の個数設置。
    (S: 1, M: 3, L: 5等々)
  7. 新規スクリプト「MovingPlatform」を作成し、コードを全文コピペ。
  8. 「MovingPlatform」に「MovingPlatform」スクリプトをアタッチ。
  9. ProjectウィンドウのAssets/Prefabsの位置に、ドラッグ&ドロップしてプレハブ化。
    (Prefabsフォルダを作っていない場合は新規作成)


using UnityEngine;

public class MovingPlatform : MonoBehaviour
{
	public GameObject go;
	public Transform tf;

	[SerializeField]
	Rigidbody2D rb2d;


	public enum Direction {
		Horizontal,
		Vertical,
	}

	public Direction direction;

//個数はplatformGosの要素数と一致させておく。
	public enum SizeType {
		S,
		M,
		L,
	}

//一応デフォルトサイズはMサイズにしてある。
	public SizeType sizeType = SizeType.M;

	[Header("移動距離 (設置位置を基準として移動)")]
	[Range(1.0f, 100.0f)]
	[SerializeField]
	float movingDistance = 10.0f;

	[Header("移動速度 (秒速)")]
	[Range(1.0f, 100.0f)]
	[SerializeField]
	float speed = 10.0f;

//Time.fixedDeltaTimeを定数化した物。
	static readonly float FixedDeltaTime = 0.02f;

//サイズ別の足場(小さい物順)。コンポーネントのアタッチ時に自動取得されるようにしたが、上手くいかなかった場合は手動で紐付けしてください。
	[SerializeField]
	GameObject[] platformGos;

	Vector3 leftmostOrBottommostPosition;
	Vector3 rightmostOrTopmostPosition;

	Vector3 nextPosition;
	Vector3 velocity;

//プレイヤーとの衝突ノーマルが、(真上を中心として)この角度以下だと上に乗られたと見なす。
	static readonly float PlatformNormalAngleLimit = 30.0f;

	static readonly string PlayerTag = "Player";
	ContactPoint2D[] playerContacts = new ContactPoint2D[3];
	ContactFilter2D playerContactFilter;

	int contactCount;



//コレによってアタッチ時にモロモロ設定される。
	void Reset()
	{
		go = gameObject;
		tf = transform;

		rb2d = GetComponent<Rigidbody2D>();


		platformGos = new GameObject[tf.childCount];
		for (int i = 0; i < tf.childCount; i++) {
			platformGos[i] = tf.GetChild(i).gameObject;
			platformGos[i].SetActive(false);
		}


//出現位置が分かるように、デフォルトで足場を表示する場合。
//        platformGos[(int)sizeType].SetActive(true);
	}

	void Awake()
	{
		for (int i = 0; i < platformGos.Length; i++) {
			if (platformGos[i].activeSelf)
				platformGos[i].SetActive(false);
		}
		platformGos[(int)sizeType].SetActive(true);


		if (direction == Direction.Horizontal) {
			leftmostOrBottommostPosition = tf.position + Vector3.left * movingDistance;
			rightmostOrTopmostPosition = tf.position + Vector3.right * movingDistance;
		} else {
			leftmostOrBottommostPosition = tf.position + Vector3.up * movingDistance;
			rightmostOrTopmostPosition = tf.position + Vector3.down * movingDistance;
		}


		playerContactFilter.SetLayerMask(1 << LayerMask.NameToLayer(PlayerTag));
	}



//足場の移動とプレイヤーの移動を連動させるサンプル。
	void FixedUpdate()
	{
		nextPosition = Vector3.Lerp(leftmostOrBottommostPosition, rightmostOrTopmostPosition, Mathf.PingPong(Time.time * speed * FixedDeltaTime, 1.0f));
		velocity = nextPosition - tf.position;

//より精度を高くするなら、足場の上面にだけトリガーモードの当たり判定を設置し、Collider2D.IsTouchingLayersを使ってチェックするべし。
		contactCount = rb2d.GetContacts(playerContactFilter, playerContacts);
		for (int i = 0; i < contactCount; i++) {            
			if (Vector2.Angle(Vector2.down, playerContacts[i].normal) <= PlatformNormalAngleLimit) {
//画面上に1体しか存在しないプレイヤーの場合には、都度GetComponentするより、シングルトンで保持していた方が軽いが割愛。
				playerContacts[i].rigidbody.GetComponent<Player>().platformVelocity = velocity;
				break;
			}
		}

		rb2d.position = nextPosition;
	}
}

是非、ご活用ください。

タイトルとURLをコピーしました