GridMap: модульный 3D-уровень и коллизии
Создать MeshLibrary, разместить и повернуть блоки, согласовать меши с физикой и безопасно сохранить библиотеку.
Модель данных
GridMap хранит по одному ID элемента MeshLibrary и индексу ориентации в каждой целочисленной клетке. MeshLibrary описывает блоки: меш, его трансформацию и отдельные физические формы. Это не иерархия из отдельной Entity на каждый блок.
Подходит для стен, пола и повторяющихся модулей фиксированного шага. Двери со скриптами, инвентарь и другие интерактивные объекты удобнее размещать отдельными Entity/префабами, если им нужно собственное состояние. Сама сетка не создаёт ECS, стриминг мира или навигацию из видимой картинки.
Запустить готовую сетку
Возьмите BlockoutLab3D.cs из примера CSG и BlockoutLibrary.cs ниже. Нужны C# SDK 1.0.0, net10.0 и Entity3D-корень уровня .rscene. Выполните dotnet build, назначьте BlockoutLab3D.cs корню и запустите.
Код создаёт 15 клеток пола, а затем заменяет четыре из них синими стенами. В каждой клетке остаётся один элемент: запись стены не добавляет второй слой пола в ту же клетку. Общая тёмная плоскость под примером — только визуальный фон, без физической формы.
BlockoutLibrary.cs
using Reckfell;
public static class BlockoutLibrary
{
public const int Floor = 0;
public const int Wall = 1;
public static StandardMaterial3D Material(Color color) => new()
{
AlbedoColor = color,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded
};
public static MeshLibrary Create()
{
var library = new MeshLibrary();
AddBox(library, Floor, "Floor", new Vector3(2, 0.2f, 2),
new Vector3(0, -0.1f, 0), new Color(0.15f, 0.18f, 0.23f));
AddBox(library, Wall, "Wall", new Vector3(2, 3, 0.4f),
new Vector3(0, 1.5f, 0), new Color(0.08f, 0.25f, 1));
return library;
}
private static void AddBox(MeshLibrary library, int id, string name,
Vector3 size, Vector3 offset, Color color)
{
library.CreateItem(id);
library.SetItemName(id, name);
library.SetItemMesh(id, new BoxMesh { Size = size, Material = Material(color) });
var transform = new Transform3D(Basis.Identity, offset);
library.SetItemMeshTransform(id, transform);
library.SetItemShapes(id, new Reckfell.Collections.Array
{
new BoxShape3D { Size = size }, transform
});
}
}Кадр снят в текущем локальном runtime 1.0.0. Управление персонажем и рисование клеток мышью в редакторе в пример не входят.
Согласовать меш и коллизию
CreateItem создаёт пустую запись. SetItemMesh назначает изображаемую геометрию; SetItemShapes — отдельный массив пар [Shape3D, Transform3D, …]. Число элементов массива должно быть чётным. SetItemMeshTransform не заменяет трансформации физических форм.
В примере стена имеет размер (2,3,0.4), а её центр поднят на 1.5, чтобы нижний край лежал на Y=0. Одинаковое смещение задано мешу и форме отдельно. Пол толщиной 0.2 смещён на -0.1, поэтому верхняя поверхность находится на Y=0.
Если картинка совпадает с сеткой, а тело сталкивается «рядом», сначала сравните эти две трансформации. После изменения клеток дождитесь физического обновления: GetCellItem уже может вернуть новое значение, когда прежняя коллизия ещё не перестроена.
Различать размер и центрирование
| Настройка | В примере | Что меняет |
|---|---|---|
| CellSize | (2,2,2) | Шаг между позициями клеток |
| CellCenterX/Z | true | Смещение на половину шага по X/Z |
| CellCenterY | false | Нет половинного смещения по высоте |
| CellScale | 1 | Масштаб размещённых мешей и физических форм |
| Размер BoxMesh | Собственный для каждого блока | Геометрию ресурса; не меняется от CellSize |
При этих настройках MapToLocal((2,3,-1)) даёт (5,6,-1). LocalToMap((-0.01,-0.01,-0.01)) даёт (-1,-1,-1), потому что выполняется округление вниз, а не отбрасывание дробной части.
Мировую точку сначала переводите Grid.ToLocal, затем передавайте LocalToMap. Обратный путь: MapToLocal, затем ToGlobal. Для попадания луча точно на границу клетки определите нужную сторону по normal и небольшому смещению; слепое округление мировой позиции часто выбирает соседнюю клетку. Эта схема преобразования проверена, готовый редактор рисования по клику здесь не реализован.
CellSize=(4,2,2) увеличит шаг по X, но не растянет исходную стену. CellScale=0.5 уменьшит меш и физическую форму, оставив прежний шаг и MapToLocal. Не пытайтесь исправить неверный масштаб модели одной настройкой сетки.
Повернуть или удалить блок
Третий аргумент SetCellItem — индекс одной из 24 ортогональных ориентаций, а не градусы. Для поворота на 90° получите Basis вокруг Vector3.Up с углом Mathf.Pi/2 и передайте его GetOrthogonalIndexFromBasis. Полученный индекс используйте в SetCellItem; для чтения есть GetCellItemOrientation и GetCellItemBasis. Проверка подтвердила поворот и меша, и физической формы.
SetCellItem(cell,-1) удаляет клетку. Отрицательный item здесь не «блок без картинки». В нативной проверке GetCellItem сразу показал -1, а после обновления физики луч перестал попадать в удалённую стену.
Попадание в физику GridMap указывает на сам GridMap, не на отдельную Entity клетки. Если нужно состояние конкретной двери или разрушаемого блока, храните его по координате клетки либо используйте отдельный префаб.
Проверять ID и сохранение
Используйте последовательные ID от 0. В текущей сборке подтверждён дефект .rdata: библиотека с элементами 0 и 7 работает в памяти, ResourceSaver возвращает Error.Ok, но данные элемента 7 после загрузки теряются. В опубликованном примере используются 0 и 1; меши, формы и смещения проходят сохранение и загрузку без кэша.
До исправления не полагайтесь на сохранение разреженных библиотек и не удаляйте исходные ассеты после одной успешной записи. При миграции ID обновляйте и ссылки клеток карты, иначе они начнут указывать на другой блок. Проверка полной .rscene с GridMap и поведение меню экспорта MeshLibrary не входят в этот тест.
Есть ещё два ограничения:
- Неизвестный ID может записаться в клетку и вернуться через GetCellItem, но не создать меш или физику. Проверяйте наличие через GetItemList библиотеки; в C# нет метода MeshLibrary.HasItem.
- Внутреннее поле элемента клетки — 16-битное. Допустимые неотрицательные ID для GridMap — 0..65535; значение 65536 в проверке стало 0. По исходникам абсолютные координаты клеток должны быть меньше 2^20 по каждой оси. Это не бесконечная карта.
Диагностика
| Симптом | Что проверить |
|---|---|
| Клетка записана, блока нет | MeshLibrary назначена, ID существует, у элемента есть Mesh |
| Блок виден, коллизии нет | Пары SetItemShapes и физические слои/маски |
| Стены парят или утоплены | CellCenterY, смещение меша и отдельное смещение формы |
| Поворот не соответствует 90° | Передан индекс ортогональной ориентации, а не число 90 |
| После загрузки часть блоков пропала | Разрывы ID и фактические данные загруженной библиотеки |
| AI не находит путь по полу | Навигационные данные и их регистрация настраиваются отдельно |