Changed `Dictionary<int, List<DtMeshTile>>` to `DtMeshTile[]` to optimize memory usage

This commit is contained in:
ikpil 2024-05-22 01:33:15 +09:00 committed by Ikpil
parent 9a03ade6c9
commit c7f03d00ff
8 changed files with 205 additions and 129 deletions

View File

@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Changed `PolyQueryInvoker` to `DtActionPolyQuery`
- Changed `DtTileCacheBuilder` to a static class
- Changed `DtTileCacheLayerHeaderReader` to a static class
- Changed `Dictionary<int, List<DtMeshTile>>` to `DtMeshTile[]` to optimize memory usage
### Removed
- Nothing

View File

@ -33,6 +33,7 @@ namespace DotRecast.Detour
public DtLink[] links; // The tile links. [Size: dtMeshHeader::maxLinkCount]
public int flags; //< Tile flags. (See: #dtTileFlags)
public DtMeshTile next; //< The next free tile, or the next tile in the spatial grid.
public DtMeshTile(int index)
{

View File

@ -39,8 +39,8 @@ namespace DotRecast.Detour
private int m_tileLutSize; //< Tile hash lookup size (must be pot).
private int m_tileLutMask; // < Tile hash lookup mask.
private Dictionary<int, List<DtMeshTile>> m_posLookup; //< Tile hash lookup.
private LinkedList<DtMeshTile> m_nextFree; //< Freelist of tiles.
private DtMeshTile[] m_posLookup; //< Tile hash lookup.
private DtMeshTile m_nextFree; //< Freelist of tiles.
private DtMeshTile[] m_tiles; //< List of tiles.
/** The maximum number of vertices per navigation polygon. */
@ -64,13 +64,14 @@ namespace DotRecast.Detour
m_tileLutMask = m_tileLutSize - 1;
m_tiles = new DtMeshTile[m_maxTiles];
m_posLookup = new Dictionary<int, List<DtMeshTile>>();
m_nextFree = new LinkedList<DtMeshTile>();
for (int i = 0; i < m_maxTiles; i++)
m_posLookup = new DtMeshTile[m_tileLutSize];
m_nextFree = null;
for (int i = m_maxTiles-1; i >= 0; --i)
{
m_tiles[i] = new DtMeshTile(i);
m_tiles[i].salt = 1;
m_nextFree.AddLast(m_tiles[i]);
m_tiles[i].next = m_nextFree;
m_nextFree = m_tiles[i];
}
return DtStatus.DT_SUCCESS;
@ -384,16 +385,14 @@ namespace DotRecast.Detour
DtMeshTile tile = null;
if (lastRef == 0)
{
// Make sure we could allocate a tile.
if (0 == m_nextFree.Count)
if (null != m_nextFree)
{
throw new Exception("Could not allocate a tile");
}
tile = m_nextFree.First?.Value;
m_nextFree.RemoveFirst();
tile = m_nextFree;
m_nextFree = tile.next;
tile.next = null;
m_tileCount++;
}
}
else
{
// Try to relocate the tile to specific index with same salt.
@ -405,14 +404,25 @@ namespace DotRecast.Detour
// Try to find the specific tile id from the free list.
DtMeshTile target = m_tiles[tileIndex];
// Remove from freelist
if (!m_nextFree.Remove(target))
DtMeshTile prev = null;
tile = m_nextFree;
while (null != tile && tile != target)
{
// Could not find the correct location.
return DtStatus.DT_FAILURE | DtStatus.DT_OUT_OF_MEMORY;
prev = tile;
tile = tile.next;
}
tile = target;
// Could not find the correct location.
if (tile != target)
return DtStatus.DT_FAILURE | DtStatus.DT_OUT_OF_MEMORY;
// Remove from freelist
if (null == prev)
m_nextFree = tile.next;
else
prev.next = tile.next;
// Restore salt.
tile.salt = DecodePolyIdSalt(lastRef);
}
@ -424,7 +434,10 @@ namespace DotRecast.Detour
}
// Insert tile into the position lut.
GetTileListByPos(header.x, header.y).Add(tile);
int h = ComputeTileHash(header.x, header.y, m_tileLutMask);
tile.next = m_posLookup[h];
m_posLookup[h] = tile;
// Patch header pointers.
tile.data = data;
@ -450,13 +463,19 @@ namespace DotRecast.Detour
tile.flags = flags;
ConnectIntLinks(tile);
// Base off-mesh connections to their starting polygons and connect connections inside the tile.
BaseOffMeshLinks(tile);
ConnectExtOffMeshLinks(tile, tile, -1);
// Create connections with neighbour tiles.
const int MAX_NEIS = 32;
DtMeshTile[] neis = new DtMeshTile[MAX_NEIS];
int nneis;
// Connect with layers in current tile.
List<DtMeshTile> neis = GetTilesAt(header.x, header.y);
for (int j = 0; j < neis.Count; ++j)
nneis = GetTilesAt(header.x, header.y, neis, MAX_NEIS);
for (int j = 0; j < nneis; ++j)
{
if (neis[j] == tile)
{
@ -472,8 +491,8 @@ namespace DotRecast.Detour
// Connect with neighbour tiles.
for (int i = 0; i < 8; ++i)
{
neis = GetNeighbourTilesAt(header.x, header.y, i);
for (int j = 0; j < neis.Count; ++j)
nneis = GetNeighbourTilesAt(header.x, header.y, i, neis, MAX_NEIS);
for (int j = 0; j < nneis; ++j)
{
ConnectExtLinks(tile, neis[j], i);
ConnectExtLinks(neis[j], tile, DtUtils.OppositeTile(i));
@ -516,30 +535,44 @@ namespace DotRecast.Detour
}
// Remove tile from hash lookup.
GetTileListByPos(tile.data.header.x, tile.data.header.y).Remove(tile);
// Remove connections to neighbour tiles.
// Create connections with neighbour tiles.
// Disconnect from other layers in current tile.
List<DtMeshTile> nneis = GetTilesAt(tile.data.header.x, tile.data.header.y);
foreach (DtMeshTile j in nneis)
int h = ComputeTileHash(tile.data.header.x, tile.data.header.y, m_tileLutMask);
DtMeshTile prev = null;
DtMeshTile cur = m_posLookup[h];
while (null != cur)
{
if (j == tile)
if (cur == tile)
{
continue;
if (null != prev)
prev.next = cur.next;
else
m_posLookup[h] = cur.next;
break;
}
UnconnectLinks(j, tile);
prev = cur;
cur = cur.next;
}
// Remove connections to neighbour tiles.
const int MAX_NEIS = 32;
DtMeshTile[] neis = new DtMeshTile[MAX_NEIS];
int nneis = 0;
// Disconnect from other layers in current tile.
nneis = GetTilesAt(tile.data.header.x, tile.data.header.y, neis, MAX_NEIS);
for (int j = 0; j < nneis; ++j)
{
if (neis[j] == tile) continue;
UnconnectLinks(neis[j], tile);
}
// Disconnect from neighbour tiles.
for (int i = 0; i < 8; ++i)
{
nneis = GetNeighbourTilesAt(tile.data.header.x, tile.data.header.y, i);
foreach (DtMeshTile j in nneis)
nneis = GetNeighbourTilesAt(tile.data.header.x, tile.data.header.y, i, neis, MAX_NEIS);
for (int j = 0; j < nneis; ++j)
{
UnconnectLinks(j, tile);
UnconnectLinks(neis[j], tile);
}
}
@ -557,7 +590,8 @@ namespace DotRecast.Detour
}
// Add to free list.
m_nextFree.AddFirst(tile);
tile.next = m_nextFree;
m_nextFree = tile;
m_tileCount--;
return GetTileRef(tile);
}
@ -1270,19 +1304,27 @@ namespace DotRecast.Detour
DtMeshTile GetTileAt(int x, int y, int layer)
{
foreach (DtMeshTile tile in GetTileListByPos(x, y))
// Find tile based on hash.
int h = ComputeTileHash(x, y, m_tileLutMask);
DtMeshTile tile = m_posLookup[h];
while (null != tile)
{
if (tile.data.header != null && tile.data.header.x == x && tile.data.header.y == y
&& tile.data.header.layer == layer)
if (null != tile.data &&
null != tile.data.header &&
tile.data.header.x == x &&
tile.data.header.y == y &&
tile.data.header.layer == layer)
{
return tile;
}
tile = tile.next;
}
return null;
}
List<DtMeshTile> GetNeighbourTilesAt(int x, int y, int side)
int GetNeighbourTilesAt(int x, int y, int side, DtMeshTile[] tiles, int maxTiles)
{
int nx = x, ny = y;
switch (side)
@ -1317,21 +1359,31 @@ namespace DotRecast.Detour
break;
}
return GetTilesAt(nx, ny);
return GetTilesAt(nx, ny, tiles, maxTiles);
}
public List<DtMeshTile> GetTilesAt(int x, int y)
public int GetTilesAt(int x, int y, DtMeshTile[] tiles, int maxTiles)
{
List<DtMeshTile> tiles = new List<DtMeshTile>();
foreach (DtMeshTile tile in GetTileListByPos(x, y))
int n = 0;
// Find tile based on hash.
int h = ComputeTileHash(x, y, m_tileLutMask);
DtMeshTile tile = m_posLookup[h];
while (null != tile)
{
if (tile.data.header != null && tile.data.header.x == x && tile.data.header.y == y)
if (null != tile.data &&
null != tile.data.header &&
tile.data.header.x == x &&
tile.data.header.y == y)
{
tiles.Add(tile);
}
if (n < maxTiles)
tiles[n++] = tile;
}
return tiles;
tile = tile.next;
}
return n;
}
public long GetTileRefAt(int x, int y, int layer)
@ -1453,9 +1505,9 @@ namespace DotRecast.Detour
return m_tileCount;
}
public int GetAvailableTileCount()
public bool IsAvailableTileCount()
{
return m_nextFree.Count;
return null != m_nextFree;
}
public DtStatus SetPolyFlags(long refs, int flags)
@ -1613,18 +1665,6 @@ namespace DotRecast.Detour
return center;
}
private List<DtMeshTile> GetTileListByPos(int x, int z)
{
var tileHash = ComputeTileHash(x, z, m_tileLutMask);
if (!m_posLookup.TryGetValue(tileHash, out var tiles))
{
tiles = new List<DtMeshTile>();
m_posLookup.Add(tileHash, tiles);
}
return tiles;
}
public void ComputeBounds(out RcVec3f bmin, out RcVec3f bmax)
{
bmin = new RcVec3f(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);

View File

@ -764,39 +764,27 @@ namespace DotRecast.Detour
// Find tiles the query touches.
RcVec3f bmin = RcVec3f.Subtract(center, halfExtents);
RcVec3f bmax = RcVec3f.Add(center, halfExtents);
foreach (var t in QueryTiles(center, halfExtents))
{
QueryPolygonsInTile(t, bmin, bmax, filter, query);
}
return DtStatus.DT_SUCCESS;
}
/**
* Finds tiles that overlap the search box.
*/
public IList<DtMeshTile> QueryTiles(RcVec3f center, RcVec3f halfExtents)
{
if (!center.IsFinite() || !halfExtents.IsFinite())
{
return RcImmutableArray<DtMeshTile>.Empty;
}
RcVec3f bmin = RcVec3f.Subtract(center, halfExtents);
RcVec3f bmax = RcVec3f.Add(center, halfExtents);
// Find tiles the query touches.
m_nav.CalcTileLoc(bmin, out var minx, out var miny);
m_nav.CalcTileLoc(bmax, out var maxx, out var maxy);
List<DtMeshTile> tiles = new List<DtMeshTile>();
const int MAX_NEIS = 32;
DtMeshTile[] neis = new DtMeshTile[MAX_NEIS];
for (int y = miny; y <= maxy; ++y)
{
for (int x = minx; x <= maxx; ++x)
{
tiles.AddRange(m_nav.GetTilesAt(x, y));
int nneis = m_nav.GetTilesAt(x, y, neis, MAX_NEIS);
for (int j = 0; j < nneis; ++j)
{
QueryPolygonsInTile(neis[j], bmin, bmax, filter, query);
}
}
}
return tiles;
return DtStatus.DT_SUCCESS;
}
/// @par

