Onvalidate unity что это
Перейти к содержимому

Onvalidate unity что это

  • автор:

MonoBehaviour.OnValidate()

Thank you for helping us improve the quality of Unity Documentation. Although we cannot accept all submissions, we do read each suggested change from our users and will make updates where applicable.

Submission failed

For some reason your suggested change could not be submitted. Please <a>try again</a> in a few minutes. And thank you for taking the time to help us improve the quality of Unity Documentation.

Description

Editor-only function that Unity calls when the script is loaded or a value changes in the Inspector.

Use this to perform an action after a value changes in the Inspector; for example, making sure that data stays within a certain range.

Note: You should not use this callback to do other tasks such as create objects or call other non-thread-safe Unity API. You should only use it to validate the data that changed. This restriction is because OnValidate can be be called often when the user interacts with an inspector in the Editor, and because OnValidate can be called from threads other than Unity’s main thread, such as the loading thread.

On Validate Method

Where the Reset Method only gets called once when the component is first added, Unity will call OnValidate much more often:

  • When the component is first added or the Reset function is used from its context menu (same as Reset ).
  • When the component is loaded, such as when opening a scene, editing a prefab, or after you modify a script and Unity recompiles it.
  • When the component is modified in the Inspector.

So we can use it to simply get the required component if one wasn’t already assigned:

Создание игры Tower Defense в Unity, часть 1

Tower defense — это жанр, в которой целью игрока является уничтожение толп врагов, пока они не добрались до своей конечной точки. Игрок выполняет свою цель, строя башни, которые атакуют врагов. У этого жанра очень много вариаций. Мы будем создавать игру с тайловым полем. Враги будут двигаться по полю в сторону своей конечной точки, а игрок будет создавать им препятствия.

Я буду считать, что вы уже изучили серию туториалов по управлению объектами.

Игровое поле — самая важная часть игры, поэтому его мы создадим первым. Это будет игровой объект (game object) с собственным компонентом GameBoard , который можно инициализировать заданием размера в двух измерениях, для чего мы можем воспользоваться значением Vector2Int . Поле должно работать с любым размером, но выбирать размер мы будем где-нибудь в другом месте, поэтому создадим для этого общий метод Initialize .

Кроме того, мы визуализируем поле одним четырёхугольником (quad), который будет обозначать землю. Мы не будем делать четырёхугольником сам объект поля, а добавим ему дочерний объект quad. При инициализации мы сделаем масштаб XY земли равным размеру поля. То есть каждый тайл будет иметь размер в одну квадратную единицу измерения движка.

Создадим объект поля в новой сцене и добавим ему дочерний quad с материалом, который выглядит как земля. Так как мы создаём простую игру-прототип, вполне достаточно будет однородного зелёного материала. Повернём quad на 90° по оси X, чтобы он лежал на плоскости XZ.

Игровое поле.

Далее создадим компонент Game , который будет отвечать за всю игру. На данном этапе это будет означать, что он инициализирует поле. Мы просто сделаем размер настраиваемым через инспектор и заставим компонент инициализировать поле при его пробуждении. Давайте используем по умолчанию размер 11×11.

Размеры поля могут быть только положительными и не имеет особого смысла создавать поле с единственным тайлом. Поэтому давайте ограничим минимум размером 2×2. Это можно сделать, добавив метод OnValidate , принудительно ограничивающий минимальные значения.

Если он существует, то редактор Unity вызывает его для компонентов после их изменения. В том числе при добавлении их к game object, после загрузки сцены, после рекомпиляции, после изменения в редакторе, после отмены/повтора и после сброса компонента.

OnValidate — это единственное место в коде, где допускается присвоение значений полям конфигурации компонентов.

Game object.

Теперь при запуске режима игры мы будем получать поле с верным размером. Во время игры расположите камеру так, чтобы была видна вся доска, скопируйте её компонент transformation, выйдите из режима игры (play mode) и вставьте значения компонента. В случае поля размером 11×11, находящегося в начале координат, для получения удобного вида сверху можно расположить камеру в позиции (0,10,0) и повернув её на 90° по оси X. Мы оставим камеру в этом фиксированном положении, но возможно изменим его в будущем.

