This commit is contained in:
wuyanchen 2025-12-25 14:32:23 +08:00
parent 1c233ddd5b
commit d148681135
8 changed files with 493 additions and 46 deletions

View File

@ -0,0 +1,45 @@
using MessagePack;
using System.Numerics;
using System.Text.Json;
namespace XNet.Business
{
// WebSocket消息类型枚举
public enum WsMsgType
{
SubscribeInstance = 1, // 客户端订阅实例
AgentPositionSync = 2 // 服务端推送Agent位置
}
// 客户端订阅实例的请求消息
[MessagePackObject]
public class SubscribeInstanceReq
{
[Key("instanceId")]
public string InstanceId { get; set; } = string.Empty;
}
// Agent位置同步消息服务端推送
[MessagePackObject]
public class AgentPositionSyncMsg
{
[Key("instanceId")]
public string InstanceId { get; set; } = string.Empty;
[Key("agentIdx")]
public int AgentIdx { get; set; }
[Key("position")]
public Vector3 Position { get; set; }
[Key("rotation")]
public float Rotation { get; set; } // 简化为绕Y轴旋转弧度
}
// WebSocket通用消息包装
[MessagePackObject]
public class WsMessage
{
[Key("type")]
public WsMsgType Type { get; set; } = WsMsgType.SubscribeInstance;
[Key("data")]
public byte[] Data { get; set; } = string.Empty;
}
}

View File

