XNet/XNet.Business/NavMeshManager.cs
2025-12-26 14:18:08 +08:00

355 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using DotRecast.Core;
using DotRecast.Core.Numerics;
using DotRecast.Detour;
using DotRecast.Recast;
using System.Numerics;
using System.Collections.Concurrent;
using System.Buffers; // 引入 ArrayPool
using System.Threading;
namespace XNet.Business
{
public class NavMeshManager
{
// --- 核心类:地图资源模板 (共享) ---
private class NavMeshTemplate : IDisposable
{
public string TemplateId { get; }
public DtNavMesh NavMesh { get; }
public DtNavMeshQuery Query { get; }
public ReaderWriterLockSlim RwLock { get; }
private bool _disposed = false;
public NavMeshTemplate(string id, DtNavMesh mesh, DtNavMeshQuery query)
{
TemplateId = id;
NavMesh = mesh;
Query = query;
RwLock = new ReaderWriterLockSlim();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// 释放托管资源
RwLock?.Dispose();
//// 释放DotRecast的非托管资源
//Query?.Dispose();
//NavMesh?.Dispose();
}
_disposed = true;
}
~NavMeshTemplate()
{
Dispose(false);
}
}
// --- 数据存储 ---
private readonly ConcurrentDictionary<string, NavMeshTemplate> _templates = new();
private readonly ConcurrentDictionary<string, string> _instanceMap = new();
// 寻路配置 (SceneAgent 需要访问,改为 Public)
private readonly DtQueryDefaultFilter _filter;
private readonly RcVec3f _extents = new RcVec3f(20.0f, 20.0f, 20.0f);
public DtQueryDefaultFilter Filter => _filter;
public RcVec3f Extents => _extents;
public NavMeshManager()
{
_filter = new DtQueryDefaultFilter(
includeFlags: 0xFFFF,
excludeFlags: 0,
areaCost: new float[] { 1f, 1f, 1f, 1f, 1f, 1f }
);
}
// ==========================================
// 1. 资源管理
// ==========================================
public void LoadTemplate(string templateId, string objFilePath)
{
Console.WriteLine($"[NavMesh] 开始构建模板: {templateId} ...");
NavMeshTemplate newTemplate = null;
try
{
newTemplate = BuildTemplateInternal(templateId, objFilePath);
}
catch (Exception ex)
{
Console.WriteLine($"[Error] 模板 {templateId} 构建失败: {ex.Message}");
return;
}
_templates.AddOrUpdate(templateId,
addValueFactory: (k) =>
{
Console.WriteLine($"[NavMesh] 模板 {k} 加载完成。");
return newTemplate;
},
updateValueFactory: (k, old) =>
{
Console.WriteLine($"[NavMesh] 模板 {k} 更新 (Reload)。");
return newTemplate;
});
}
public void UnloadTemplate(string templateId)
{
if (_templates.TryRemove(templateId, out var template))
{
template.Dispose();
Console.WriteLine($"[NavMesh] 模板 {templateId} 已卸载。");
}
}
// ==========================================
// 2. 副本实例管理
// ==========================================
public bool CreateInstance(string instanceId, string templateId)
{
if (!_templates.ContainsKey(templateId))
{
Console.WriteLine($"[Error] 无法创建实例 {instanceId}, 资源 {templateId} 不存在。");
return false;
}
_instanceMap[instanceId] = templateId;
return true;
}
public void RemoveInstance(string instanceId)
{
_instanceMap.TryRemove(instanceId, out _);
}
// ==========================================
// 3. 【新增】供 SceneAgent 使用的辅助方法
// ==========================================
/// <summary>
/// 获取指定副本实例底层的 NavMesh 和 Query 对象
/// 注意:返回的对象是共享资源,请勿 Dispose也请勿修改其拓扑结构
/// </summary>
public bool GetNavMeshAndQuery(string instanceId, out DtNavMesh navMesh, out DtNavMeshQuery query)
{
navMesh = null!;
query = null!;
if (!_instanceMap.TryGetValue(instanceId, out var templateId)) return false;
if (!_templates.TryGetValue(templateId, out var template)) return false;
navMesh = template.NavMesh;
query = template.Query;
return true;
}
// ==========================================
// 4. 高性能寻路
// ==========================================
public List<Vector3>? FindPath(string instanceId, Vector3 start, Vector3 end)
{
if (!_instanceMap.TryGetValue(instanceId, out var templateId)) return null;
if (!_templates.TryGetValue(templateId, out var template)) return null;
const int MAX_POLYS = 256;
const int MAX_SMOOTH = 256;
long[] polyBuffer = ArrayPool<long>.Shared.Rent(MAX_POLYS);
DtStraightPath[] straightPathBuffer = ArrayPool<DtStraightPath>.Shared.Rent(MAX_SMOOTH);
template.RwLock.EnterReadLock();
try
{
var query = template.Query;
var startPos = new RcVec3f(start.X, start.Y, start.Z);
var endPos = new RcVec3f(end.X, end.Y, end.Z);
query.FindNearestPoly(startPos, _extents, _filter, out long startRef, out var startPt, out var _);
query.FindNearestPoly(endPos, _extents, _filter, out long endRef, out var endPt, out var _);
if (startRef == 0 || endRef == 0) return null;
var status = query.FindPath(
startRef, endRef, startPt, endPt, _filter,
polyBuffer, out int pathCount, MAX_POLYS
);
if (status.Failed() || pathCount == 0) return null;
query.FindStraightPath(
startPt, endPt, polyBuffer.AsSpan(0, pathCount), pathCount,
straightPathBuffer, out int straightPathCount, MAX_SMOOTH, 0
);
var result = new List<Vector3>(straightPathCount);
for (int i = 0; i < straightPathCount; i++)
{
var pos = straightPathBuffer[i].pos;
result.Add(new Vector3(pos.X, pos.Y, pos.Z));
}
return result;
}
finally
{
template.RwLock.ExitReadLock();
ArrayPool<long>.Shared.Return(polyBuffer, clearArray: true);
ArrayPool<DtStraightPath>.Shared.Return(straightPathBuffer, clearArray: true);
}
}
// ==========================================
// 5. 内部构建逻辑
// ==========================================
private NavMeshTemplate BuildTemplateInternal(string id, string objFilePath)
{
var objData = SimpleObjParser.Parse(objFilePath);
var geom = new SimpleInputGeomProvider(objData.Vertices, objData.Indices);
// --- 2. 配置烘焙参数 ---
var walkableAreaMod = new RcAreaModification(63);
var config = new RcConfig(
useTiles: false, // 单块 Mesh
tileSizeX: 0,
tileSizeZ: 0,
borderSize: 0,
partition: RcPartition.WATERSHED,
cellSize: 0.1f,
cellHeight: 0.1f,
agentMaxSlope: 90.0f, // 允许 90 度坡
agentHeight: 2.0f,
agentRadius: 0.05f, // 极小半径
agentMaxClimb: 0.5f,
minRegionArea: 0.0f, // 不过滤小区域
mergeRegionArea: 0.0f,
edgeMaxLen: 12.0f,
edgeMaxError: 1.3f,
vertsPerPoly: 6,
detailSampleDist: 6.0f,
detailSampleMaxError: 1.0f,
filterLowHangingObstacles: false, // 关闭所有过滤
filterLedgeSpans: false,
filterWalkableLowHeightSpans: false,
walkableAreaMod: walkableAreaMod,
buildMeshDetail: true
);
// --- 3. 构建 NavMesh ---
RcBuilder builder = new RcBuilder();
RcVec3f bmin = geom.GetMeshBoundsMin();
RcVec3f bmax = geom.GetMeshBoundsMax();
// 【关键修复】确保高度
if (Math.Abs(bmax.Y - bmin.Y) < 0.001f)
{
Console.WriteLine("Flat mesh detected. Increasing Bounds Height...");
bmax.Y += 5.0f;
bmin.Y -= 0.5f;
}
else // 即使不是绝对平面,如果太薄,也强制加高
{
bmax.Y += 5.0f;
bmin.Y -= 0.5f;
}
//Console.WriteLine($"Adjusted Bounds: Min({bmin.X},{bmin.Y},{bmin.Z}) Max({bmax.X},{bmax.Y},{bmax.Z})");
//Console.WriteLine($"Adjusted Size: {bmax.X - bmin.X}, {bmax.Y - bmin.Y}, {bmax.Z - bmin.Z}");
var builderConfig = new RcBuilderConfig(config, bmin, bmax);
if (builderConfig.width <= 0 || builderConfig.height <= 0)
{
throw new Exception($"网格尺寸计算错误Width: {builderConfig.width}, Height: {builderConfig.height}.");
}
RcBuilderResult result = builder.Build(geom, builderConfig, false);
var meshData = result.Mesh;
// 检查点npolys 应该是 > 0
if (meshData.npolys == 0)
{
throw new Exception($"Recast 构建完成,但生成了 0 个多边形。请检查 OBJ 数据和参数。");
}
// 【强制修复】设置 Flags 为 1 (Walkable)
for (int i = 0; i < meshData.npolys; ++i)
{
meshData.flags[i] = 1;
}
// --- 4. 转换为 Detour 数据 ---
var meshDetail = result.MeshDetail;
var navMeshDataParams = new DtNavMeshCreateParams
{
verts = meshData.verts,
vertCount = meshData.nverts,
polys = meshData.polys,
polyFlags = meshData.flags,
polyAreas = meshData.areas,
polyCount = meshData.npolys,
nvp = meshData.nvp,
detailMeshes = meshDetail?.meshes ?? new int[0],
detailVerts = meshDetail?.verts ?? new float[0],
detailVertsCount = meshDetail?.nverts ?? 0,
detailTris = meshDetail?.tris ?? new int[0],
detailTriCount = meshDetail?.ntris ?? 0,
offMeshConVerts = new float[0],
offMeshConRad = new float[0],
offMeshConDir = new int[0],
offMeshConAreas = new int[0],
offMeshConFlags = new int[0],
offMeshConUserID = new int[0], // 确保是 uint[]
offMeshConCount = 0,
bmin = meshData.bmin,
bmax = meshData.bmax,
walkableHeight = config.WalkableHeightWorld,
walkableRadius = config.WalkableRadiusWorld,
walkableClimb = config.WalkableClimbWorld,
cs = config.Cs,
ch = config.Ch,
buildBvTree = true
};
var navMeshData = DtNavMeshBuilder.CreateNavMeshData(navMeshDataParams);
if (navMeshData == null) throw new Exception("Failed to create Detour NavMeshData.");
// 1. 初始化一个空的 NavMesh 容器
var dtParams = new DtNavMeshParams();
dtParams.orig = navMeshData.header.bmin; // 设置原点
dtParams.tileWidth = navMeshData.header.bmax.X - navMeshData.header.bmin.X; // 设置块宽
dtParams.tileHeight = navMeshData.header.bmax.Z - navMeshData.header.bmin.Z; // 设置块高
dtParams.maxTiles = 1; // 只有一块
dtParams.maxPolys = navMeshData.header.polyCount;
var navMesh = new DtNavMesh();
// 使用 Init 方法加载数据
navMesh.Init(dtParams, 6);
// 关键添加导航数据Tile到NavMesh
navMesh.AddTile(navMeshData, 0, 0, out long tileRef);
if (tileRef == 0)
{
throw new Exception("Failed to add tile to NavMesh!");
}
var navMeshQuery = new DtNavMeshQuery(navMesh);
return new NavMeshTemplate(id, navMesh, navMeshQuery);
}
}
}