`
This commit is contained in:
parent
1c233ddd5b
commit
d148681135
45
XNet.Business/Dto/SyncMsg.cs
Normal file
45
XNet.Business/Dto/SyncMsg.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -8,14 +8,16 @@ namespace XNet.Business
|
||||
{
|
||||
private readonly NavMeshManager _navMeshManager;
|
||||
private readonly SceneAgent _sceneAgent;
|
||||
private readonly WsConnectionManager _wsManager; // 新增WebSocket管理器
|
||||
|
||||
private const int TARGET_FPS = 30;
|
||||
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;
|
||||
_sceneAgent = sceneAgent;
|
||||
_wsManager = wsManager;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
@ -29,7 +31,7 @@ namespace XNet.Business
|
||||
|
||||
// 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();
|
||||
|
||||
// 注册 NavMesh 映射
|
||||
@ -72,11 +74,19 @@ namespace XNet.Business
|
||||
// 这行代码利用 SceneAgent 内部的 Parallel.ForEach,效率极高
|
||||
_sceneAgent.UpdateAll(deltaTime);
|
||||
|
||||
//var p = _sceneAgent.GetAgentPosition(instanceGuid_A, monsterA);
|
||||
//if (p != null)
|
||||
//{
|
||||
// Console.WriteLine(p);
|
||||
//}
|
||||
// 2. 收集所有需要同步的Agent状态
|
||||
List<AgentPositionSyncMsg> allSyncMsgs =
|
||||
[
|
||||
// 遍历所有实例(可从_navMeshManager获取实例列表,或SceneAgent维护)
|
||||
.. _sceneAgent.GetAgentsNeedSync(instanceGuid_A),
|
||||
.. _sceneAgent.GetAgentsNeedSync(instanceGuid_B),
|
||||
];
|
||||
|
||||
// 3. 异步发送WebSocket消息(不阻塞游戏循环)
|
||||
if (allSyncMsgs.Count > 0)
|
||||
{
|
||||
_ = _wsManager.SendAgentPositionBatchAsync(allSyncMsgs);
|
||||
}
|
||||
|
||||
// 帧率控制
|
||||
long frameWorkTime = stopwatch.ElapsedMilliseconds - currentTime;
|
||||
|
||||
12
XNet.Business/Properties/launchSettings.json
Normal file
12
XNet.Business/Properties/launchSettings.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"XNet.Business": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:52803;http://localhost:52804"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,21 +13,169 @@ namespace XNet.Business
|
||||
|
||||
private class CrowdInstance
|
||||
{
|
||||
public DtCrowd Crowd { get; set; } = null!;
|
||||
public object SyncRoot { get; } = new object();
|
||||
public string InstanceId { get; }
|
||||
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 const float AGENT_RADIUS = 0.5f;
|
||||
private const float AGENT_HEIGHT = 2.0f;
|
||||
//private const float AGENT_RADIUS = 0.5f;
|
||||
//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)
|
||||
{
|
||||
_navMeshManager = navMeshManager;
|
||||
}
|
||||
|
||||
public bool CreateCrowdForInstance(string instanceId)
|
||||
// ==========================================
|
||||
// 修复4:GetAgentsNeedSync 遍历 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 方法
|
||||
if (!_navMeshManager.GetNavMeshAndQuery(instanceId, out var navMesh, out _))
|
||||
@ -35,7 +183,7 @@ namespace XNet.Business
|
||||
return false;
|
||||
}
|
||||
|
||||
var config = new DtCrowdConfig(0.6f);
|
||||
var config = new DtCrowdConfig(maxAgentRadius);
|
||||
var crowd = new DtCrowd(config, navMesh);
|
||||
|
||||
var paramsData = crowd.GetObstacleAvoidanceParams(0);
|
||||
@ -45,8 +193,13 @@ namespace XNet.Business
|
||||
paramsData.adaptiveDepth = 2;
|
||||
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)
|
||||
@ -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)
|
||||
{
|
||||
if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return -1;
|
||||
if (!_crowdInstances.TryGetValue(instanceId, out var ci))
|
||||
{
|
||||
throw new ArgumentException($"实例 {instanceId} 不存在");
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
// 创建Agent(原有逻辑)
|
||||
var agentParams = new DtCrowdAgentParams();
|
||||
agentParams.radius = radius;
|
||||
agentParams.height = height;
|
||||
agentParams.maxAcceleration = 2.0f;
|
||||
agentParams.maxSpeed = 3.0f;
|
||||
agentParams.collisionQueryRange = radius * 8.0f;
|
||||
agentParams.pathOptimizationRange = radius * 32.0f;
|
||||
agentParams.separationWeight = 1.0f;
|
||||
|
||||
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)
|
||||
{
|
||||
// 原有逻辑(无需修改)
|
||||
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 _);
|
||||
|
||||
@ -113,7 +270,6 @@ namespace XNet.Business
|
||||
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);
|
||||
@ -122,21 +278,27 @@ namespace XNet.Business
|
||||
return false;
|
||||
}
|
||||
|
||||
public Vector3? GetAgentPosition(string instanceId, int agentIdx)
|
||||
// ==========================================
|
||||
// 修复3:GetAgentPosition 确保坐标转换一致
|
||||
// ==========================================
|
||||
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)
|
||||
{
|
||||
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 new Vector3(p.X, p.Y, -p.Z);
|
||||
return Vector3.Zero;
|
||||
}
|
||||
|
||||
// 坐标转换(和AddAgent时一致:Z轴取反)
|
||||
return new Vector3(agent.npos.X, agent.npos.Y, -agent.npos.Z);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void UpdateAll(float deltaTime)
|
||||
|
||||
140
XNet.Business/WsConnectionManager.cs
Normal file
140
XNet.Business/WsConnectionManager.cs
Normal 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
69
XNet.Business/WsServer.cs
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
@ -9,8 +9,13 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotRecast.Detour.Crowd" 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.Extensions.DependencyInjection" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||
<PackageReference Include="Nanoid" Version="3.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -13,11 +13,15 @@ builder.Services.AddOpenApi();
|
||||
builder.Services.AddSingleton<NavMeshManager>();
|
||||
builder.Services.AddSingleton<SceneAgent>();
|
||||
|
||||
builder.Services.AddSingleton<WsConnectionManager>();
|
||||
|
||||
// 2. 注册 SimulationLoop 为 HostedService (后台运行)
|
||||
builder.Services.AddHostedService<GameLoopService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// 启动WebSocket服务
|
||||
app.MapWebSocketServer(app.Services.GetRequiredService<WsConnectionManager>());
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user