Я давно вынашиваю планы сделать парочку игр, где будет примерно одно и то же: персонаж бегает по местности.
Предыдущие части: Начало, Объекты против кода
Так как с 3D работать я практически не умею, а готовыми движками до сих пор не овладевал, то склонялся к 2D-изометрии. Но опыт с Юнити воодушевляет меня попробовать 3D по-настоящему.
В Юнити есть готовый инструмент для генерации ландшафтов, да хоть и с деревьями сразу. Кроме того, в ютубе есть куча роликов от игроделов, которые также решают этот вопрос. То есть проблемы никакой нет.
Я, однако, хочу пройти этот путь сам. Плюс мне нужен не просто какой-то ландшафт, а бесконечный. То есть пока персонаж бежит вперёд, перед ним будут появляться всё новые и новые куски территории.
Очевидно, что нельзя заранее создать бесконечный объём данных. Поэтому типичное решение выглядит так:
- Сделать коллекцию непосредственно доступных кусков карты, окружающих игрока (допустим, это будет матрица 3*3 куска, игрок в центре)
- По мере движения игрока старые куски удалять, а новые добавлять.
Пока ставлю задачу-минимум: сгенерировать один квадратный кусок карты программным способом.
Я создам новый проект, в котором по умолчанию будут камера и свет. Их я программно создавать не буду, так как нет смысла.
Далее я как обычно создам пустой GameObject и прикреплю к нему компонент Script, который назову Map (карта).
Затем я буду редактировать код этого скрипта. Всё как и в прошлый раз: скрипт создаст новый GameObject, а к нему добавит компоненты MeshFilter и MeshRenderer и настроит материал.
Почему скрипт будет создавать новый GameObject, а не добавлять компоненты к уже имеющемуся? Потому что нужно будет генерировать много кусков карты, и каждый из них будет новым GameObject.
На этот раз посмотрим подробно на массивы vertices, triangles и normals класса Mesh. Именно из них получается геометрия.
Эта организация данных не является чем-то особенным для Unity, она общая практически везде.
Геометрия строится как набор вершин. Каждая вершина это точка в пространстве с координатами (x, y, z). Надуйте воздушный шарик и нарисуйте на нём много точек, чтобы они равномерно покрывали всю его поверхность. Это и есть вершины, которые описывают шарик.
В коде каждая вершина задаётся с помощью класса Vector3. Это структура, состоящая из трёх чисел (x, y, z). Для неё заданы перегрузки математических операций, т.е. мы можем сложить или умножить два вектора, как если бы они были обычными числами.
Координаты в Unity представлены так: если мы стоим на плоскости в самом центре (0, 0, 0) и смотрим вперёд (в экран), то слева направо идёт ось x, снизу вверх ось y, а сзади вперёд ось z.
Так как ландшафт это то, что у нас под ногами, он лежит в плоскости xz, а его возвышения или впадины это координата y.
Чтобы восстановить форму объекта, одних вершин недостаточно. Нужно вершины соединить друг с другом в правильном порядке, чтобы получились плоскости (грани), которые приближённо соответствуют внешней оболочке объекта. Наиболее простым способом соединения вершин считается треугольник. Это минимальная геометрическая фигура, которая задаёт грань.
Таким образом, для имеющегося набора вершин (vertices) мы задаём набор граней-треугольников (triangles). Каждому треугольнику нужно три вершины. Так как набор вершин уже есть, то мы задаём не координаты Vector3, а просто индексы вершин из массива vertices. При этом одна и та же вершина может принадлежать нескольким треугольникам.
Это в принципе уже достаточно, геометрия восстановлена.
Читайте также:
Но остаются ещё нормали (массив normals).
Нормаль это воображаемый вектор, проведённый перпедикулярно плоскости. Например, у буквы "Т" её "ножка" это нормаль, проведённая вниз от "перекладины".
Хотя сами по себе нормали для геометрии не нужны, они играют важную роль. Например, с их помощью определяется видимость граней объекта: если нормаль от грани смотрит в нашу сторону, то эта грань видима, а если в обратную сторону, то мы смотрим на грань с "изнанки" и видеть её, как правило, не должны.
В Unity, однако, лицевая и изнаночная сторона плоскости определяются по-другому. Если вершины, из которых состоит треугольник, следуют друг за другом по часовой стрелке, то это его лицевая сторона, а если против, то изнаночная.
Для чего тогда нужны нормали? Для освещения. Фактически, это та же видимость, только не для нас, а для света. Если нормаль направлена в сторону источника света, то грань освещена тем ярче, чем меньше угол между нормалью и светом. Если же нормаль направлена в противоположную сторону, то грань совсем не освещена.
Нормаль освещения в нашем случае рассчитывается не для грани, а для вершины. Фактически, освещаются только вершины, а грань закрашивается усреднённо исходя из освещённости трёх её вершин.
Если все грани соединены в одно плоское полотно, то нормали вершин совпадают с нормалями граней:
Если же вершина находится на "сгибе" между двумя гранями, то у неё должна быть своя нормаль. Её мы можем в принципе задать как угодно, но логично сделать её средним от всех смежных граней:
Итак, я сделаю следующее:
- кусок карты будет размером 5 * 5 юнитов (в пространстве Юнити это ~5 метров)
- Кусок будет поделён на 10 частей по горизонтали и вертикали. Это значит, что нужно 11 * 11 вершин (включая краевые).
Треугольников будет 10*10*2. То есть если вершин N * N, то треугольников (N - 1) * (N - 1) * 2.
Каждый треугольник это три индекса вершин, поэтому общая длина массива с треугольниками это 10 * 10 * 2 * 3.
Я задам в классе Map кое-какие константы и объявлю массивы:
Далее в методе Start() подготовлю решётку из 11 * 11 вершин, отстоящих друг от друга на вычисленную величину spanSize:
Вершины ставятся так, чтобы центр плоскости был в центре координат. Иначе говоря, плоскость занимает координаты от -2.5 до 2.5 по осям x и z. Координата z увеличивается в сторону "от нас", поэтому массив заполняется так, что самые первые вершины в нём самые дальние.
Так как количество нормалей равно количеству вершин, то в этом же цикле заполняются нормали. Пока что они все направлены вертикально вверх. Это можно сделать с помощью Vector3(0, 1, 0), или короткой формы Vector3.up.
Далее я заполняю массив треугольников. Иду по массиву вершин и для каждой вершины беру ещё две соседних: справа и снизу. Это первый треугольник. Второй треугольник: вершины снизу, справа и справа-снизу. Обратите внимание, что в массив треугольников пишутся не сами вершины, а их смещения в массиве _vertices.
Устройство массива _triangles таково, что нам не нужно там разделять, где какой треугольник. Движок делает это сам – просто берёт первые три элемента, затем следующие три, и т.д.
Закончим подготовку, как делали ранее:
Чтобы увидеть эту плоскость в игре, я в редакторе подвину и наклоню камеру соответствующим образом:
И можно запускать проигрыватель:
Эта плоскость состоит из 200 треугольников. Но выглядит просто как плоскость. Чтобы проверить, как оно на самом деле, обратимся к режиму отображения Scenе и выберем объект, который был создан:
Теперь видно, что это не просто прямоугольник, а действительно сетка из вершин и треугольников.
Далее, чтобы получить некое подобие рельефа, эту сетку нужно "помять", то есть какие-то её части понизить, а какие-то повысить.
Этим займёмся в следующем выпуске.
Следующая часть: