Я начал разработку Minecraft на движке Unity. Пока не решил, какие механики я повторю из оригинала, а какие добавлю от себя. Но сегодня я вам расскажу о моей логике построения чанка.
Далее предоставлю полный скрипт для построения чанка:
using Cysharp.Threading.Tasks; using System.Collections.Generic; using UnityEngine; namespace MyGame.Gameplay.Terrain { public sealed class TerrainChunkBuilder { private Mesh _mesh = new(); private int[,,] _blockIds; private readonly List<Vector3> _vertices = new(); private readonly List<int> _triangles = new(); private readonly List<Vector2> _uvs = new(); private readonly List<Vector3> _normals = new(); public async UniTask<Mesh> Build(int[,,] blockIds, bool isAsync) { _blockIds = blockIds; _mesh.Clear(); _vertices.Clear(); _triangles.Clear(); _uvs.Clear(); _normals.Clear(); for (int z = 1; z < Settings.ChunkSize.z + 1; z++) { if (isAsync) await UniTask.DelayFrame(1); for (int y = 0; y < Settings.ChunkSize.y; y++) { for (int x = 1; x < Settings.ChunkSize.x + 1; x++) { if (_blockIds[x, y, z] != 0) { TryCreateLeft(x, y, z); TryCreateRight(x, y, z); TryCreateBack(x, y, z); TryCreateForward(x, y, z); TryCreateDown(x, y, z); TryCreateUp(x, y, z); } } } } _mesh.vertices = _vertices.ToArray(); _mesh.triangles = _triangles.ToArray(); _mesh.uv = _uvs.ToArray(); _mesh.normals = _normals.ToArray(); return _mesh; } private void TryCreateLeft(int x, int y, int z) { if(_blockIds[x - 1, y, z] == 0) { _vertices.AddRange(new Vector3[] { new Vector3(x - 1, y, z), new Vector3(x - 1, y + 1, z), new Vector3(x - 1, y + 1, z - 1), new Vector3(x - 1, y, z - 1) }); CreateTriangles(); CreateUvs(); CreateNormals(Vector3.left); } } private void TryCreateRight(int x, int y, int z) { if (_blockIds[x + 1, y, z] == 0) { _vertices.AddRange(new Vector3[] { new Vector3(x, y, z - 1), new Vector3(x, y + 1, z - 1), new Vector3(x, y + 1, z), new Vector3(x, y, z) }); CreateTriangles(); CreateUvs(); CreateNormals(Vector3.right); } } private void TryCreateBack(int x, int y, int z) { if (_blockIds[x, y, z - 1] == 0) { _vertices.AddRange(new Vector3[] { new Vector3(x - 1, y, z - 1), new Vector3(x - 1, y + 1, z - 1), new Vector3(x, y + 1, z - 1), new Vector3(x, y, z - 1) }); CreateTriangles(); CreateUvs(); CreateNormals(Vector3.back); } } private void TryCreateForward(int x, int y, int z) { if (_blockIds[x, y, z + 1] == 0) { _vertices.AddRange(new Vector3[] { new Vector3(x, y, z), new Vector3(x, y + 1, z), new Vector3(x - 1, y + 1, z), new Vector3(x - 1, y, z) }); CreateTriangles(); CreateUvs(); CreateNormals(Vector3.forward); } } private void TryCreateDown(int x, int y, int z) { if (y != 0 && _blockIds[x, y - 1, z] == 0) { _vertices.AddRange(new Vector3[] { new Vector3(x - 1, y, z), new Vector3(x - 1, y, z - 1), new Vector3(x, y, z - 1), new Vector3(x, y, z) }); CreateTriangles(); CreateUvs(); CreateNormals(Vector3.down); } } private void TryCreateUp(int x, int y, int z) { if (y == Settings.ChunkSize.y - 1 || _blockIds[x, y + 1, z] == 0) { _vertices.AddRange(new Vector3[] { new Vector3(x - 1, y + 1, z - 1), new Vector3(x - 1, y + 1, z), new Vector3(x, y + 1, z), new Vector3(x, y + 1, z - 1) }); CreateTriangles(); CreateUvs(); CreateNormals(Vector3.up); } } private void CreateTriangles() { _triangles.AddRange(new int[] { _vertices.Count - 4, _vertices.Count - 3, _vertices.Count - 2, _vertices.Count - 4, _vertices.Count - 2, _vertices.Count - 1 }); } private void CreateUvs() { _uvs.AddRange(new Vector2[] { new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0) }); } private void CreateNormals(Vector3 normal) { for (int i = 0; i < 4; i++) _normals.Add(normal); } } }
Остановимся подробнее на каждом блоке кода:
public async UniTask<Mesh> Build(int[,,] blockIds, bool isAsync) { _blockIds = blockIds; _mesh.Clear(); _vertices.Clear(); _triangles.Clear(); _uvs.Clear(); _normals.Clear(); for (int z = 1; z < Settings.ChunkSize.z + 1; z++) { if (isAsync) await UniTask.DelayFrame(1); for (int y = 0; y < Settings.ChunkSize.y; y++) { for (int x = 1; x < Settings.ChunkSize.x + 1; x++) { if (_blockIds[x, y, z] != 0) { TryCreateLeft(x, y, z); TryCreateRight(x, y, z); TryCreateBack(x, y, z); TryCreateForward(x, y, z); TryCreateDown(x, y, z); TryCreateUp(x, y, z); } } } } _mesh.vertices = _vertices.ToArray(); _mesh.triangles = _triangles.ToArray(); _mesh.uv = _uvs.ToArray(); _mesh.normals = _normals.ToArray(); return _mesh; }
Метод (таск) возвращает меш. Как раз таки меш и отвечает за геометрию объекта.
1 строчка кода: Принимаемые параметры:
blockIds - представляет из себя трехмерный массив, содержащий id каждого блока. Id может быть либо 0, либо 1.
0 - отсутствие блока
1 - наличие блока. Позже вариантов id будет больше, но для простоты восприятия пока только 1.
isAsync - если true, то код будет выполняться асинхронно, не тормозя другие процессы.
false - код будет выполняться синхронно, останавливая другие процессы.
isAsync = false я использую на старте игры
isAsync = true я использую во время игры, когда нужно подгрузить чанки вблизи игрока.
4 - 8 строчки кода:
У Mesh и Листов вызываю метод Clear() перед использованием, так как класс привязан к конкретному чанку, а у чанка я могу удалить блок или установить в него новый блок.
_vertices - массив точек для построения полигонов и uv-координат и нормалей. Позже поймете, надеюсь).
_triangles - массив полигонов. Значения внутри массива - int. По сути это ссылки на _vertices. 3 последовательных значения - это 1 полигон. Наверное пока не совсем понятно, но скоро все поймете.
_uvs - массив координат на текстуре. _uvs.Count должен совпадать с _vertices.Count. Каждая точка в _vertices имеет координату на текстуре в _uvs.
_normals - нужны для правильной работы со светом. Так же _normals.Count должен совпадать с _vertices.Count.
9 - 29 строчки кода: Построение меша (заполнение листов: _vertices, _triangles, _uvs и _normals)
Идет перебор blockIds.
Settings.ChunkSize - это размер чанка. Количество блоков в каждой оси.
blockIds больше Settings.ChunkSize, так как для построения чанка, я перебираю каждый бок и у каждого блока проверяю id его соседей. На границах чанка не все соседи есть. Например у крайних левых блоков нету соседей слева, там уже другой чанк. И что бы не обращаться к соседнему чанку, blockIds увеличен на 2 по оси x и z. По оси y увеличивать не нужно, так как выше и ниже других чанков не будет.
blockIds = [Settings.ChunkSize.x + 2, Settings.ChunkSize.y, Settings.ChunkSize.z + 2]
По этому внутри циклов x и z сдвинуты на 1.
11 - 12 строчки. Идет проверка на асинхронность. Выделяется 1 кадр внутри z-цикла. можно перенести эти строчки в y-цикл или x-цикл, но тогда чанки будут появляться дольше, а fps игры будет выше, так как программа будет чаще останавливаться на 1 кадр.
18 - 26 строчки: идет проверка блока на 0, если не 0, то пробуем строить стенки блока-куба.
30 - 33 строчки:
Присваиваем мешу листы, преобразовывая их в массивы.
private void TryCreateRight(int x, int y, int z) { if (_blockIds[x + 1, y, z] == 0) { _vertices.AddRange(new Vector3[] { new Vector3(x, y, z - 1), new Vector3(x, y + 1, z - 1), new Vector3(x, y + 1, z), new Vector3(x, y, z) }); CreateTriangles(); CreateUvs(); CreateNormals(Vector3.right); } }
Пример построения правой стенки блока-куба.
В начале идет проверка id блока стоящего правее. Если id == 0, то значит там нету блока и стенку нужно рисовать. Если бы id соседнего блока было не 0, это бы значило, что там есть блок. И игрок не будет видеть правую стенку текущего блока, так как соседний блок будет ее перекрывать, а следовательно эту стенку рисовать не нужно, производительность)
Помним, что _vertices это массив точек с координатами. Это координаты относительно центра чанка. Я добавляю 4 точки, так как квадрат состоит из 4 точек, а квадрат является стороной блока-куба.


Для простоты представим, что в TryCreateRight() мы передаем x = 1, y = 0, z = 1. Помним, что x = 0 и z = 0 мы не передаем никогда, так как циклы начинаются с 1. Это будет самый первый блок. А строить мы начинаем с координат (0,0,0).
Значит точки должны иметь следующие координаты:
1. (1, 0, 0)
2. (1, 1, 0)
3. (1, 1, 1)
4. (1, 0, 1)
Подставим x, y и z в эти формулы 7 - 10 строчки кода:
new Vector3(x, y, z - 1),
new Vector3(x, y + 1, z - 1),
new Vector3(x, y + 1, z),
new Vector3(x, y, z)
и понимаем для чего нужно +1 и -1
Для других сторон будет так:





private void CreateTriangles() { _triangles.AddRange(new int[] { _vertices.Count - 4, _vertices.Count - 3, _vertices.Count - 2, _vertices.Count - 4, _vertices.Count - 2, _vertices.Count - 1 }); }

Следующим этапом строим треугольники, а именно 2 треугольника на квадрат. Нумерацию точек я начал с 0, так будет нагляднее. Треугольники рисуются по часовой стрелке.
1 треугольник - 0, 1, 2
2 треугольник - 0, 2, 3
Предыдущим этапом я добавлял _vertices, а именно добавил 4 точки. Представим что это первый квадрат который рисуется. _vertices.Count будет равен 4. И что бы добраться до id нужных точек, я делаю _vertices.Count - 4, _vertices.Count - 3 и т. д.
Так как я делаю все по по порядку добавляю в _vertices 4 точки, а потом сразу добавляю полигоны, я могу не беспокоиться о том, что значения _vertices.Count будут некорректные.
private void CreateUvs() { _uvs.AddRange(new Vector2[] { new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0) }); }
На этом этапе добавляется текстура к квадрату. (0, 0) - левый нижний угол текстуры. (1, 1) - правый верхний угол текстуры. Точки соответствуют порядку добавления точек в _vertices.
private void CreateNormals(Vector3 normal) { for (int i = 0; i < 4; i++) _normals.Add(normal); }
А тут добавляются нормали, я до конца не понимаю как они работают, но нормали должны быть перпендикулярно плоскости квадрата.
Багов с визуализацией чанков мною замечено не было, код работает корректно.
В следующих статьях я расскажу о других скриптах моей игры. Так же в моем телеграмм канале есть видео с геймплеем игры. Я периодически выкладываю туда прогресс разработки проекта.
Всем спасибо за внимание! Может кому-то поможет вся эта информация в создании своих игр.
