TileMapLayer: уровень, координаты и правки клеток
Запустить карту с персонажем, менять стены и понимать границы бинарного сохранения.
Запустить уровень
Нужен C#-проект Reckfell.NET.Sdk 1.0.0, net10.0 с рендерером Compatibility. Сохраните три файла ниже, выполните dotnet build, создайте уровень с корнем Entity2D и прикрепите TileLevel.cs. Сохраните tiles.rscene и запустите этот уровень.
Стрелки перемещают белого персонажа. Левая кнопка мыши переключает внутреннюю клетку между полом и стеной; граница уровня защищена от этого переключения. В стандартных назначениях используются ui_left/ui_right/ui_up/ui_down. Дайте окну игры фокус.
Скрипт создаёт карту 20×15, TileSet и персонажа. Каждая клетка занимает 16×16 единиц; логический размер окна во время запуска — 320×240. Не добавляйте второй комплект карты и тел вручную. Это демонстрация правок карты, а не готовый игровой редактор: не вставляйте коллизию внутрь персонажа без отдельного разрешения пересечения.
ExampleTileSet.cs
using Reckfell;
public static class ExampleTileSet
{
public const int SourceId = 0;
public static readonly Vector2I Floor = new(0, 0);
public static readonly Vector2I Wall = new(1, 0);
public static TileSet Create()
{
using var image = Image.CreateEmpty(32, 16, false, Image.Format.Rgba8);
image.Fill(new Color(0.12f, 0.14f, 0.18f));
image.FillRect(new Rect2I(1, 1, 14, 14), new Color(0.17f, 0.19f, 0.23f));
image.FillRect(new Rect2I(16, 0, 16, 16), new Color(0.2f, 0.38f, 0.9f));
image.FillRect(new Rect2I(17, 1, 14, 3), new Color(0.4f, 0.57f, 1));
var atlas = new TileSetAtlasSource
{
Texture = ImageTexture.CreateFromImage(image),
TextureRegionSize = new Vector2I(16, 16)
};
var tiles = new TileSet { TileSize = new Vector2I(16, 16) };
tiles.AddPhysicsLayer();
tiles.SetPhysicsLayerCollisionLayer(0, 1);
tiles.SetPhysicsLayerCollisionMask(0, 1);
tiles.AddCustomDataLayer();
tiles.SetCustomDataLayerName(0, "walkable");
tiles.SetCustomDataLayerType(0, Variant.Type.Bool);
tiles.AddSource(atlas, SourceId);
atlas.CreateTile(Floor);
atlas.CreateTile(Wall);
atlas.GetTileData(Floor, 0).SetCustomData("walkable", true);
var wall = atlas.GetTileData(Wall, 0);
wall.SetCustomData("walkable", false);
wall.SetCollisionPolygonsCount(0, 1);
wall.SetCollisionPolygonPoints(0, 0, new[]
{
new Vector2(-8, -8), new Vector2(8, -8),
new Vector2(8, 8), new Vector2(-8, 8)
});
return tiles;
}
}TileWalker.cs
using Reckfell;
public partial class TileWalker : CharacterBody2D
{
public override void _Ready()
{
MotionMode = MotionModeEnum.Floating;
AddChild(new CollisionShape2D { Shape = new CircleShape2D { Radius = 6 } });
}
public override void _PhysicsProcess(double delta)
{
Velocity = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down") * 120;
MoveAndSlide();
}
public override void _Draw() => DrawCircle(Vector2.Zero, 6, Colors.White);
}TileLevel.cs
using Reckfell;
public partial class TileLevel : Entity2D
{
public TileMapLayer Map { get; private set; } = null!;
public TileWalker Player { get; private set; } = null!;
public override void _Ready()
{
if (GetViewport() is Window window)
{
window.ContentScaleSize = new Vector2I(320, 240);
window.ContentScaleMode = Window.ContentScaleModeEnum.Viewport;
}
Map = new TileMapLayer { TileSet = ExampleTileSet.Create(), TextureFilter = TextureFilterEnum.Nearest };
AddChild(Map);
for (int y = 0; y < 15; y++)
for (int x = 0; x < 20; x++)
{
bool wall = x == 0 || x == 19 || y == 0 || y == 14 || (x == 12 && y >= 3 && y <= 11);
Map.SetCell(new Vector2I(x, y), ExampleTileSet.SourceId, wall ? ExampleTileSet.Wall : ExampleTileSet.Floor);
}
Map.UpdateInternals();
Player = new TileWalker { Position = Map.MapToLocal(new Vector2I(5, 7)) };
AddChild(Player);
}
public bool ToggleCell(Vector2I cell)
{
if (cell.X <= 0 || cell.X >= 19 || cell.Y <= 0 || cell.Y >= 14) return false;
Vector2I next = Map.GetCellAtlasCoords(cell) == ExampleTileSet.Wall ? ExampleTileSet.Floor : ExampleTileSet.Wall;
Map.SetCell(cell, ExampleTileSet.SourceId, next);
return true;
}
public override void _UnhandledInput(InputEvent input)
{
if (input is InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left })
ToggleCell(Map.LocalToMap(Map.GetLocalMousePosition()));
}
}Выбрать правильные координаты
| Значение | Пример | Смысл |
|---|---|---|
| Координата клетки | (2,3) | Целочисленная позиция в сетке |
| MapToLocal((2,3)) | (40,56) | Центр этой клетки в локальных координатах слоя |
| ToGlobal(MapToLocal(cell)) | Зависит от Transform слоя | Позиция центра в canvas |
| LocalToMap(localPoint) | Например (−1,−1) для точки (−1,−1) | Преобразование локальной точки в клетку |
Для canvas-позиции используйте LocalToMap(ToLocal(position)). Не передавайте InputEvent.Position напрямую: это координаты Viewport, на которые влияют камера и растяжение. В примере GetLocalMousePosition сразу получает подходящую локальную позицию карты.
Не заменяйте LocalToMap делением с приведением к int. Для отрицательной координаты такое приведение округляет не туда; изометрическая или шестигранная раскладка также требует правил TileSet. MapToLocal возвращает центр клетки, а не обязательно центр визуального арта с TextureOrigin.
Поставить, заменить или удалить тайл
SetCell получает координату клетки, sourceId, atlasCoords и alternativeTile. Для этого атласа передавайте sourceId=0, Floor=(0,0) или Wall=(1,0), alternativeTile=0. Координата карты не является координатой атласа.
Важный случай: SetCell(cell, 0) не означает «выбрать первый тайл». Неуказанный atlasCoords соответствует недопустимым (−1,−1), поэтому клетка очищается. Для удаления можно явно использовать EraseCell. Идентификаторы должны соответствовать назначенному TileSet.
Изменение логических данных видно сразу через GetCellSourceId/GetCellAtlasCoords. Рендеринг, физика и прочие внутренние пакеты обновляются отложенно. При необходимости вызовите UpdateInternals один раз после группы изменений; не вызывайте его внутри цикла для каждой клетки. Он не выполняет дополнительный шаг физической симуляции.
На настоящих кадрах ниже персонаж сначала остановлен синей стеной, затем проходит через клетку, заменённую полом. Клетки и их коллизии не становятся отдельными Entity в дереве.
Стена закрыта
Стена заменена полом
Читать данные карты
GetUsedCells возвращает занятые клетки. GetUsedRect охватывает их в координатах сетки: у нашего уровня позиция (0,0), размер (20,15), а не (320,240). Пустые клетки не входят в этот прямоугольник.
GetCellTileData работает с atlas-тайлами и может вернуть null для пустой клетки или другого типа источника. Данные принадлежат тайлу TileSet, а не уникальному экземпляру клетки. Для дверей, разрушений и другого состояния уровня используйте собственную структуру по координатам либо осознанно меняйте ссылку клетки на другой тайл.
Сохранение и большие координаты
TileMapData содержит бинарные ссылки клеток, но не сам TileSet и не текстуры. При восстановлении требуется тот же согласованный набор идентификаторов ресурса.
В проверенном формате координаты записываются как знаковые 16-битные числа: допустимый диапазон сохранения по каждой оси — от −32768 до 32767. Клетка (40000,0) после сохранения и загрузки стала (−25536,0). Vector2I в API не означает, что бинарный формат сохраняет весь его диапазон.
Не используйте этот массив как бесконтрольный межверсионный формат сейва. Для длительного хранения задайте собственную версию схемы и стабильные игровые идентификаторы. Большие миры требуют проектирования регионов с небольшими локальными координатами; автоматической подгрузки регионов этот пример не реализует.
Если карта ведёт себя неверно
- Картинка есть, столкновения нет: проверьте physics layer TileSet, полигон тайла и маски тела.
- Вместо тайла клетка исчезла: передайте atlasCoords явно и проверьте существование source/alternative.
- Щелчок меняет соседнюю клетку: проверьте пространство координат и трансформацию слоя.
- Сохранённая клетка сместилась далеко: проверьте диапазон координат бинарного формата.
- При правке кадр тормозит: сгруппируйте SetCell; не перестраивайте всё после каждой клетки.