@ -8,14 +8,16 @@ namespace XNet.Business
{ {
private readonly NavMeshManager _navMeshManager; private readonly NavMeshManager _navMeshManager;
private readonly SceneAgent _sceneAgent; private readonly SceneAgent _sceneAgent;
private readonly WsConnectionManager _wsManager; // 新增WebSocket管理器
private const int TARGET_FPS = 30; private const int TARGET_FPS = 30;
private const int FRAME_TIME_MS = 1000 / TARGET_FPS; private const int FRAME_TIME_MS = 1000 / TARGET_FPS;
public GameLoopService(NavMeshManager navMeshManager, SceneAgent sceneAgent) public GameLoopService(NavMeshManager navMeshManager, SceneAgent sceneAgent, WsConnectionManager wsManager)
{ {
_navMeshManager = navMeshManager; _navMeshManager = navMeshManager;
_sceneAgent = sceneAgent; _sceneAgent = sceneAgent;
_wsManager = wsManager;
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@ -29,7 +31,7 @@ namespace XNet.Business
// 2. 模拟创建副本逻辑 (实际应由 WebAPI 或 MatchService 触发) // 2. 模拟创建副本逻辑 (实际应由 WebAPI 或 MatchService 触发)
// 假设现在有两个队伍分别开启了森林副本 // 假设现在有两个队伍分别开启了森林副本
string instanceGuid_A = "Instance_TeamA_1";// + Guid.NewGuid(); string instanceGuid_A = "Instance_TeamA_" + Guid.NewGuid();
string instanceGuid_B = "Instance_TeamB_" + Guid.NewGuid(); string instanceGuid_B = "Instance_TeamB_" + Guid.NewGuid();
// 注册 NavMesh 映射 // 注册 NavMesh 映射
@ -72,11 +74,19 @@ namespace XNet.Business
// 这行代码利用 SceneAgent 内部的 Parallel.ForEach效率极高 // 这行代码利用 SceneAgent 内部的 Parallel.ForEach效率极高
_sceneAgent.UpdateAll(deltaTime); _sceneAgent.UpdateAll(deltaTime);
//var p = _sceneAgent.GetAgentPosition(instanceGuid_A, monsterA); // 2. 收集所有需要同步的Agent状态
//if (p != null) List<AgentPositionSyncMsg> allSyncMsgs =
//{ [
// Console.WriteLine(p); // 遍历所有实例可从_navMeshManager获取实例列表或SceneAgent维护
//} .. _sceneAgent.GetAgentsNeedSync(instanceGuid_A),
.. _sceneAgent.GetAgentsNeedSync(instanceGuid_B),
];
// 3. 异步发送WebSocket消息不阻塞游戏循环
if (allSyncMsgs.Count > 0)
{
_ = _wsManager.SendAgentPositionBatchAsync(allSyncMsgs);
}
// 帧率控制 // 帧率控制
long frameWorkTime = stopwatch.ElapsedMilliseconds - currentTime; long frameWorkTime = stopwatch.ElapsedMilliseconds - currentTime;

View File

@ -0,0 +1,12 @@
{
"profiles": {
"XNet.Business": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:52803;http://localhost:52804"
}
}
}

View File

@ -13,21 +13,169 @@ namespace XNet.Business
private class CrowdInstance private class CrowdInstance
{ {
public DtCrowd Crowd { get; set; } = null!; public string InstanceId { get; }
public object SyncRoot { get; } = new object(); public DtCrowd Crowd { get; set; }
public object SyncRoot { get; } = new object(); // 线程锁
// 新增:存储该实例下所有 Agent 的索引(关键修复)
public List<int> AgentIndices { get; } = new List<int>();
// 构造函数
public CrowdInstance(string instanceId, DtCrowd crowd)
{
InstanceId = instanceId;
Crowd = crowd;
}
// 辅助:添加 Agent 时自动记录索引
public void AddAgentIndex(int agentIdx)
{
lock (SyncRoot)
{
if (!AgentIndices.Contains(agentIdx))
{
AgentIndices.Add(agentIdx);
}
}
}
// 辅助:移除 Agent 时清理索引
public void RemoveAgentIndex(int agentIdx)
{
lock (SyncRoot)
{
AgentIndices.Remove(agentIdx);
}
}
} }
private readonly ConcurrentDictionary<string, CrowdInstance> _crowdInstances = new(); private readonly ConcurrentDictionary<string, CrowdInstance> _crowdInstances = new();
private const float AGENT_RADIUS = 0.5f; //private const float AGENT_RADIUS = 0.5f;
private const float AGENT_HEIGHT = 2.0f; //private const float AGENT_HEIGHT = 2.0f;
// 新增Agent状态缓存实例ID -> AgentIdx -> 上一帧状态)
private readonly ConcurrentDictionary<string, ConcurrentDictionary<int, AgentState>> _agentLastState = new();
// 同步阈值(可配置)
private const float POSITION_THRESHOLD = 0.01f; // 位置变化超过0.01米才同步
private const float ROTATION_THRESHOLD = 0.017f; // 旋转变化超过1度0.017弧度)才同步
// Agent状态结构体
private struct AgentState
{
public Vector3 Position;
public float Rotation; // 绕Y轴旋转弧度
}
public SceneAgent(NavMeshManager navMeshManager) public SceneAgent(NavMeshManager navMeshManager)
{ {
_navMeshManager = navMeshManager; _navMeshManager = navMeshManager;
} }
public bool CreateCrowdForInstance(string instanceId) // ==========================================
// 修复4GetAgentsNeedSync 遍历 AgentIndices
// ==========================================
public List<AgentPositionSyncMsg> GetAgentsNeedSync(string instanceId)
{
List<AgentPositionSyncMsg> syncList = new();
if (!_crowdInstances.TryGetValue(instanceId, out var ci) || !_agentLastState.TryGetValue(instanceId, out var lastStates))
{
return syncList;
}
lock (ci.SyncRoot)
{
// 遍历实例内所有Agent索引修复AgentIndices报错
foreach (var agentIdx in ci.AgentIndices)
{
var agent = ci.Crowd.GetAgent(agentIdx);
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
{
continue;
}
// 获取当前状态
Vector3 currPos = GetAgentPosition(instanceId, agentIdx);
float currRot = GetAgentRotation(instanceId, agentIdx);
// 对比上一帧状态
if (lastStates.TryGetValue(agentIdx, out var lastState))
{
// 判断是否超过阈值
bool posChanged = Vector3.Distance(currPos, lastState.Position) > POSITION_THRESHOLD;
bool rotChanged = Math.Abs(currRot - lastState.Rotation) > ROTATION_THRESHOLD;
if (posChanged || rotChanged)
{
// 加入同步列表
syncList.Add(new AgentPositionSyncMsg
{
InstanceId = instanceId,
AgentIdx = agentIdx,
Position = currPos,
Rotation = currRot
});
// 更新缓存
lastStates[agentIdx] = new AgentState { Position = currPos, Rotation = currRot };
}
}
else
{
// 首次同步,直接加入并缓存
syncList.Add(new AgentPositionSyncMsg
{
InstanceId = instanceId,
AgentIdx = agentIdx,
Position = currPos,
Rotation = currRot
});
lastStates.TryAdd(agentIdx, new AgentState { Position = currPos, Rotation = currRot });
}
}
}
return syncList;
}
// ==========================================
// 修复2重写 GetAgentRotation通过速度计算朝向
// ==========================================
public float GetAgentRotation(string instanceId, int agentIdx)
{
if (!_crowdInstances.TryGetValue(instanceId, out var ci))
{
return 0f;
}
lock (ci.SyncRoot)
{
var agent = ci.Crowd.GetAgent(agentIdx);
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
{
return 0f;
}
// 核心逻辑通过Agent的速度向量计算绕Y轴的旋转角度弧度
// 速度向量 (vx, vz) -> 朝向角度 = atan2(vz, vx)
float vx = agent.vel.X;
float vz = -agent.vel.Z; // 和坐标转换一致Z轴取反
float rotation = 0f;
// 只有速度大于阈值时才计算朝向(避免静止时角度抖动)
if (Math.Abs(vx) > 0.001f || Math.Abs(vz) > 0.001f)
{
rotation = (float)Math.Atan2(vz, vx); // 计算弧度(范围 -π ~ π)
}
return rotation;
}
}
// 初始化实例的Agent状态缓存
public bool CreateCrowdForInstance(string instanceId, float maxAgentRadius = 0.1f)
{ {
// 使用 NavMeshManager 新增的 Helper 方法 // 使用 NavMeshManager 新增的 Helper 方法
if (!_navMeshManager.GetNavMeshAndQuery(instanceId, out var navMesh, out _)) if (!_navMeshManager.GetNavMeshAndQuery(instanceId, out var navMesh, out _))
@ -35,7 +183,7 @@ namespace XNet.Business
return false; return false;
} }
var config = new DtCrowdConfig(0.6f); var config = new DtCrowdConfig(maxAgentRadius);
var crowd = new DtCrowd(config, navMesh); var crowd = new DtCrowd(config, navMesh);
var paramsData = crowd.GetObstacleAvoidanceParams(0); var paramsData = crowd.GetObstacleAvoidanceParams(0);
@ -45,8 +193,13 @@ namespace XNet.Business
paramsData.adaptiveDepth = 2; paramsData.adaptiveDepth = 2;
crowd.SetObstacleAvoidanceParams(0, paramsData); crowd.SetObstacleAvoidanceParams(0, paramsData);
var instance = new CrowdInstance { Crowd = crowd };
return _crowdInstances.TryAdd(instanceId, instance); // 添加到实例字典
bool isAddCrowdOK = _crowdInstances.TryAdd(instanceId, new CrowdInstance(instanceId, crowd));
// 初始化状态缓存
bool isAddStateOK = _agentLastState.TryAdd(instanceId, new ConcurrentDictionary<int, AgentState>());
return isAddCrowdOK && isAddStateOK;
} }
public void RemoveCrowdInstance(string instanceId) public void RemoveCrowdInstance(string instanceId)
@ -56,27 +209,34 @@ namespace XNet.Business
public int AddAgent(string instanceId, Vector3 position, float radius, float height, float maxAcceleration = 8.0f, float maxSpeed = 3.5f) 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; if (!_crowdInstances.TryGetValue(instanceId, out var ci))
{
throw new ArgumentException($"实例 {instanceId} 不存在");
}
lock (ci.SyncRoot) lock (ci.SyncRoot)
{ {
var pos = new RcVec3f(position.X, position.Y, -position.Z); // 创建Agent原有逻辑
var ap = new DtCrowdAgentParams var agentParams = new DtCrowdAgentParams();
{ agentParams.radius = radius;
radius = radius, agentParams.height = height;
height = height, agentParams.maxAcceleration = 2.0f;
maxAcceleration = maxAcceleration, agentParams.maxSpeed = 3.0f;
maxSpeed = maxSpeed, agentParams.collisionQueryRange = radius * 8.0f;
collisionQueryRange = radius * 12.0f, agentParams.pathOptimizationRange = radius * 32.0f;
pathOptimizationRange = radius * 30.0f, agentParams.separationWeight = 1.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; var rcPos = new RcVec3f(position.X, position.Y, -position.Z); // 坐标转换(和之前一致)
int agentIdx = ci.Crowd.AddAgent(rcPos, agentParams).idx;
// 关键记录Agent索引到实例的AgentIndices
ci.AddAgentIndex(agentIdx);
// 初始化状态缓存
_agentLastState.GetOrAdd(instanceId, _ => new ConcurrentDictionary<int, AgentState>())
.TryAdd(agentIdx, new AgentState { Position = position, Rotation = 0f });
return agentIdx;
} }
} }
@ -97,14 +257,11 @@ namespace XNet.Business
public bool AgentGoto(string instanceId, int agentIdx, Vector3 destination) public bool AgentGoto(string instanceId, int agentIdx, Vector3 destination)
{ {
// 原有逻辑(无需修改)
if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return false; if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return false;
// 使用 NavMeshManager 获取 Query
if (!_navMeshManager.GetNavMeshAndQuery(instanceId, out _, out var query)) return false; if (!_navMeshManager.GetNavMeshAndQuery(instanceId, out _, out var query)) return false;
var targetPos = new RcVec3f(destination.X, destination.Y, -destination.Z); var targetPos = new RcVec3f(destination.X, destination.Y, -destination.Z);
// 使用 public 的 Extents 和 Filter
query.FindNearestPoly(targetPos, _navMeshManager.Extents, _navMeshManager.Filter, query.FindNearestPoly(targetPos, _navMeshManager.Extents, _navMeshManager.Filter,
out long targetRef, out var realTargetPos, out var _); out long targetRef, out var realTargetPos, out var _);
@ -113,7 +270,6 @@ namespace XNet.Business
lock (ci.SyncRoot) lock (ci.SyncRoot)
{ {
var agent = ci.Crowd.GetAgent(agentIdx); var agent = ci.Crowd.GetAgent(agentIdx);
// 【修复】检查 state
if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID) if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
{ {
return ci.Crowd.RequestMoveTarget(agent, targetRef, realTargetPos); return ci.Crowd.RequestMoveTarget(agent, targetRef, realTargetPos);
@ -122,21 +278,27 @@ namespace XNet.Business
return false; return false;
} }
public Vector3? GetAgentPosition(string instanceId, int agentIdx) // ==========================================
// 修复3GetAgentPosition 确保坐标转换一致
// ==========================================
public Vector3 GetAgentPosition(string instanceId, int agentIdx)
{ {
if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return null; if (!_crowdInstances.TryGetValue(instanceId, out var ci))
{
return Vector3.Zero;
}
lock (ci.SyncRoot) lock (ci.SyncRoot)
{ {
var agent = ci.Crowd.GetAgent(agentIdx); var agent = ci.Crowd.GetAgent(agentIdx);
// 【修复】检查 state if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
{ {
var p = agent.npos; return Vector3.Zero;
return new Vector3(p.X, p.Y, -p.Z);
} }
// 坐标转换和AddAgent时一致Z轴取反
return new Vector3(agent.npos.X, agent.npos.Y, -agent.npos.Z);
} }
return null;
} }
public void UpdateAll(float deltaTime) public void UpdateAll(float deltaTime)

View File

@ -0,0 +1,140 @@
using MessagePack;
using NanoidDotNet;
using System.Collections.Concurrent;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace XNet.Business
{
public class WsConnectionManager
{
// 存储所有活跃的WebSocket连接
private readonly ConcurrentDictionary<string, WebSocket> _connections = new();
// 存储实例ID -> 订阅该实例的连接ID列表
private readonly ConcurrentDictionary<string, HashSet<string>> _instanceSubscribers = new();
// 线程安全的随机数生成器生成连接ID
private readonly Random _random = new();
// 序列化选项(复用避免重复创建)
private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
// 新增WebSocket连接
public string AddConnection(WebSocket socket)
{
string connId = GenerateConnId();
_connections.TryAdd(connId, socket);
Console.WriteLine($"[WS] 新连接:{connId},当前连接数:{_connections.Count}");
return connId;
}
// 移除WebSocket连接清理订阅关系
public void RemoveConnection(string connId)
{
if (_connections.TryRemove(connId, out _))
{
// 清理该连接的所有订阅
foreach (var instanceId in _instanceSubscribers.Keys)
{
lock (_instanceSubscribers[instanceId])
{
_instanceSubscribers[instanceId].Remove(connId);
}
}
Console.WriteLine($"[WS] 连接断开:{connId},当前连接数:{_connections.Count}");
}
}
// 客户端订阅实例
public bool SubscribeInstance(string connId, string instanceId)
{
if (!_connections.ContainsKey(connId))
{
Console.WriteLine($"[WS] 订阅失败:连接 {connId} 不存在");
return false;
}
// 初始化实例的订阅列表(不存在则创建)
_instanceSubscribers.GetOrAdd(instanceId, _ => new HashSet<string>());
lock (_instanceSubscribers[instanceId])
{
_instanceSubscribers[instanceId].Add(connId);
}
Console.WriteLine($"[WS] 连接 {connId} 订阅实例 {instanceId}");
return true;
}
// 批量发送Agent位置同步消息异步不阻塞游戏循环
public async Task SendAgentPositionBatchAsync(List<AgentPositionSyncMsg> syncMsgs)
{
if (syncMsgs.Count == 0) return;
// 按实例ID分组减少重复发送
var groupedMsgs = syncMsgs.GroupBy(m => m.InstanceId);
foreach (var group in groupedMsgs)
{
string instanceId = group.Key;
if (!_instanceSubscribers.TryGetValue(instanceId, out var subscriberConnIds))
{
continue; // 无订阅者,跳过
}
// 序列化该实例的所有同步消息
byte[] jsonData = MessagePackSerializer.Serialize(group.ToList());
var wsMsg = new WsMessage
{
Type = WsMsgType.AgentPositionSync,
Data = jsonData
};
byte[] sendBytes = MessagePackSerializer.Serialize(wsMsg);
// 异步发送给所有订阅者(逐个发送,失败则清理连接)
List<string> deadConnIds = new();
lock (subscriberConnIds)
{
foreach (var connId in subscriberConnIds)
{
if (_connections.TryGetValue(connId, out var socket))
{
_ = SendToSingleConnAsync(socket, sendBytes, connId, deadConnIds);
}
}
}
// 清理失效连接
foreach (var deadConnId in deadConnIds)
{
RemoveConnection(deadConnId);
}
}
}
// 内部:发送消息给单个连接(异步)
private async Task SendToSingleConnAsync(WebSocket socket, byte[] data, string connId, List<string> deadConnIds)
{
try
{
if (socket.State == WebSocketState.Open)
{
await socket.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Text, true, CancellationToken.None);
}
else
{
deadConnIds.Add(connId);
}
}
catch (Exception ex)
{
Console.WriteLine($"[WS] 发送失败 {connId}{ex.Message}");
deadConnIds.Add(connId);
}
}
// 生成唯一连接ID
private string GenerateConnId()
{
return $"Conn_{Nanoid.Generate()}";// {DateTime.Now.Ticks}_{_random.Next(1000, 9999)}";
}
}
}

69
XNet.Business/WsServer.cs Normal file
View File

@ -0,0 +1,69 @@
using MessagePack;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace XNet.Business
{
public static class WsServer
{
// 启动WebSocket监听
public static void MapWebSocketServer(this WebApplication app, WsConnectionManager wsManager)
{
app.Map("/ws", async context =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
// 建立WebSocket连接
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
string connId = wsManager.AddConnection(webSocket);
// 接收客户端消息(订阅实例等)
var buffer = new byte[1024 * 4];
try
{
while (webSocket.State == WebSocketState.Open)
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client", CancellationToken.None);
break;
}
// 解析客户端消息
string msgStr = Encoding.UTF8.GetString(buffer, 0, result.Count);
byte[] data = new byte[result.Count];
Buffer.BlockCopy(buffer, 0, data, 0, result.Count);
var wsMsg = MessagePackSerializer.Deserialize<WsMessage>(data);
if (wsMsg == null) continue;
// 处理订阅实例请求
if (wsMsg.Type == WsMsgType.SubscribeInstance)
{
var subscribeReq = MessagePackSerializer.Deserialize<SubscribeInstanceReq>(wsMsg.Data);
if (subscribeReq != null)
{
wsManager.SubscribeInstance(connId, subscribeReq.InstanceId);
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[WS] 连接 {connId} 异常:{ex.Message}");
}
finally
{
wsManager.RemoveConnection(connId);
}
});
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
@ -9,8 +9,13 @@
<ItemGroup> <ItemGroup>
<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="MessagePack" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.3.6" />
<PackageReference Include="Microsoft.ClearScript.V8" Version="7.5.0" /> <PackageReference Include="Microsoft.ClearScript.V8" Version="7.5.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageReference Include="Nanoid" Version="3.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -13,11 +13,15 @@ builder.Services.AddOpenApi();
builder.Services.AddSingleton<NavMeshManager>(); builder.Services.AddSingleton<NavMeshManager>();
builder.Services.AddSingleton<SceneAgent>(); builder.Services.AddSingleton<SceneAgent>();
builder.Services.AddSingleton<WsConnectionManager>();
// 2. 注册 SimulationLoop 为 HostedService (后台运行) // 2. 注册 SimulationLoop 为 HostedService (后台运行)
builder.Services.AddHostedService<GameLoopService>(); builder.Services.AddHostedService<GameLoopService>();
var app = builder.Build(); var app = builder.Build();
// 启动WebSocket服务
app.MapWebSocketServer(app.Services.GetRequiredService<WsConnectionManager>());
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
{ {