This commit is contained in:
wuyanchen 2025-12-25 13:18:48 +08:00
parent 4603f3c573
commit 1c233ddd5b
9 changed files with 800 additions and 104 deletions

View File

@ -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 ===");
}
}
}

View File

@ -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()}");
}
}
}

View File

@ -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<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);
}
}
}

153
XNet.Business/SceneAgent.cs Normal file
View File

@ -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<string, CrowdInstance> _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);
}
});
}
}
}

View File

@ -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<RcOffMeshConnection> _offMeshConnections = new List<RcOffMeshConnection>();
private readonly List<RcConvexVolume> _convexVolumes = new List<RcConvexVolume>();
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<RcTriMesh> 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<RcOffMeshConnection> filter)
{
_offMeshConnections.RemoveAll(filter);
}
// [这里是你报错的地方,已修复]
// 显式接口实现,返回内部列表
List<RcOffMeshConnection> IInputGeomProvider.GetOffMeshConnections()
{
return _offMeshConnections;
}
// 如果外部也需要访问,可以保留这个公共方法
public IList<RcOffMeshConnection> GetOffMeshConnections()
{
return _offMeshConnections;
}
// --- 接口实现Convex Volumes ---
public void AddConvexVolume(RcConvexVolume convexVolume)
{
_convexVolumes.Add(convexVolume);
}
// 显式接口实现
IList<RcConvexVolume> IInputGeomProvider.ConvexVolumes()
{
return _convexVolumes;
}
// 公共方法
public IEnumerable<RcConvexVolume> ConvexVolumes()
{
return _convexVolumes;
}
}
}

View File

@ -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<float>();
var indices = new List<int>();
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() };
}
}
}

View File

@ -10,6 +10,7 @@
<PackageReference Include="DotRecast.Detour.Crowd" Version="2025.2.1" /> <PackageReference Include="DotRecast.Detour.Crowd" Version="2025.2.1" />
<PackageReference Include="DotRecast.Recast" Version="2025.2.1" /> <PackageReference Include="DotRecast.Recast" Version="2025.2.1" />
<PackageReference Include="Microsoft.ClearScript.V8" Version="7.5.0" /> <PackageReference Include="Microsoft.ClearScript.V8" Version="7.5.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Numerics;
using XNet.Business; using XNet.Business;
namespace XNet.Api.Controllers namespace XNet.Api.Controllers
@ -8,10 +9,16 @@ namespace XNet.Api.Controllers
[ApiController] [ApiController]
public class MapController : ControllerBase public class MapController : ControllerBase
{ {
private readonly NavMeshManager? _mapAgent = null;
public MapController(NavMeshManager mapAgent)
{
_mapAgent = mapAgent;
}
[HttpGet] [HttpGet]
public void MapInit() public void MapInit()
{ {
new MapAgent().SampleUsage(@"D:\Rock.nav"); _mapAgent!.FindPath("Instance_TeamA_1", new Vector3(-4, 0, -4), new Vector3(5, 0, 3));
} }
} }
} }

View File

@ -1,4 +1,5 @@
using Scalar.AspNetCore; using Scalar.AspNetCore;
using XNet.Business;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -7,6 +8,13 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
//注入游戏地图导航服务
// 1. 注册 MapAgent 为单例
builder.Services.AddSingleton<NavMeshManager>();
builder.Services.AddSingleton<SceneAgent>();
// 2. 注册 SimulationLoop 为 HostedService (后台运行)
builder.Services.AddHostedService<GameLoopService>();
var app = builder.Build(); var app = builder.Build();