GridMap: a modular 3D level with collision
Create a MeshLibrary, place and rotate blocks, align mesh and physics, and persist the library safely.
Data model
GridMap stores one MeshLibrary item ID and one orientation index per integer cell. MeshLibrary defines each block: a mesh, its transform and separate physics shapes. It does not create an individual entity for every block.
Use it for walls, floors and repeated modules with fixed spacing. Scripted doors, inventory and other interactive objects often fit separate entities/prefabs when they need individual state. The grid alone does not create ECS, world streaming or navigation from visible artwork.
Run the complete grid
Take BlockoutLab3D.cs from the CSG example and BlockoutLibrary.cs below. Use C# SDK 1.0.0, net10.0 and an Entity3D-rooted .rscene level. Run dotnet build, attach BlockoutLab3D.cs to the root and run.
Code creates 15 floor cells, then replaces four with blue walls. Each cell still has one item: setting a wall does not add a second floor layer in that cell. The shared dark plane below is a visual backdrop without a physics shape.
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
});
}
}Captured in the current local 1.0.0 runtime. Character controls and editor mouse painting are not part of this example.
Align mesh and collision
CreateItem adds an empty entry. SetItemMesh assigns visible geometry; SetItemShapes supplies a separate array of [Shape3D, Transform3D, …] pairs. The array must have an even number of elements. SetItemMeshTransform does not replace physics-shape transforms.
The sample wall measures (2,3,0.4), with its center raised by 1.5 so its bottom lies on Y=0. The same offset is explicitly assigned to the mesh and shape separately. The 0.2-thick floor is offset by -0.1, putting its top at Y=0.
If artwork aligns but collision appears beside it, compare those transforms first. Wait for a physics update after cell edits: GetCellItem can already report the new value while old physics is still pending replacement.
Distinguish dimensions and centering
| Setting | Sample | Effect |
|---|---|---|
| CellSize | (2,2,2) | Spacing between cell positions |
| CellCenterX/Z | true | Half-step offset along X/Z |
| CellCenterY | false | No half-step vertical offset |
| CellScale | 1 | Scale of placed meshes and physics shapes |
| BoxMesh dimensions | Per block | Resource geometry, unchanged by CellSize |
These settings make MapToLocal((2,3,-1)) return (5,6,-1). LocalToMap((-0.01,-0.01,-0.01)) returns (-1,-1,-1), because it floors rather than truncates toward zero.
Convert world input through Grid.ToLocal before LocalToMap. Reverse with MapToLocal followed by ToGlobal. A ray hitting exactly on a cell boundary requires choosing the intended side using its normal and a small offset; blindly rounding a world position often chooses a neighboring cell. Coordinate conversion is verified here; a complete click-painting tool is not implemented.
CellSize=(4,2,2) increases X spacing without stretching the source wall. CellScale=0.5 shrinks both mesh and physics shape, preserving original spacing and MapToLocal. A grid setting alone cannot correct every model-scale error.
Rotate or erase a block
The third SetCellItem argument is an index among 24 orthogonal orientations, not degrees. For a 90° turn, construct a Basis around Vector3.Up with angle Mathf.Pi/2 and pass it to GetOrthogonalIndexFromBasis. Use the returned index in SetCellItem; read it through GetCellItemOrientation or GetCellItemBasis. Native checks confirm both mesh and physics-shape rotation.
SetCellItem(cell,-1) erases a cell. A negative item does not mean an invisible block. In the native check, GetCellItem immediately reported -1, and the physics ray stopped hitting the erased wall after an update.
A GridMap physics hit identifies the GridMap itself, not an individual cell entity. Store per-door or destructible-block state by cell coordinate, or use a separate prefab.
Validate IDs and persistence
Use contiguous IDs starting at 0. The current build has a confirmed .rdata defect: items 0 and 7 work in memory, ResourceSaver returns Error.Ok, but item 7 data is lost after reload. The published example uses 0 and 1; meshes, shapes and offsets survive saving and uncached loading.
Until fixed, do not rely on sparse-library persistence or remove source assets after one successful write. ID migration must update map cell references too, or they will point at different blocks. Full .rscene GridMap persistence and the MeshLibrary export-menu workflow are outside this test.
Two further limits matter:
- An unknown ID can be stored and returned by GetCellItem without producing mesh or physics. Check the library GetItemList; the C# MeshLibrary has no HasItem method.
- The cell item field is 16-bit. Valid nonnegative GridMap item IDs are 0..65535; the native check turns 65536 into 0. Source bounds require cell-coordinate magnitudes below 2^20 per axis. This is not an infinite map.
Diagnosis
| Symptom | Check |
|---|---|
| Stored cell has no visible block | Assigned MeshLibrary, existing ID and item Mesh |
| Visible block has no collision | SetItemShapes pairs and physics layers/masks |
| Walls float or sink | CellCenterY, mesh offset and separate shape offset |
| Rotation does not match 90° | Supply an orthogonal orientation index, not the number 90 |
| Blocks disappear after reload | Sparse IDs and actual reloaded library data |
| AI cannot find a floor route | Navigation data and registration require separate configuration |