From 1c233ddd5b9447f9f390385bdbbc84a488ccaec5 Mon Sep 17 00:00:00 2001 From: wuyanchen <307378529@qq.com> Date: Thu, 25 Dec 2025 13:18:48 +0800 Subject: [PATCH] ` --- XNet.Business/GameLoopService.cs | 94 ++++++ XNet.Business/MapAgent.cs | 103 ------- XNet.Business/NavMeshManager.cs | 355 +++++++++++++++++++++++ XNet.Business/SceneAgent.cs | 153 ++++++++++ XNet.Business/SimpleInputGeomProvider.cs | 102 +++++++ XNet.Business/SimpleObjParser.cs | 79 +++++ XNet.Business/XNet.Business.csproj | 1 + XNet/Controllers/MapController.cs | 9 +- XNet/Program.cs | 8 + 9 files changed, 800 insertions(+), 104 deletions(-) create mode 100644 XNet.Business/GameLoopService.cs delete mode 100644 XNet.Business/MapAgent.cs create mode 100644 XNet.Business/NavMeshManager.cs create mode 100644 XNet.Business/SceneAgent.cs create mode 100644 XNet.Business/SimpleInputGeomProvider.cs create mode 100644 XNet.Business/SimpleObjParser.cs diff --git a/XNet.Business/GameLoopService.cs b/XNet.Business/GameLoopService.cs new file mode 100644 index 0000000..cadf6d6 --- /dev/null +++ b/XNet.Business/GameLoopService.cs @@ -0,0 +1,94 @@ +using Microsoft.Extensions.Hosting; +using System.Diagnostics; +using System.Numerics; + +namespace XNet.Business +{ + public class GameLoopService : BackgroundService + { + private readonly NavMeshManager _navMeshManager; + private readonly SceneAgent _sceneAgent; + + private const int TARGET_FPS = 30; + private const int FRAME_TIME_MS = 1000 / TARGET_FPS; + + public GameLoopService(NavMeshManager navMeshManager, SceneAgent sceneAgent) + { + _navMeshManager = navMeshManager; + _sceneAgent = sceneAgent; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Console.WriteLine("=== Server Initializing Resources... ==="); + + // 1. 加载所有静态地图资源 (Templates) + // 这里假设路径固定,实际可从配置表读取 + _navMeshManager.LoadTemplate("Map_Forest", @"D:\NavMeshExport.obj"); + //_navMeshManager.LoadTemplate("Map_Dungeon", @"D:\NavMeshExport.obj"); + + // 2. 模拟创建副本逻辑 (实际应由 WebAPI 或 MatchService 触发) + // 假设现在有两个队伍分别开启了森林副本 + string instanceGuid_A = "Instance_TeamA_1";// + Guid.NewGuid(); + string instanceGuid_B = "Instance_TeamB_" + Guid.NewGuid(); + + // 注册 NavMesh 映射 + _navMeshManager.CreateInstance(instanceGuid_A, "Map_Forest"); + _navMeshManager.CreateInstance(instanceGuid_B, "Map_Forest"); // 复用同一份 NavMesh 内存 + + // 为这两个副本创建独立的物理/避障模拟器 + _sceneAgent.CreateCrowdForInstance(instanceGuid_A); + _sceneAgent.CreateCrowdForInstance(instanceGuid_B); + + // 添加一些测试怪物 + int monsterA = _sceneAgent.AddAgent(instanceGuid_A, new Vector3(-4, 0, -4), 0.05f, 1); + _sceneAgent.AgentGoto(instanceGuid_A, monsterA, new Vector3(5, 0, 3)); + + Console.WriteLine("=== Server Game Loop Started ==="); + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + long lastTime = stopwatch.ElapsedMilliseconds; + + while (!stoppingToken.IsCancellationRequested) + { + long currentTime = stopwatch.ElapsedMilliseconds; + long elapsedMs = currentTime - lastTime; + + // 防止调试断点导致 delta 过大,限制最大单帧时间 + if (elapsedMs > 100) elapsedMs = 100; + + float deltaTime = elapsedMs / 1000.0f; + lastTime = currentTime; + + // 【核心循环】 + // 现在不需要一把大锁锁住全服了 + + // 1. 如果有来自 WebAPI 的异步命令(如动态创建副本),可以在 NavMeshManager 内部处理 + // 或者在这里手动处理队列 (如果使用了 CommandQueue) + // _navMeshManager.ProcessCommands(); + + // 2. 并行更新所有副本的 Crowd + // 这行代码利用 SceneAgent 内部的 Parallel.ForEach,效率极高 + _sceneAgent.UpdateAll(deltaTime); + + //var p = _sceneAgent.GetAgentPosition(instanceGuid_A, monsterA); + //if (p != null) + //{ + // Console.WriteLine(p); + //} + + // 帧率控制 + long frameWorkTime = stopwatch.ElapsedMilliseconds - currentTime; + int delay = (int)(FRAME_TIME_MS - frameWorkTime); + + if (delay > 0) + { + await Task.Delay(delay, stoppingToken); + } + } + + Console.WriteLine("=== Server Game Loop Stopped ==="); + } + } +} \ No newline at end of file diff --git a/XNet.Business/MapAgent.cs b/XNet.Business/MapAgent.cs deleted file mode 100644 index ec62551..0000000 --- a/XNet.Business/MapAgent.cs +++ /dev/null @@ -1,103 +0,0 @@ -using DotRecast.Core; -using DotRecast.Detour; -using DotRecast.Detour.Io; -using System; -using System.Collections.Generic; -using System.Text; - -namespace XNet.Business -{ - public class MapAgent - { - public MapAgent() { } - - public DtNavMesh LoadFromBabylon(string filePath) - { - byte[] data = File.ReadAllBytes(filePath); - data[3] = 77; // 修正 Magic (TSET -> MSET) - - RcByteBuffer bb = new RcByteBuffer(data); - bb.Order(RcByteOrder.LITTLE_ENDIAN); - - int magic = bb.GetInt(); - int version = bb.GetInt(); - int numTiles = bb.GetInt(); - - DtNavMeshParams option = new DtNavMeshParams(); - option.orig.X = bb.GetFloat(); - option.orig.Y = bb.GetFloat(); - option.orig.Z = bb.GetFloat(); - option.tileWidth = bb.GetFloat(); - option.tileHeight = bb.GetFloat(); - option.maxTiles = bb.GetInt(); - option.maxPolys = bb.GetInt(); - - // --- 关键修正点 1:探测偏移量 --- - // 有些版本的 Babylon 导出的 Params 后面多了一个 int (可能是 maxVertsPerPoly) - // 如果我们发现读出来的 dataSize 大得离谱,就尝试往后挪 4 个字节 - - int savedPos = bb.Position(); - long testTileRef = bb.GetInt(); - int testDataSize = bb.GetInt(); - - // 逻辑判断:如果 dataSize 超过了剩余文件大小,或者是个负数,说明对准错了 - if (testDataSize <= 0 || testDataSize > (data.Length - bb.Position())) - { - Console.WriteLine("检测到字节对齐偏离,尝试跳过 4 字节..."); - bb.Position(savedPos + 4); // 往后挪 4 位再试 - } - else - { - bb.Position(savedPos); // 看起来是对的,退回去正常读 - } - // -------------------------------- - - DtNavMesh navMesh = new DtNavMesh(); - navMesh.Init(option, 6); - - DtMeshDataReader tileReader = new DtMeshDataReader(); - - for (int i = 0; i < numTiles; i++) - { - if (bb.Position() + 8 > data.Length) break; - - long tileRef = bb.GetInt(); - int dataSize = bb.GetInt(); - - Console.WriteLine($"Tile[{i}] Ref: {tileRef}, Size: {dataSize}"); - - if (dataSize <= 0 || dataSize > data.Length) - { - Console.WriteLine("警告:dataSize 依然非法,解析中断。"); - break; - } - - byte[] tileData = new byte[dataSize]; - Array.Copy(data, bb.Position(), tileData, 0, dataSize); - bb.Position(bb.Position() + dataSize); - - RcByteBuffer tileBuffer = new RcByteBuffer(tileData); - tileBuffer.Order(RcByteOrder.LITTLE_ENDIAN); - - DtMeshData meshData = tileReader.Read(tileBuffer, 6); - if (meshData != null) - { - navMesh.AddTile(meshData, 0, tileRef, out long result); - } - } - - return navMesh; - } - - public void SampleUsage(string path) - { - DtNavMesh navMesh = LoadFromBabylon(path); - - // 3. 创建查询对象 - DtNavMeshQuery query = new DtNavMeshQuery(navMesh); - - // 之后就可以进行寻路了,例如 query.FindPath(...) - Console.WriteLine($"NavMesh loaded. Max Tiles: {navMesh.GetMaxTiles()}"); - } - } -} diff --git a/XNet.Business/NavMeshManager.cs b/XNet.Business/NavMeshManager.cs new file mode 100644 index 0000000..c516bf7 --- /dev/null +++ b/XNet.Business/NavMeshManager.cs @@ -0,0 +1,355 @@ +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 _templates = new(); + private readonly ConcurrentDictionary _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 使用的辅助方法 + // ========================================== + + /// + /// 获取指定副本实例底层的 NavMesh 和 Query 对象 + /// 注意:返回的对象是共享资源,请勿 Dispose,也请勿修改其拓扑结构 + /// + 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? 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.Shared.Rent(MAX_POLYS); + DtStraightPath[] straightPathBuffer = ArrayPool.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(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.Shared.Return(polyBuffer, clearArray: true); + ArrayPool.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); + } + } +} \ No newline at end of file diff --git a/XNet.Business/SceneAgent.cs b/XNet.Business/SceneAgent.cs new file mode 100644 index 0000000..18a7a19 --- /dev/null +++ b/XNet.Business/SceneAgent.cs @@ -0,0 +1,153 @@ +using DotRecast.Core; +using DotRecast.Core.Numerics; +using DotRecast.Detour; +using DotRecast.Detour.Crowd; +using System.Collections.Concurrent; +using System.Numerics; + +namespace XNet.Business +{ + public class SceneAgent + { + private readonly NavMeshManager _navMeshManager; + + private class CrowdInstance + { + public DtCrowd Crowd { get; set; } = null!; + public object SyncRoot { get; } = new object(); + } + + private readonly ConcurrentDictionary _crowdInstances = new(); + + private const float AGENT_RADIUS = 0.5f; + private const float AGENT_HEIGHT = 2.0f; + + public SceneAgent(NavMeshManager navMeshManager) + { + _navMeshManager = navMeshManager; + } + + public bool CreateCrowdForInstance(string instanceId) + { + // 使用 NavMeshManager 新增的 Helper 方法 + if (!_navMeshManager.GetNavMeshAndQuery(instanceId, out var navMesh, out _)) + { + return false; + } + + var config = new DtCrowdConfig(0.6f); + var crowd = new DtCrowd(config, navMesh); + + var paramsData = crowd.GetObstacleAvoidanceParams(0); + paramsData.velBias = 0.4f; + paramsData.adaptiveDivs = 5; + paramsData.adaptiveRings = 2; + paramsData.adaptiveDepth = 2; + crowd.SetObstacleAvoidanceParams(0, paramsData); + + var instance = new CrowdInstance { Crowd = crowd }; + return _crowdInstances.TryAdd(instanceId, instance); + } + + public void RemoveCrowdInstance(string instanceId) + { + _crowdInstances.TryRemove(instanceId, out _); + } + + public int AddAgent(string instanceId, Vector3 position, float radius, float height, float maxAcceleration = 8.0f, float maxSpeed = 3.5f) + { + if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return -1; + + lock (ci.SyncRoot) + { + var pos = new RcVec3f(position.X, position.Y, -position.Z); + var ap = new DtCrowdAgentParams + { + radius = radius, + height = height, + maxAcceleration = maxAcceleration, + maxSpeed = maxSpeed, + collisionQueryRange = radius * 12.0f, + pathOptimizationRange = radius * 30.0f, + updateFlags = DtCrowdAgentUpdateFlags.DT_CROWD_ANTICIPATE_TURNS + | DtCrowdAgentUpdateFlags.DT_CROWD_OBSTACLE_AVOIDANCE + | DtCrowdAgentUpdateFlags.DT_CROWD_SEPARATION + | DtCrowdAgentUpdateFlags.DT_CROWD_OPTIMIZE_VIS + | DtCrowdAgentUpdateFlags.DT_CROWD_OPTIMIZE_TOPO + }; + + return ci.Crowd.AddAgent(pos, ap).idx; + } + } + + public void RemoveAgent(string instanceId, int agentIdx) + { + if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return; + + lock (ci.SyncRoot) + { + var agent = ci.Crowd.GetAgent(agentIdx); + // 【修复】判断 Agent 是否有效,需要检查 state + if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID) + { + ci.Crowd.RemoveAgent(agent); + } + } + } + + public bool AgentGoto(string instanceId, int agentIdx, Vector3 destination) + { + if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return false; + + // 使用 NavMeshManager 获取 Query + if (!_navMeshManager.GetNavMeshAndQuery(instanceId, out _, out var query)) return false; + + var targetPos = new RcVec3f(destination.X, destination.Y, -destination.Z); + + // 使用 public 的 Extents 和 Filter + query.FindNearestPoly(targetPos, _navMeshManager.Extents, _navMeshManager.Filter, + out long targetRef, out var realTargetPos, out var _); + + if (targetRef == 0) return false; + + lock (ci.SyncRoot) + { + var agent = ci.Crowd.GetAgent(agentIdx); + // 【修复】检查 state + if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID) + { + return ci.Crowd.RequestMoveTarget(agent, targetRef, realTargetPos); + } + } + return false; + } + + public Vector3? GetAgentPosition(string instanceId, int agentIdx) + { + if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return null; + + lock (ci.SyncRoot) + { + var agent = ci.Crowd.GetAgent(agentIdx); + // 【修复】检查 state + if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID) + { + var p = agent.npos; + return new Vector3(p.X, p.Y, -p.Z); + } + } + return null; + } + + public void UpdateAll(float deltaTime) + { + Parallel.ForEach(_crowdInstances.Values, ci => + { + lock (ci.SyncRoot) + { + ci.Crowd.Update(deltaTime, null); + } + }); + } + } +} \ No newline at end of file diff --git a/XNet.Business/SimpleInputGeomProvider.cs b/XNet.Business/SimpleInputGeomProvider.cs new file mode 100644 index 0000000..dd5ea89 --- /dev/null +++ b/XNet.Business/SimpleInputGeomProvider.cs @@ -0,0 +1,102 @@ +using DotRecast.Core.Numerics; +using DotRecast.Recast; +using DotRecast.Recast.Geom; +using System; +using System.Collections.Generic; +using System.Text; + +namespace XNet.Business +{ + // 1. 定义一个简单的几何输入类,适配 DotRecast 接口 + public class SimpleInputGeomProvider : IInputGeomProvider + { + private readonly float[] _vertices; + private readonly int[] _faces; + private readonly RcVec3f _bmin; + private readonly RcVec3f _bmax; + private readonly RcTriMesh _triMesh; + + // [关键] 定义数据存储列表 + private readonly List _offMeshConnections = new List(); + private readonly List _convexVolumes = new List(); + + public SimpleInputGeomProvider(float[] vertices, int[] faces) + { + _vertices = vertices; + _faces = faces; + _triMesh = new RcTriMesh(_vertices, _faces); + + _bmin = new RcVec3f(float.MaxValue, float.MaxValue, float.MaxValue); + _bmax = new RcVec3f(float.MinValue, float.MinValue, float.MinValue); + + for (int i = 0; i < vertices.Length; i += 3) + { + float x = vertices[i]; + float y = vertices[i + 1]; + float z = vertices[i + 2]; + + _bmin.X = Math.Min(_bmin.X, x); + _bmin.Y = Math.Min(_bmin.Y, y); + _bmin.Z = Math.Min(_bmin.Z, z); + + _bmax.X = Math.Max(_bmax.X, x); + _bmax.Y = Math.Max(_bmax.Y, y); + _bmax.Z = Math.Max(_bmax.Z, z); + } + } + + public RcVec3f GetMeshBoundsMin() => _bmin; + public RcVec3f GetMeshBoundsMax() => _bmax; + + public RcTriMesh GetMesh() => _triMesh; + + public IEnumerable Meshes() + { + yield return _triMesh; + } + + // --- 接口实现:Off-Mesh Connections --- + + public void AddOffMeshConnection(RcVec3f start, RcVec3f end, float radius, bool bidir, int area, int flags) + { + _offMeshConnections.Add(new RcOffMeshConnection(start, end, radius, bidir, area, flags)); + } + + public void RemoveOffMeshConnections(Predicate filter) + { + _offMeshConnections.RemoveAll(filter); + } + + // [这里是你报错的地方,已修复] + // 显式接口实现,返回内部列表 + List IInputGeomProvider.GetOffMeshConnections() + { + return _offMeshConnections; + } + + // 如果外部也需要访问,可以保留这个公共方法 + public IList GetOffMeshConnections() + { + return _offMeshConnections; + } + + // --- 接口实现:Convex Volumes --- + + public void AddConvexVolume(RcConvexVolume convexVolume) + { + _convexVolumes.Add(convexVolume); + } + + // 显式接口实现 + IList IInputGeomProvider.ConvexVolumes() + { + return _convexVolumes; + } + + // 公共方法 + public IEnumerable ConvexVolumes() + { + return _convexVolumes; + } + } +} diff --git a/XNet.Business/SimpleObjParser.cs b/XNet.Business/SimpleObjParser.cs new file mode 100644 index 0000000..a3cfe66 --- /dev/null +++ b/XNet.Business/SimpleObjParser.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace XNet.Business +{ + public class SimpleObjParser + { + public class ObjData + { + public required float[] Vertices { get; set; } + public required int[] Indices { get; set; } + } + + public static ObjData Parse(string filePath) + { + var verts = new List(); + var indices = new List(); + + if (!File.Exists(filePath)) throw new FileNotFoundException("Obj file not found", filePath); + var lines = File.ReadAllLines(filePath); + + foreach (var line in lines) + { + var trimmed = line.Trim(); + if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#")) continue; + var parts = trimmed.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) continue; + + if (parts[0] == "v") + { + // 修改为 (翻转 Z 轴并交换 Y/Z,视具体 OBJ 情况尝试): + float x = float.Parse(parts[1], CultureInfo.InvariantCulture); + float y = float.Parse(parts[2], CultureInfo.InvariantCulture); + float z = float.Parse(parts[3], CultureInfo.InvariantCulture); + + // 方案 A: 标准 Unity 导出 (无需修改,保持 x, y, z) - 如果 Size Y 正常,用这个。 + verts.Add(x); + verts.Add(y); + verts.Add(z); + + // 方案 B: 修正翻转 (如果你的地图竖起来了,或者 X 轴镜像了) + //verts.Add(x); + //verts.Add(z); // 把 Z 变成 Y + //verts.Add(y); // 把 Y 变成 Z + } + else if (parts[0] == "f") + { + if (parts.Length >= 4) + { + indices.Add(int.Parse(parts[1].Split('/')[0]) - 1); + indices.Add(int.Parse(parts[2].Split('/')[0]) - 1); + indices.Add(int.Parse(parts[3].Split('/')[0]) - 1); + } + + //if (parts.Length >= 4) + //{ + // int v1 = int.Parse(parts[1].Split('/')[0]) - 1; + // int v2 = int.Parse(parts[2].Split('/')[0]) - 1; + // int v3 = int.Parse(parts[3].Split('/')[0]) - 1; + + // // --- 修改开始 --- + // // 尝试 1: 默认顺序 (你当前的代码) + // // indices.Add(v1); indices.Add(v2); indices.Add(v3); + + // // 尝试 2: [强力推荐] 交换 v2 和 v3 (反转面朝向) + // // 如果之前生成为 0,这行代码通常能救命 + // indices.Add(v1); + // indices.Add(v3); // 注意:这里换成了 v3 + // indices.Add(v2); // 注意:这里换成了 v2 + // // --- 修改结束 --- + //} + } + } + return new ObjData { Vertices = verts.ToArray(), Indices = indices.ToArray() }; + } + } +} diff --git a/XNet.Business/XNet.Business.csproj b/XNet.Business/XNet.Business.csproj index 720dfe7..ab61126 100644 --- a/XNet.Business/XNet.Business.csproj +++ b/XNet.Business/XNet.Business.csproj @@ -10,6 +10,7 @@ + diff --git a/XNet/Controllers/MapController.cs b/XNet/Controllers/MapController.cs index bd00d7a..f6439d1 100644 --- a/XNet/Controllers/MapController.cs +++ b/XNet/Controllers/MapController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using System.Numerics; using XNet.Business; namespace XNet.Api.Controllers @@ -8,10 +9,16 @@ namespace XNet.Api.Controllers [ApiController] public class MapController : ControllerBase { + private readonly NavMeshManager? _mapAgent = null; + public MapController(NavMeshManager mapAgent) + { + _mapAgent = mapAgent; + } + [HttpGet] public void MapInit() { - new MapAgent().SampleUsage(@"D:\Rock.nav"); + _mapAgent!.FindPath("Instance_TeamA_1", new Vector3(-4, 0, -4), new Vector3(5, 0, 3)); } } } diff --git a/XNet/Program.cs b/XNet/Program.cs index a6e7ee7..46c8aae 100644 --- a/XNet/Program.cs +++ b/XNet/Program.cs @@ -1,4 +1,5 @@ using Scalar.AspNetCore; +using XNet.Business; var builder = WebApplication.CreateBuilder(args); @@ -7,6 +8,13 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); +//עϷͼ +// 1. ע MapAgent Ϊ +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// 2. ע SimulationLoop Ϊ HostedService (̨) +builder.Services.AddHostedService(); var app = builder.Build();