Unity チュートリアルのタワーディフェンステンプレートを触ってみる(39)では SingleTowerPlacementArea の解説を行った。今回は TowerPlacementGrid の解説を行っていく。
1.タワーディフェンステンプレートのステージの設定編 – TowerPlacementGrid.cs
TowerPlacementGrid.cs について稚拙ながら解説
「TowerPlacementGrid.cs」は「Assets/Scripts/TowerDefense/Towers/Placement/TowerPlacementGrid.cs」の指しておりスクリプトについては以下の通り。内容としてはグリッド状のタワー設置に関する処理を行っている。
[cce_csharp]using System; using Core.Utilities; using TowerDefense.UI.HUD; using UnityEngine; namespace TowerDefense.Towers.Placement { /// <summary> /// A tower placement location made from a grid. /// Its origin is centered in the middle of the lower-right cell. It can be oriented in any direction /// </summary> [RequireComponent(typeof(BoxCollider))] public class TowerPlacementGrid : MonoBehaviour, IPlacementArea { /// <summary> /// Prefab used to visualise the grid /// </summary> public PlacementTile placementTilePrefab; /// <summary> /// Visualisation prefab to instantiate on mobile platforms /// </summary> public PlacementTile placementTilePrefabMobile; /// <summary> /// The dimensions of the grid /// </summary> public IntVector2 dimensions; /// <summary> /// Size of the edge of a cell /// </summary> [Tooltip("The size of the edge of one grid cell for this area. Should match the physical grid size of towers")] public float gridSize = 1; /// <summary> /// Inverted grid size, to multiply with /// </summary> float m_InvGridSize; /// <summary> /// Array of available cells /// </summary> bool[,] m_AvailableCells; /// <summary> /// Array of <see cref="PlacementTile"/>s /// </summary> PlacementTile[,] m_Tiles; /// <summary> /// Converts a location in world space into local grid coordinates. /// </summary> /// <param name="worldLocation"><see cref="Vector3"/> indicating world space coordinates to convert.</param> /// <param name="sizeOffset"><see cref="IntVector2"/> indicating size of object to center.</param> /// <returns><see cref="IntVector2"/> containing the grid coordinates corresponding to this location.</returns> public IntVector2 WorldToGrid(Vector3 worldLocation, IntVector2 sizeOffset) { Vector3 localLocation = transform.InverseTransformPoint(worldLocation); // Scale by inverse grid size localLocation *= m_InvGridSize; // Offset by half size var offset = new Vector3(sizeOffset.x * 0.5f, 0.0f, sizeOffset.y * 0.5f); localLocation -= offset; int xPos = Mathf.RoundToInt(localLocation.x); int yPos = Mathf.RoundToInt(localLocation.z); return new IntVector2(xPos, yPos); } /// <summary> /// Returns the world coordinates corresponding to a grid location. /// </summary> /// <param name="gridPosition">The coordinate in grid space</param> /// <param name="sizeOffset"><see cref="IntVector2"/> indicating size of object to center.</param> /// <returns>Vector3 containing world coordinates for specified grid cell.</returns> public Vector3 GridToWorld(IntVector2 gridPosition, IntVector2 sizeOffset) { // Calculate scaled local position Vector3 localPos = new Vector3(gridPosition.x + (sizeOffset.x * 0.5f), 0, gridPosition.y + (sizeOffset.y * 0.5f)) * gridSize; return transform.TransformPoint(localPos); } /// <summary> /// Tests whether the indicated cell range represents a valid placement location. /// </summary> /// <param name="gridPos">The grid location</param> /// <param name="size">The size of the item</param> /// <returns>Whether the indicated range is valid for placement.</returns> public TowerFitStatus Fits(IntVector2 gridPos, IntVector2 size) { // If the tile size of the tower exceeds the dimensions of the placement area, immediately decline placement. if ((size.x > dimensions.x) || (size.y > dimensions.y)) { return TowerFitStatus.OutOfBounds; } IntVector2 extents = gridPos + size; // Out of range of our bounds if ((gridPos.x < 0) || (gridPos.y < 0) || (extents.x > dimensions.x) || (extents.y > dimensions.y)) { return TowerFitStatus.OutOfBounds; } // Ensure there are no existing towers within our tile silhuette. for (int y = gridPos.y; y < extents.y; y++) { for (int x = gridPos.x; x < extents.x; x++) { if (m_AvailableCells[x, y]) { return TowerFitStatus.Overlaps; } } } // If we've got this far, we've got a valid position. return TowerFitStatus.Fits; } /// <summary> /// Sets a cell range as being occupied by a tower. /// </summary> /// <param name="gridPos">The grid location</param> /// <param name="size">The size of the item</param> public void Occupy(IntVector2 gridPos, IntVector2 size) { IntVector2 extents = gridPos + size; // Validate the dimensions and size if ((size.x > dimensions.x) || (size.y > dimensions.y)) { throw new ArgumentOutOfRangeException("size", "Given dimensions do not fit in our grid"); } // Out of range of our bounds if ((gridPos.x < 0) || (gridPos.y < 0) || (extents.x > dimensions.x) || (extents.y > dimensions.y)) { throw new ArgumentOutOfRangeException("gridPos", "Given footprint is out of range of our grid"); } // Fill those positions for (int y = gridPos.y; y < extents.y; y++) { for (int x = gridPos.x; x < extents.x; x++) { m_AvailableCells[x, y] = true; // If there's a placement tile, clear it if (m_Tiles != null && m_Tiles[x, y] != null) { m_Tiles[x, y].SetState(PlacementTileState.Filled); } } } } /// <summary> /// Removes a tower from a grid, setting its cells as unoccupied. /// </summary> /// <param name="gridPos">The grid location</param> /// <param name="size">The size of the item</param> public void Clear(IntVector2 gridPos, IntVector2 size) { IntVector2 extents = gridPos + size; // Validate the dimensions and size if ((size.x > dimensions.x) || (size.y > dimensions.y)) { throw new ArgumentOutOfRangeException("size", "Given dimensions do not fit in our grid"); } // Out of range of our bounds if ((gridPos.x < 0) || (gridPos.y < 0) || (extents.x > dimensions.x) || (extents.y > dimensions.y)) { throw new ArgumentOutOfRangeException("gridPos", "Given footprint is out of range of our grid"); } // Fill those positions for (int y = gridPos.y; y < extents.y; y++) { for (int x = gridPos.x; x < extents.x; x++) { m_AvailableCells[x, y] = false; // If there's a placement tile, clear it if (m_Tiles != null && m_Tiles[x, y] != null) { m_Tiles[x, y].SetState(PlacementTileState.Empty); } } } } /// <summary> /// Initialize values /// </summary> protected virtual void Awake() { ResizeCollider(); // Initialize empty bool array (defaults are false, which is what we want) m_AvailableCells = new bool[dimensions.x, dimensions.y]; // Precalculate inverted grid size, to save a division every time we translate coords m_InvGridSize = 1 / gridSize; SetUpGrid(); } /// <summary> /// Set collider's size and center /// </summary> void ResizeCollider() { var myCollider = GetComponent<BoxCollider>(); Vector3 size = new Vector3(dimensions.x, 0, dimensions.y) * gridSize; myCollider.size = size; // Collider origin is our bottom-left corner myCollider.center = size * 0.5f; } /// <summary> /// Instantiates Tile Objects to visualise the grid and sets up the <see cref="m_AvailableCells" /> /// </summary> protected void SetUpGrid() { PlacementTile tileToUse; #if UNITY_STANDALONE tileToUse = placementTilePrefab; #else tileToUse = placementTilePrefabMobile; #endif if (tileToUse != null) { // Create a container that will hold the cells. var tilesParent = new GameObject("Container"); tilesParent.transform.parent = transform; tilesParent.transform.localPosition = Vector3.zero; tilesParent.transform.localRotation = Quaternion.identity; m_Tiles = new PlacementTile[dimensions.x, dimensions.y]; for (int y = 0; y < dimensions.y; y++) { for (int x = 0; x < dimensions.x; x++) { Vector3 targetPos = GridToWorld(new IntVector2(x, y), new IntVector2(1, 1)); targetPos.y += 0.01f; PlacementTile newTile = Instantiate(tileToUse); newTile.transform.parent = tilesParent.transform; newTile.transform.position = targetPos; newTile.transform.localRotation = Quaternion.identity; m_Tiles[x, y] = newTile; newTile.SetState(PlacementTileState.Empty); } } } } #if UNITY_EDITOR /// <summary> /// On editor/inspector validation, make sure we size our collider correctly. /// Also make sure the collider component is hidden so nobody can mess with its settings to ensure its integrity. /// Also communicates the idea that the user should not need to modify those values ever. /// </summary> void OnValidate() { // Validate grid size if (gridSize <= 0) { Debug.LogError("Negative or zero grid size is invalid"); gridSize = 1; } // Validate dimensions if (dimensions.x <= 0 || dimensions.y <= 0) { Debug.LogError("Negative or zero grid dimensions are invalid"); dimensions = new IntVector2(Mathf.Max(dimensions.x, 1), Mathf.Max(dimensions.y, 1)); } // Ensure collider is the correct size ResizeCollider(); GetComponent<BoxCollider>().hideFlags = HideFlags.HideInInspector; } /// <summary> /// Draw the grid in the scene view /// </summary> void OnDrawGizmos() { Color prevCol = Gizmos.color; Gizmos.color = Color.cyan; Matrix4x4 originalMatrix = Gizmos.matrix; Gizmos.matrix = transform.localToWorldMatrix; // Draw local space flattened cubes for (int y = 0; y < dimensions.y; y++) { for (int x = 0; x < dimensions.x; x++) { var position = new Vector3((x + 0.5f) * gridSize, 0, (y + 0.5f) * gridSize); Gizmos.DrawWireCube(position, new Vector3(gridSize, 0, gridSize)); } } Gizmos.matrix = originalMatrix; Gizmos.color = prevCol; // Draw icon too, in center of position Vector3 center = transform.TransformPoint(new Vector3(gridSize * dimensions.x * 0.5f, 1, gridSize * dimensions.y * 0.5f)); Gizmos.DrawIcon(center, "build_zone.png", true); } #endif } }[/cce_csharp]
57行目 : 「public IntVector2 WorldToGrid(Vector3 worldLocation, IntVector2 sizeOffset)」は IPlacementArea で定義した関数の処理を作成している。内容としては引数として与えたれた世界座標をローカル座標変換し、グリッド位置の計算を行っている。
80行目 : 「public Vector3 GridToWorld(IntVector2 gridPosition, IntVector2 sizeOffset)」は IPlacementArea で定義した関数の処理を作成している。内容としては引数として与えたれたローカル座標をオフセット分ずらし、世界座標系への変換を行っている。
95行目 : 「public TowerFitStatus Fits(IntVector2 gridPos, IntVector2 size)」は IPlacementArea で定義した関数の処理を作成している。内容としてはタワーサイズがグリッド内に収まっているか判定し収まっていなければ OutOfBounds を返却している。また、範囲内にすでにタワーが存在していれば Overlaps を返却し、範囲内にすでにタワーが存在していなければ Fits を返却している。
133行目 : 「public void Occupy(IntVector2 gridPos, IntVector2 size)」は IPlacementArea で定義した関数の処理を作成している。内容としてはタワーサイズがグリッド内に収まっているか判定し収まっていなければ エラーを出力し、グリッド内のタイルの状態を Filled にしている。
171行目 : 「public void Clear(IntVector2 gridPos, IntVector2 size)」は IPlacementArea で定義した関数の処理を作成している。内容としてはタワーサイズがグリッド内に収まっているか判定し収まっていなければ エラーを出力し、グリッド内のタイルの状態を Empty にしている。
207行目 : 「protected virtual void Awake()」は Unity 固有の起動処理を行っている。内容としては ResizeCollider 処理後、グリッド情報の初期化し、SetUpGrid 処理を行っている。
223行目 : 「void ResizeCollider()」は Collider のサイズと中心座標の設定をし直している。
236行目 : 「protected void SetUpGrid()」は #if UNITY_STANDALONE で PC として起動されているか、携帯機として起動されているか判定し、使用するタイルの設定を行っている。タイルの状態を Empty 状態で初期化している。