Камера над полем.

Префаб тайла

Поле состоит из квадратных тайлов. Враги смогут передвигаться с тайла на тайл, пересекая края, но не по диагонали. Движение всегда будет происходит по направлению к ближайшей конечной точке. Давайте графически обозначим направление движения по тайлу стрелкой. Скачать текстуру стрелки можно здесь.

Стрелка на чёрном фоне.

Поместите текстуру стрелки в свой проект и включите опцию Alpha As Transparency. Затем создайте для стрелки материал, который может быть стандартным материалом (default material), для которого выбран режим cutout, а в качестве основной текстуры выберите стрелку.

Материал стрелки.

Для обозначения каждого тайла в игре мы будем использовать game object. Каждый из них будет иметь свой quad с материалом стрелки, так же, как у поля есть quad земли. Также мы добавим тайлам компонент GameTile со ссылкой на их стрелку.

Создайте объект тайла и превратите его в префаб. Тайлы будут находиться вровень с землёй, поэтому приподнимите стрелку немного вверх, чтобы избежать проблем с глубиной при рендеринге. Также немного уменьшите масштаб стрелки, чтобы между соседними стрелками было немного пространства. Подойдёт смещение по Y 0.001 и одинаковый для всех осей масштаб 0.8.

Префаб тайла.

Учтите, что сами тайлы необязательно должны быть game objects. Они нужны только для того, чтобы отслеживать состояние поля. Мы могли бы использовать тот же подход, что и для поведения в серии туториалов Object Management. Но на ранних этапах простых игр или прототипов game objects вполне нас устраивают. В будущем это можно будет изменить.

Располагаем тайлы

Для создания тайлов GameBoard должен иметь ссылку на префаб тайла.

Ссылка на префаб тайла.

Затем он может создать его экземляры с помощью двойного цикла по двум измерениям сетки. Хоть размер и выражен как X и Y, мы будем располагать тайлы на плоскости XZ, как и само поле. Так как поле центрировано относительно точки начала координат, нам нужно вычесть из компонентов позиции тайла соответствующий размер минус один, разделённый на два. Учтите, что это должно быть деление с плавающей запятой, в противном случае для чётных размеров оно не сработает.

Созданные экземпляры тайлов.

Позже нам понадобится доступ к этим тайлам, поэтому будем отслеживать их в массиве. Нам не нужен список, потому что после инициализации размер поля меняться не будет.

Это сцеплённое присвоение. В данном случае это означает, что мы присваиваем ссылку на экземпляр тайла и элементу массива, и локальной переменной. Эти операции выполняют то же, что и показанный ниже код.

Поиск пути

На этом этапе у каждого тайла есть стрелка, но все они указывают в положительном направлении оси Z, которое мы будем интерпретировать как север. Следующим этапом будет определение правильного направления для тайла. Мы реализуем это нахождением пути, по которому враги должны следовать к конечной точке.

Соседи тайлов

Пути идут от тайла к тайлу, в северном, восточном, южном или западном направлении. Чтобы упростить поиск, заставим GameTile отслеживать ссылки на четырёх его соседей.

Отношения между соседями симметричны. Если тайл является восточным соседом второго тайла, то второй является западным соседом первого. Добавим к GameTile общий статический метод для определения этих отношений между двумя тайлами.

После установления связи она никогда не должна меняться. Если это случится, то мы совершили ошибку в коде. Можно проверять это, сравнивая обе ссылки перед присваиванием значений с null , и выводя в консоль ошибку, если это неверно. Для этого можно использовать метод Debug.Assert .

Добавим аналогичный метод для создания отношений между северными и южными соседями.

Мы можем установить эти отношения при создании тайлов в GameBoard.Initialize . Если координата X больше нуля, то мы можем создать отношение восток-запад между текущим и предыдущим тайлом. Если координата Y больше нуля, то мы можем создать отношение север-юг между текущим тайлом и тайлом из предыдущей строки.