View File

@ -68,8 +68,8 @@ namespace DotRecast.Recast.Toolset.Tools
tileTriCount = 0; // ...
tileMemUsage = 0; // ...
var availableTileCount = navMesh.GetAvailableTileCount();
if (0 >= availableTileCount)
var availableTile = navMesh.IsAvailableTileCount();
if (!availableTile)
{
return false;
}

View File

@ -24,7 +24,6 @@ using NUnit.Framework;
namespace DotRecast.Detour.Test.Io;
public class MeshSetReaderTest
{
private readonly DtMeshSetReader reader = new DtMeshSetReader();
@ -39,20 +38,28 @@ public class MeshSetReaderTest
Assert.That(mesh.GetMaxTiles(), Is.EqualTo(128));
Assert.That(mesh.GetParams().maxPolys, Is.EqualTo(0x8000));
Assert.That(mesh.GetParams().tileWidth, Is.EqualTo(9.6f).Within(0.001f));
List<DtMeshTile> tiles = mesh.GetTilesAt(4, 7);
Assert.That(tiles.Count, Is.EqualTo(1));
const int MAX_NEIS = 32;
DtMeshTile[] tiles = new DtMeshTile[MAX_NEIS];
int nneis = 0;
nneis = mesh.GetTilesAt(4, 7, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(7));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(22 * 3));
tiles = mesh.GetTilesAt(1, 6);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(1, 6, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(7));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(26 * 3));
tiles = mesh.GetTilesAt(6, 2);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(6, 2, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(1));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(4 * 3));
tiles = mesh.GetTilesAt(7, 6);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(7, 6, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(8));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(24 * 3));
}
@ -68,20 +75,28 @@ public class MeshSetReaderTest
Assert.That(mesh.GetMaxTiles(), Is.EqualTo(128));
Assert.That(mesh.GetParams().maxPolys, Is.EqualTo(0x8000));
Assert.That(mesh.GetParams().tileWidth, Is.EqualTo(9.6f).Within(0.001f));
List<DtMeshTile> tiles = mesh.GetTilesAt(6, 9);
Assert.That(tiles.Count, Is.EqualTo(1));
const int MAX_NEIS = 32;
DtMeshTile[] tiles = new DtMeshTile[MAX_NEIS];
int nneis = 0;
nneis = mesh.GetTilesAt(6, 9, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(7 * 3));
tiles = mesh.GetTilesAt(2, 9);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(2, 9, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(9 * 3));
tiles = mesh.GetTilesAt(4, 3);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(4, 3, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(3));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(6 * 3));
tiles = mesh.GetTilesAt(2, 8);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(2, 8, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(5));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(17 * 3));
}
@ -97,20 +112,28 @@ public class MeshSetReaderTest
Assert.That(mesh.GetMaxTiles(), Is.EqualTo(128));
Assert.That(mesh.GetParams().maxPolys, Is.EqualTo(0x8000));
Assert.That(mesh.GetParams().tileWidth, Is.EqualTo(9.6f).Within(0.001f));
List<DtMeshTile> tiles = mesh.GetTilesAt(6, 9);
Assert.That(tiles.Count, Is.EqualTo(1));
const int MAX_NEIS = 32;
DtMeshTile[] tiles = new DtMeshTile[MAX_NEIS];
int nneis = 0;
nneis = mesh.GetTilesAt(6, 9, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(7 * 3));
tiles = mesh.GetTilesAt(2, 9);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(2, 9, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(9 * 3));
tiles = mesh.GetTilesAt(4, 3);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(4, 3, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(3));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(6 * 3));
tiles = mesh.GetTilesAt(2, 8);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(2, 8, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(5));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(17 * 3));
}

