Unity チュートリアルのタワーディフェンステンプレートを触ってみる(40)
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 状態で初期化している。