`
This commit is contained in:
parent
4603f3c573
commit
1c233ddd5b
94
XNet.Business/GameLoopService.cs
Normal file
94
XNet.Business/GameLoopService.cs
Normal 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 ===");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
355
XNet.Business/NavMeshManager.cs
Normal file
355
XNet.Business/NavMeshManager.cs
Normal 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
153
XNet.Business/SceneAgent.cs
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
102
XNet.Business/SimpleInputGeomProvider.cs
Normal file
102
XNet.Business/SimpleInputGeomProvider.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
XNet.Business/SimpleObjParser.cs
Normal file
79
XNet.Business/SimpleObjParser.cs
Normal 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() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user