Учтите, что тайлы на краях поля имеют не четырёх соседей. Одна или две ссылки на соседей будут оставаться равными null .

Расстояние и направление

Мы не будем заставлять всех врагов постоянно искать путь. Это необходимо делать только один раз за тайл. Тогда враги смогут запрашивать у тайла, в котором находятся, куда двигаться дальше. Мы будем хранить эту информацию в GameTile , добавив ссылку на следующий тайл пути. Кроме того, мы также сохраним расстояние до конечной точки, выраженную в виде количества тайлов, которые нужно посетить, прежде чем враг достигнет конечной точки. Для врагов эта информация бесполезна, но мы будем применять её для нахождения кратчайших путей.

Каждый раз, когда мы решим, что нужно искать пути, нам нужно будет инициализировать данные пути. Пока путь не найден, следующего тайла нет и расстояние можно считать бесконечным. Мы можем представить это максимальным возможным целочисленным значением int.MaxValue . Добавим общий метод ClearPath , чтобы выполнить сброс GameTile к этому состоянию.

Пути можно искать, только если у нас есть конечная точка. Это значит, что тайл должен стать конечной точкой. Такой тайл имеет расстояние, равное нулю, и у него нет последнего тайла, потому что путь завершается на нём. Добавим общий метод, превращающий тайл в конечную точку.

В конечном итоге все тайлы должны превратиться в путь, поэтому их расстояние больше не будет равно int.MaxValue . Добавим удобное свойство-геттер, чтобы проверять, есть ли в данный момент у тайла путь.

Это укороченная запись задания свойства-геттера, содержащего только одно выражение. Она делает то же самое, что и показанный ниже код.

Оператор-стрелку => также можно использовать по отдельности для геттера и сеттера свойств, для тел методов, конструкторов и в некоторых других местах.

Выращиваем путь

Если у нас есть тайл с путём, то мы можем позволить ему вырастить путь по направлению к одному из соседей. Изначально единственным тайлом с путём является конечная точка, поэтому мы начинаем с нулевого расстояния и увеличиваем его отсюда, перемещаясь в противоположном движению врагов направлении. То есть все непосредственные соседи конечной точки будут иметь расстояние 1, а все соседи этих тайлов — расстояние 2, и так далее.

Добавим GameTile скрытый метод для выращивания пути к одному из его соседей, задаваемому через параметр. Расстояние до соседа становится на единицу больше, чем у текущего тайла, а путь соседа указывает на текущий тайл. Этот метод должен вызываться только для тех тайлов, у которых уже есть путь, так что давайте проверять это с помощью assert.

Идея заключается в том, что мы вызываем этот метод один раз для каждого из четырёх соседей тайла. Так как некоторые из этих ссылок будут равны null , то мы будем проверять это и прекращать выполнение, если это так. Кроме того, если у соседа уже есть путь, то мы ничего не должны делать и тоже прекращаем выполнение.

То, как GameTile отслеживает своих соседей, неизвестно остальному коду. Поэтому GrowPathTo является скрытым. Мы добавим общие методы, приказывающие тайлу вырастить его путь в определённом направлении, косвенно вызывая GrowPathTo . Но код, который занимается поиском по всему полю, должен отслеживать, какие тайлы были посещены. Поэтому сделаем так, чтобы он возвращал соседа или null , если выполнение прекращено.

Теперь добавим методы для выращивания пути в конкретных направлениях.

Поиск в ширину

Гарантировать, что все тайлы содержат верные данные пути, должен GameBoard . Мы реализуем это выполнением поиска в ширину (breadth-first search). Начнём с тайла конечной точки, а затем вырастим путь до его соседей, потом до соседей этих тайлов, и так далее. С каждым шагом расстояние увеличивается на единицу, а пути никогда не растут в сторону тайлов, у которых уже есть пути. Это гарантирует, что все тайлы в результате будут указывать вдоль кратчайшего пути к конечной точке.

