Unity チュートリアルのタワーディフェンステンプレートを触ってみる(40)

Unity チュートリアルのタワーディフェンステンプレートを触ってみる(39)では SingleTowerPlacementArea の解説を行った。今回は TowerPlacementGrid の解説を行っていく。

1.タワーディフェンステンプレートのステージの設定編 – TowerPlacementGrid.cs

TowerPlacementGrid.cs について稚拙ながら解説

「TowerPlacementGrid.cs」は「Assets/Scripts/TowerDefense/Towers/Placement/TowerPlacementGrid.cs」の指しておりスクリプトについては以下の通り。内容としてはグリッド状のタワー設置に関する処理を行っている。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
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
    }
}

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 状態で初期化している。

スポンサーリンク
%d人のブロガーが「いいね」をつけました。