View File

@ -107,20 +107,28 @@ public class MeshSetReaderWriterTest
Assert.That(mesh.GetMaxTiles(), Is.EqualTo(128));
Assert.That(mesh.GetParams().maxPolys, Is.EqualTo(0x8000));
Assert.That(mesh.GetParams().tileWidth, Is.EqualTo(9.6f).Within(0.001f));
List<DtMeshTile> tiles = mesh.GetTilesAt(6, 9);
Assert.That(tiles.Count, Is.EqualTo(1));
const int MAX_NEIS = 32;
DtMeshTile[] tiles = new DtMeshTile[MAX_NEIS];
int nneis = 0;
nneis = mesh.GetTilesAt(6, 9, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(7 * 3));
tiles = mesh.GetTilesAt(2, 9);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(2, 9, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(2));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(9 * 3));
tiles = mesh.GetTilesAt(4, 3);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(4, 3, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(3));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(6 * 3));
tiles = mesh.GetTilesAt(2, 8);
Assert.That(tiles.Count, Is.EqualTo(1));
nneis = mesh.GetTilesAt(2, 8, tiles, MAX_NEIS);
Assert.That(nneis, Is.EqualTo(1));
Assert.That(tiles[0].data.polys.Length, Is.EqualTo(5));
Assert.That(tiles[0].data.verts.Length, Is.EqualTo(17 * 3));
}