Для выполнения поиска нам нужно отслеживать тайлы, которые мы добавили к пути, но из которых пока не вырастили путь. Эту коллекцию тайлов часто называют границей поиска (search frontier). Важно, чтобы тайлы обрабатывались в том же порядке, в котором они добавляются к границе, поэтому давайте используем очередь Queue . Позже нам придётся выполнять поиск несколько раз, поэтому зададим её как поле (field) GameBoard .

Чтобы состояние игрового поля всегда было верным, мы должны находить пути в конце Initialize , но поместить код в отдельный метод FindPaths . Первым делом нужно очистить путь у всех тайлов, затем сделать один тайл конечной точкой и добавить его к границе. Давайте сначала выберем первый тайл. Так как tiles является массивом, мы можем использовать цикл foreach , не боясь загрязнения памяти. Если позже мы перейдём от массива к списку, то нужно будет также заменить циклы foreach циклами for .

Далее нам нужно взять один тайл из границы и вырастить путь ко всем его соседям, добавив их все в границу. Сначала двинемся на север, потом на восток, юг и наконец запад.

Повторяем этот этап, пока в границе есть тайлы.

Выращивание пути не всегда приводит нас к новому тайлу. Перед добавлением в очередь нам нужно проверять значение на null , но можно и отложить проверку на null до момента после вывода из очереди.

Отображаем пути

Теперь у нас есть поле, содержащее верные пути, но пока мы этого не видим. Надо настроить стрелки так, чтобы они указывали вдоль пути через их тайлы. Это можно сделать, повернув их. Так как эти повороты всегда одинаковы, добавим в GameTile по одному статическому полю Quaternion для каждого из направлений.

Также добавим общий метод ShowPath . Если расстояние равно нулю, то тайл является конечной точкой и ему не на что указывать, поэтому деактивируем его стрелку. В противном случае активирум стрелку и задаём её поворот. Нужное направление можно определить, сравнив nextOnPath с его соседями.

Вызовем этот метод для всех тайлов в конце GameBoard.FindPaths .

Найденные пути.

Изменяем приоритет поиска

Оказывается, что когда конечной точкой является юго-западный угол, все пути идут ровно на запад, пока не достигнут края поля, после чего поворачивают на юг. Здесь всё верно, потому что более кратких путей к конечной точке и в самом деле нет, ведь диагональные перемещения невозможны. Однако существует множество других кратчайших путей, которые могут выглядеть красивее.

Чтобы лучше понимать, почему находятся такие пути, переместим конечную точку в центр карты. При нечётном размере поля это просто тайл в середине массива.

Конечная точка в центре.

Результат кажется логичным, если вспомнить, как работает поиск. Так как мы добавляем соседей в порядке «север-восток-юг-запад», наивысший приоритет имеет север. Так как мы выполняем поиск в обратном порядке, это значит, что последним пройденным направлением оказывается юг. Именно поэтому всего несколько стрелок указывает на юг и многие указывают на восток.

Изменить результат можно, настроив приоритеты направлений. Давайте поменяем местами восток и юг. Так мы должны получить симметрию «север-юг» и «восток-запад».

Порядок поиска «север-юг-восток-запад».

Это выглядит красивее, но лучше, чтобы пути меняли направление, приближаясь к движению по диагонали там, где это будет выглядеть естественно. Мы можем сделать это, перевернув приоритеты поиска соседних тайлов в шахматном порядке.

Вместо того, чтобы выяснять, какой тип тайла мы обрабатываем во время поиска, добавим в GameTile общее свойство, указывающее, является ли текущий тайл альтернативным.

Это свойство мы будем задавать в GameBoard.Initialize . Сначала пометим тайлы как альтернативные, если их координата X чётная.

Одиночный амперсанд — это двоичный оператор И (AND). Он выполняет логическую операцию И для каждой отдельной пары битов его операндов. Поэтому чтобы конечный бит был равен 1, оба бита пары должны быть равны 1. Например 10101010 и 00001111 дают нам 00001010.

В компьютерах числа хранятся в двоичном виде. В них могут использоваться только 0 и 1. В двоичном виде последовательность 1, 2, 3, 4 записывается как 1, 10, 11, 100. Как видите, самый младший разряд чётных чисел равен нулю.

Мы используем двоичное AND как маску, игнорируя всё, кроме самого младшего разряда. Если результат равен нулю, то мы имеем дело с чётным числом.

Во-вторых, изменим знак результата, если их координата Y чётная. Так мы создадим шахматный узор.

В FindPaths мы сохраним тот же порядок поиска для альтернативных тайлов, но сделаем его обратным для всех прочих тайлов. Это заставит пути стремиться к диагональному движению и создавать зигзаги.

Переменный порядок поиска.

Изменяем тайлы

На этом этапе все тайлы пусты. Один тайл используется как конечная точка, но кроме отсутствия видимой стрелки он выглядит так же, как все остальные. Мы добавим возможность изменения тайлов размещением на них объектов.

Содержимое тайла

Сами по себе объекты тайлов — это просто способ отслеживания информации о тайле. Мы не изменяем эти объекты напрямую. Вместо этого добавим отдельное содержимое и разместим его на поле. Пока мы можем различать пустые тайлы и тайл конечной точки. Для обозначения этих случаев создадим перечисление GameTileContentType .

Далее создадим тип компонента GameTileContent , который позволит задавать тип его содержимого через инспектор, а доступ к нему будет осуществляться через общее свойство-геттер.

Затем создадим префабы для двух типов контента, у каждого из которых компонент GameTileContent с соответствующим заданным типом. Давайте для обозначения тайлов конечных точек воспользуемся голубым сплющенным кубом. Так как он почти плоский, коллайдер ему не нужен. Для префаба пустого содержимого используем пустой game object.

destination

empty

Префабы конечной точки и пустого содержимого.

Мы дадим пустым тайлам объект содержимого, потому что тогда у всех тайлов всегда будет содержимое, а значит, нам не нужно будет проверять ссылки на содержимое на равенство null .

Фабрика содержимого

Чтобы сделать содержимое редактируемым, мы также создадим для этого фабрику, воспользовавшись тем же подходом, что и в туториале Object Management. Это значит, что GameTileContent должен отслеживать свою исходную фабрику, которая должна задаваться только один раз, и отправлять себя обратно на фабрику в методе Recycle .

Это предполагает существование GameTileContentFactory , поэтому создадим для этого тип scriptable object с обязательным методом Recycle . На данном этапе мы пока не будем заморачиваться созданием полнофункциональной фабрики, утилизирующей содержимое, поэтому заставим её просто уничтожать содержимое. Позже можно будет добавить к фабрике многократное использование объектов без изменения всего остального кода.

Добавим фабрике скрытый метод Get с префабом в качестве параметра. Здесь мы снова пропустим многократное применение объектов. Он создаёт экземпляр объекта, задаёт его исходную фабрику, перемещает его на сцену фабрики и возвращает его.

Экземпляр перемещён на сцену содержимого фабрики, которую можно создавать по необходимости. Если мы находимся в редакторе, то прежде чем создавать сцену, нужно проверить, существует ли она, на случай, если мы потеряем её из виду при горячем перезапуске.

У нас есть только два типа содержимого, поэтому просто добавим для них два поля конфигурации префабов.

Последнее, что нужно сделать для работы фабрики — создать общий метод Get с параметром GameTileContentType , получающий экземпляр соответствующего префаба.

Создадим ассет фабрики и настроим её ссылки на префабы.

Фабрика содержимого.

А затем передадим Game ссылку на фабрику.

Game с фабрикой.

Касание тайла

Чтобы изменять поле, нам нужно иметь возможность выбора тайла. Мы сделаем так, чтобы это было возможно в режиме игры. Будем испускать луч в сцену в месте, где игрок нажал на окно игры. Если луч пересекается с тайлом, то его коснулся игрок, то есть его необходимо изменить. Game будет обрабатывать ввод игрока, но за определение того, какого тайла коснулся игрок, будет отвечать GameBoard .

Не все лучи пересекутся с тайлом, поэтому иногда мы не будем получать ничего. Поэтому добавим в GameBoard метод GetTile , который изначально всегда возвращает null (это означает, что тайл не был найден).