View File

@ -26,7 +26,6 @@ using NUnit.Framework;
namespace DotRecast.Detour.TileCache.Test;
public class TempObstaclesTest : AbstractTileCacheTest
{
[Test]
@ -43,21 +42,29 @@ public class TempObstaclesTest : AbstractTileCacheTest
tc.BuildNavMeshTile(refs);
}
List<DtMeshTile> tiles = tc.GetNavMesh().GetTilesAt(1, 4);
const int MAX_NEIS = 32;
DtMeshTile[] tiles = new DtMeshTile[MAX_NEIS];
int nneis = 0;
nneis = tc.GetNavMesh().GetTilesAt(1, 4, tiles, MAX_NEIS);
DtMeshTile tile = tiles[0];
Assert.That(tile.data.header.vertCount, Is.EqualTo(16));
Assert.That(tile.data.header.polyCount, Is.EqualTo(6));
long o = tc.AddObstacle(new RcVec3f(-1.815208f, 9.998184f, -20.307983f), 1f, 2f);
bool upToDate = tc.Update();
Assert.That(upToDate, Is.True);
tiles = tc.GetNavMesh().GetTilesAt(1, 4);
nneis = tc.GetNavMesh().GetTilesAt(1, 4, tiles, MAX_NEIS);
tile = tiles[0];
Assert.That(tile.data.header.vertCount, Is.EqualTo(22));
Assert.That(tile.data.header.polyCount, Is.EqualTo(11));
tc.RemoveObstacle(o);
upToDate = tc.Update();
Assert.That(upToDate, Is.True);
tiles = tc.GetNavMesh().GetTilesAt(1, 4);
nneis = tc.GetNavMesh().GetTilesAt(1, 4, tiles, MAX_NEIS);
tile = tiles[0];
Assert.That(tile.data.header.vertCount, Is.EqualTo(16));
Assert.That(tile.data.header.polyCount, Is.EqualTo(6));
@ -77,24 +84,32 @@ public class TempObstaclesTest : AbstractTileCacheTest
tc.BuildNavMeshTile(refs);
}
List<DtMeshTile> tiles = tc.GetNavMesh().GetTilesAt(1, 4);
const int MAX_NEIS = 32;
DtMeshTile[] tiles = new DtMeshTile[MAX_NEIS];
int nneis = 0;
nneis = tc.GetNavMesh().GetTilesAt(1, 4, tiles, MAX_NEIS);
DtMeshTile tile = tiles[0];
Assert.That(tile.data.header.vertCount, Is.EqualTo(16));
Assert.That(tile.data.header.polyCount, Is.EqualTo(6));
long o = tc.AddBoxObstacle(
new RcVec3f(-2.315208f, 9.998184f, -20.807983f),
new RcVec3f(-1.315208f, 11.998184f, -19.807983f)
);
bool upToDate = tc.Update();
Assert.That(upToDate, Is.True);
tiles = tc.GetNavMesh().GetTilesAt(1, 4);
nneis = tc.GetNavMesh().GetTilesAt(1, 4, tiles, MAX_NEIS);
tile = tiles[0];
Assert.That(tile.data.header.vertCount, Is.EqualTo(22));
Assert.That(tile.data.header.polyCount, Is.EqualTo(11));
tc.RemoveObstacle(o);
upToDate = tc.Update();
Assert.That(upToDate, Is.True);
tiles = tc.GetNavMesh().GetTilesAt(1, 4);
nneis = tc.GetNavMesh().GetTilesAt(1, 4, tiles, MAX_NEIS);
tile = tiles[0];
Assert.That(tile.data.header.vertCount, Is.EqualTo(16));
Assert.That(tile.data.header.polyCount, Is.EqualTo(6));