Чтобы определить, пересёк ли луч тайл, нам нужно вызвать Physics.Raycast , указав в качестве аргумента луч. Он возвращает информацию о том, было ли пересечение. Если да, то мы сможем вернуть тайл, хоть пока и не знаем какой, поэтому пока возвращаем null .

Чтобы узнать, было ли пересечение с тайлом, нам нужно больше информации о пересечении. Physics.Raycast может предоставить эту информацию с помощью второго параметра RaycastHit . Это выходной параметр, что обозначается словом out перед ним. Это означает, что вызов метода может присвоить значение переменной, которую мы ему передаём.

Мы можем встроить объявление переменных, используемых для выходных параметров, поэтому давайте так и сделаем.

Нас не волнует, с каким именно коллайдером произошло пересечение, мы просто используем позицию XZ пересечения, чтобы определить тайл. Координаты тайла мы получаем, прибавив к координатам точки пересечения половину размера поля, а затем преобразовав результаты в целые значения. Окончательный индекс тайла в результате будет его координатой X плюс координатой Y, умноженной на ширину поля.

Но это возможно только когда координаты тайла находятся в пределах поля, поэтому будем проверять это. Если это не так, то тайл не возвращаем.

Изменение содержимого

Чтобы можно было изменять содержимое тайла, добавим к GameTile общее свойство Content . Его геттер просто возвращает содержимое, а сеттер утилизирует предыдущее содержимое, если оно было, и размещает новое содержимое.

Это единственное место, где нужно проверять содержимое на null , потому что изначально у нас нет содержимого. Для гарантии выполним assert, чтобы сеттер не вызывался с null .

И, наконец, нам нужен ввод игрока. Преобразование щелчка мыши в луч можно выполнить вызовом ScreenPointToRay с Input.mousePosition в качестве аргумента. Вызов нужно выполнять для основной камеры, доступ к которой можно получить через Camera.main . Добавим для этого свойство в Game .

Затем добавим метод Update , проверяющий, была ли при обновлении нажата основная клавиша мыши. Для этого нужно вызвать Input.GetMouseButtonDown с нулём в качестве аргумента. Если клавиша была нажата, обрабатываем касание игрока, то есть берём тайл с поля, и задаём в качестве его содержимого конечную точку, взяв её из фабрики.

Теперь мы можем превратить любой тайл в конечную точку нажатием курсора.

Несколько конечных точек.

Делаем поле правильным

Хоть мы и можем превращать тайлы в конечные точки, это пока не влияет на пути. Кроме того, мы пока не задали пустое содержимое для тайлов. Сохранение правильности и целостности поля является задачей GameBoard , поэтому давайте передадим ему и обязанность задания содержимого тайла. Чтобы реализовать это, передадим ему ссылку на фабрику содержимого через его метод Intialize , и используем её, чтобы дать всем тайлам экземпляр пустого содержимого.

Теперь Game должен передать свою фабрику полю.

Так как теперь у нас есть несколько конечных точек, изменим GameBoard.FindPaths так, чтобы он вызывал BecomeDestination для каждой и добавлял их все в границу. И это всё, что нужно для поддержки нескольких конечных точек. Все остальные тайлы как обычно очищаются. Затем мы удаляем жёстко заданную конечную точку в центре.

Но если мы можем превращать тайлы в конечные точки, то у нас должна быть возможность выполнять и обратную операцию, превращать конечные точки в пустые тайлы. Но тогда у нас может получиться поле совсем без конечных точек. В таком случае FindPaths не сможет выполнять свою задачу. Это происходит, когда граница пуста после инициализации путей для всех ячеек. Обозначим это как неверное состояние поля, возвращая false и завершая выполнение; в противном случае возвращаем в конце true .

Проще всего реализовать поддержку удаления конечных точек, сделав её операцией переключения. Нажав на пустые тайлы, мы будем превращать их в конечные точки, а нажимая на конечные точки, мы будем их удалять. Но теперь изменением содержимого занимается GameBoard , поэтому дадим ему общий метод ToggleDestination , параметром которого является тайл. Если тайл является конечной точкой, то делаем её пустой и вызываем FindPaths . В противном случае делаем его конечной точкой и тоже вызываем FindPaths .

Добавление конечной точки никогда не может создать неверное состояние поля, а удаление конечной точки — может. Поэтому будем проверять, удалось ли успешно выполнить FindPaths после того, как мы сделали тайл пустым. Если нет, то отменяем изменение, снова превратив тайл в конечную точку, и снова вызываем FindPaths , чтобы возвратиться к предыдущему верному состоянию.

Теперь в конце Initialize мы можем вызвать ToggleDestination с центральным тайлом в качестве аргумента, вместо того, чтобы явно вызывать FindPaths . Это единственный раз, когда мы начинаем с неверного состояния поля, но закончим мы гарантированно с правильным состоянием.

Наконец, заставим Game вызывать ToggleDestination вместо того, чтобы задавать само содержимое тайла.

Несколько конечных точек с правильными путями.

Стены

Цель игры tower defense — не позволить врагам достичь конечной точки. Эта цель достигается двумя способами. Во-первых, мы их убиваем, во-вторых, замедляем их, чтобы было больше времени на их убийство. На тайловом поле время можно растянуть, увеличив расстояние, которое нужно пройти врагам. Это можно реализовать размещением на поле препятствий. Обычно это башни, которые ещё и убивают врагов, но в этом туториале мы ограничимся только стенами.

Содержимое

Стены — это ещё один тип содержимого, поэтому добавим в GameTileContentType элемент для них.

Затем создадим префаб стены. На этот раз создадим game object содержимого тайла и добавим ему дочерний куб, который будет находиться поверх поля и заполнять тайл целиком. Сделаем его высотой в половину единицы и сохраним коллайдер, потому что стены могут визуально перекрывать часть тайлов за ним. Поэтому когда игрок касается стены, он будет влиять на соответствующий тайл.

root

cube

prefab

Префаб стены.

Добавим префаб стены в фабрику, и в коде, и в инспекторе.

Фабрика с префабом стены.

Включение и отключение стен

Добавим в GameBoard метод включения-отключения стен, как мы сделали это для конечной точки. Изначально проверять неверное состояние поля мы не будем.

Мы обеспечим поддержку переключения только между пустыми тайлами и тайлами стен, не позволяя стенам напрямую заменять конечные точки. Поэтому создавать стену мы будем только тогда, когда тайл пуст. Кроме того, стены должны блокировать поиск пути. Но каждый тайл должен иметь путь к конечной точке, в противном случае враги застрянут. Для этого нам снова нужно использовать проверку FindPaths , и отменять изменения, если они создали неверное состояние поля.

Включение-отключение стен будет использоваться гораздо чаще, чем включение-отключение конечных точек, поэтому сделаем так, чтобы переключение стен в Game выполнялось основным касанием. Конечные точки можно переключать дополнительным касанием (обычно это правая клавиша мыши), которое можно распознать, передав в Input.GetMouseButtonDown значение 1.

Теперь у нас есть стены.

Давайте также сделаем так, чтобы конечные точки не могли напрямую заменять стены.

Блокировка поиска пути

Чтобы стены блокировали поиск пути, нам достаточно не добавлять тайлы со стенами в границу поиска. Это можно сделать, заставив GameTile.GrowPathTo не возвращать тайлы со стенами. Но путь всё равно должен вырастать по направлению стены, чтобы все тайлы на поле имели путь. Это необходимо, потому что существует возможность того, что тайл с врагами внезапно превратится в стену.

Чтобы гарантировать, что у всех тайлов есть путь, GameBoard.FindPaths должен проверять это после завершения поиска. Если это не так, то состояние поля является неверным и нужно вернуть false . Обновлять визуализацию пути для неверных состояний не нужно, потому что поле вернётся к предыдущему состоянию.

Стены влияют на пути.

Чтобы убедиться, что у стен и в самом деле есть правильные пути, нужно сделать кубы полупрозрачными.

Прозрачные стены.

Учтите, что требование правильности всех путей не позволяет оградить стенами часть поля, в котором нет конечной точки. Мы можем разделить карту, но только если в каждой части есть хотя бы одна конечная точка. Кроме того, каждая стена должна быть соседней с пустым тайлом или конечной точкой, в противном случае она не сможет сама иметь путь. Например, невозможно сделать сплошной блок из стен размером 3×3.

Скрываем пути

Визуализация путей позволяет нам увидеть, как работает поиск пути и убедиться, что он и в самом деле верен. Но её не нужно показывать игроку, или по крайней мере необязательно. Поэтому давайте обеспечим возможность отключения стрелок. Это можно сделать, добавив в GameTile общий метод HidePath , который просто отключает его стрелку.

Состояние отображения путей — это часть состояния поля. Добавим к GameBoard булево поле, по умолчанию равное false , чтобы отслеживать его состояние, а также общее свойство в качестве геттера и сеттера. Сеттер должен показывать или скрывать пути на всех тайлах.

Теперь метод FindPaths должен показывать обновлённые пути, только если включена визуализация.

По умолчанию визуализация путей отключена. Отключим стрелку в префабе тайла.

Стрелка префаба по умолчанию неактивна.

Сделаем так, чтобы Game переключал состояние визуализации при нажатии клавиши. Логично было бы использовать клавишу P, но она также является горячей клавишей включения-отключения режима игры в редакторе Unity. В результате визуализация будет переключаться, когда использована горячая клавиша выхода из режима игры, что выглядит не очень красиво. Поэтому давайте используем клавишу V (сокращение от visualization).

Без стрелок.

Отображение сетки

Когда стрелки скрыты, становится трудно разглядеть расположение каждого тайла. Давайте добавим линии сетки. Скачайте отсюда текстуру сетки с квадратной границей, которую можно использовать как контур отдельного тайла.

Текстура сетки.

Мы не будем добавлять эту текстуру по отдельности к каждому тайлу, а применим её к земле. Но сделаем эту сетку необязательной, как и визуализацию путей. Поэтому добавим в GameBoard поле конфигурации Texture2D и выберем для него текстуру сетки.

Поле с текстурой сетки.

Добавим ещё одно булево поле и свойство для управления состоянием визуализации сетки. В данном случае сеттер должен изменять материал земли, что можно реализовать вызовом GetComponent<MeshRenderer> для земли и получив доступ к свойству material результата. Если сетку нужно отображать, то назначим свойству mainTexture материала текстуру сетки. В противном случае назначим ему null . Учтите, что при изменении текстуры материала будут создаваться дубликаты экземпляра материала, поэтому он становится независимым от ассета материала.

Сделаем так, чтобы Game переключал визуализацию сетки клавишей G.

Кроме того, добавим визуализацию сетки по умолчанию в Awake .

Неотмасштабированная сетка.

Пока мы получили границу вокруг всего поля. Она соответствует текстуре, но это не то, что нам нужно. Нам нужно отмасштабировать основную текстуру материала, чтобы она соответствовала размеру сетки. Можно это сделать, вызвав метод SetTextureScale материала с именем свойства текстуры (_MainTex) и двухмерным размером. Мы можем использовать непосредственно размер поля, который косвенно преобразуется в значение Vector2 .

without

with

Отмасштабированная сетка с отключенной и включённой визуализацией путей.

Итак, на данном этапе мы получили функционирующее поле для тайловой игры жанра tower defense. В следующем туториале мы добавим врагов.

Reset() и OnValidate() — полезные методы. Рассказываю на примерах в Unity3d

Спасибо за видео. Хотелось бы ещё услышать про паттерны. К примеру абстрактная фабрика, декоратор, стратегия или фабричный метод. Что-то из таких стандартных.

Спасибо за очередное классное видео! метод Reset() был для меня просто потрясением, как же сильно это упрощает жизнь))
Хотелось бы еще спросить, почему так часто используешь ключевое слово «this»? Это действительно помогает в конкретных ситуациях, или просто привычка?
Мне правда очень интересно))��

@Лавка Разработчика Понял, спасибо за ответ)

Это я был в поисках своего личного код стайла, и this не прижилась, как-то так)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *