This commit is contained in:
wuyanchen 2025-12-29 17:24:47 +08:00
parent a2399cd5f0
commit 6b76b48cf8
8 changed files with 416 additions and 234 deletions

View File

@ -1,44 +1,167 @@
using MessagePack; using MessagePack;
using XNet.Business.Dto;
using XNet.Business.Net; using XNet.Business.Net;
namespace XNet.Business namespace XNet.Business
{ {
// WebSocket消息类型枚举 // WebSocket消息类型枚举
public enum WsMsgType public enum WsMsgType : ushort
{ {
SubscribeInstance = 1, // 客户端订阅实例 SUBSCRIBE_ROOM = 0x0000, // 客户端订阅实例
AgentPositionSync = 2 // 服务端推送Agent位置 /// <summary>
/// 登录
/// </summary>
LOGIN = 0x0001,
/// <summary>
/// 房间群发信息
/// </summary>
ROOM_MSG = 0x0002,
/// <summary>
/// 房间群发信息,自己不接收
/// </summary>
ROOM_MSG_OTHER = 0x0003,
/// <summary>
/// 用户下线
/// </summary>
OFFLINE = 0x0004,
/// <summary>
/// 用户做主机
/// </summary>
HOST = 0x0005,
/// <summary>
/// 本局游戏结束
/// </summary>
GAME_END = 0x0006,
/// <summary>
/// 心跳包请求
/// </summary>
HEART_BEAT = 0x0007,
/// <summary>
/// 心跳包响应
/// </summary>
HEART_BEAT_REPLY = 0x0008,
/// <summary>
/// 加入或者创建房间
/// </summary>
CREATE_OR_JOIN_ROOM = 0x0009,
/// <summary>
/// 个人私有的消息
/// </summary>
PRIVATGE = 0x0010,
/// <summary>
/// 添加AI玩家
/// </summary>
ADD_AI_PLAYER = 0x0011,
/// <summary>
/// 删除所有AI
/// </summary>
DELETE_ALL_AI_PLAYER = 0x0012,
/// <summary>
/// 发送给自己的消息
/// </summary>
TO_SELF = 0x0013,
/// <summary>
/// 公开或者关闭房间
/// </summary>
ENABLE_ROOM_PUBLIC = 0x0014,
/// <summary>
/// 启用或者关闭AI
/// </summary>
ENABLE_AI = 0x0015,
/// <summary>
/// 游戏将要结束,改变倒计时文字为闪烁或红色
/// </summary>
GAME_WILLOVER = 0x0016,
/// <summary>
/// 本局游戏将要结束
/// </summary>
GAME_WILL_END = 0x0017,
/// <summary>
/// Agent位置同步消息
/// </summary>
AGENT_POSITION_SYNC = 0x0018,
/// <summary>
/// Agent角度同步消息
/// </summary>
AGENT_ROTATION_SYNC = 0x0019,
} }
// 客户端订阅实例的请求消息 // 客户端订阅实例的请求消息
[MessagePackObject] [MessagePackObject]
public class SubscribeInstanceReq public class SubscribeInstanceReq
{ {
[Key("instanceId")] [Key("roomId")]
public string InstanceId { get; set; } = string.Empty; public string RoomId { get; set; } = string.Empty;
} }
// Agent位置同步消息服务端推送 // Agent位置同步消息服务端推送
[MessagePackObject] [MessagePackObject]
public class AgentPositionSyncMsg public class AgentLocationSyncMsg
{ {
[Key("instanceId")]
public string InstanceId { get; set; } = string.Empty;
[Key("agentIdx")] [Key("agentIdx")]
public int AgentIdx { get; set; } public int AgentIdx { get; set; }
[Key("position")] [Key("position")]
public Vector3Msg Position { get; set; } public Vec3? Position { get; set; }
[Key("rotation")] [Key("rotation")]
public float Rotation { get; set; } // 简化为绕Y轴旋转弧度 public Vec3? Rotation { get; set; }
} }
// WebSocket通用消息包装 /// <summary>
/// 房间信息
/// </summary>
[MessagePackObject] [MessagePackObject]
public class WsMessage public class RoomMsg<T>
{
[Key("roomId")]
public string RoomId { get; set; } = string.Empty;
[Key("type")]
public WsMsgType Type { get; set; }
[Key("senderId")]
public string SenderId { get; set; } = string.Empty;
[Key("data")]
public T? Data { get; set; } = default;
}
/// <summary>
/// 消息基类
/// </summary>
[MessagePackObject]
public class BaseMsg
{ {
[Key("type")] [Key("type")]
public WsMsgType Type { get; set; } = WsMsgType.SubscribeInstance; public WsMsgType Type { get; set; }
[Key("senderId")]
public string SenderId { get; set; } = string.Empty;
[Key("data")] [Key("data")]
public byte[] Data { get; set; } = []; public byte[]? Data { get; set; } = default;
} }
//// WebSocket通用消息包装
//[MessagePackObject]
//public class WsMessage<T>
//{
// [Key("type")]
// public WsMsgType Type { get; set; }
// [Key("data")]
// public T? Data { get; set; } = default;
//}
} }

View File

@ -13,7 +13,6 @@ namespace XNet.Business
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;
private readonly List<string> _instanceIds = new List<string>();
public GameLoopService(NavMeshManager navMeshManager, SceneAgent sceneAgent, WsConnectionManager wsManager) public GameLoopService(NavMeshManager navMeshManager, SceneAgent sceneAgent, WsConnectionManager wsManager)
{ {
_navMeshManager = navMeshManager; _navMeshManager = navMeshManager;
@ -21,18 +20,27 @@ namespace XNet.Business
_wsManager = wsManager; _wsManager = wsManager;
} }
public void CreateInstance(string instanceId, string mapId)
{
_navMeshManager.CreateInstance(instanceId, mapId);
_instanceIds.Add(instanceId);
}
public void RemoveInstance(string instanceId) private async Task SendSyncLocationMessage()
{ {
_navMeshManager.RemoveInstance(instanceId); List<RoomMsg<AgentLocationSyncMsg>> allSyncMsgs = [];
_instanceIds.Remove(instanceId); foreach (var instanceIdKv in _sceneAgent.InstanceIds)
} {
var syncMsgs = _sceneAgent.GetAgentsNeedSync(instanceIdKv.Key);
////Debug 输出查看
//foreach (var msg in syncMsgs)
//{
// Console.WriteLine($"[Sync] Instance: {instanceIdKv.Key}, AgentIdx: {msg.Data!.AgentIdx}, Pos: {msg.Data.Position?.X},{msg.Data.Position?.Y},{msg.Data.Position?.Z}");
//}
allSyncMsgs.AddRange(syncMsgs);
}
if (allSyncMsgs.Count > 0)
{
await _wsManager.SendMessageToRoomBatchAsync(allSyncMsgs);
}
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
@ -49,16 +57,26 @@ namespace XNet.Business
string instanceGuid_B = "Instance_TeamB_" + Guid.NewGuid(); string instanceGuid_B = "Instance_TeamB_" + Guid.NewGuid();
// 注册 NavMesh 映射 // 注册 NavMesh 映射
CreateInstance(instanceGuid_A, "Map_Forest"); _sceneAgent.CreateInstance(instanceGuid_A, "Map_Forest");
CreateInstance(instanceGuid_B, "Map_Forest"); // 复用同一份 NavMesh 内存 _sceneAgent.CreateInstance(instanceGuid_B, "Map_Forest"); // 复用同一份 NavMesh 内存
// 为这两个副本创建独立的物理/避障模拟器 // 为这两个副本创建独立的物理/避障模拟器
_sceneAgent.CreateCrowdForInstance(instanceGuid_A); _sceneAgent.CreateCrowdForInstance(instanceGuid_A);
_sceneAgent.CreateCrowdForInstance(instanceGuid_B); _sceneAgent.CreateCrowdForInstance(instanceGuid_B);
// 添加一些测试怪物 //// 添加一些测试怪物
int monsterA = _sceneAgent.AddAgent(instanceGuid_A, new Vector3(-4, 0, -4), 0.05f, 1); //for(int i = 0; i < 1000; i++)
_sceneAgent.AgentGoto(instanceGuid_A, monsterA, new Vector3(5, 0, 3)); //{
// int monsterA = _sceneAgent.AddAgent(instanceGuid_A, new Vector3(-4, 0, -4), 0.05f, 1);
// _sceneAgent.AgentGoto(instanceGuid_A, monsterA, new Vector3(5, 0, 3));
//}
//// 添加一些测试怪物
//for (int i = 0; i < 9000; i++)
//{
// int monsterB = _sceneAgent.AddAgent(instanceGuid_B, new Vector3(-4, 0, -4), 0.05f, 1);
// _sceneAgent.AgentGoto(instanceGuid_B, monsterB, new Vector3(5, 0, 3));
//}
Console.WriteLine("=== Server Game Loop Started ==="); Console.WriteLine("=== Server Game Loop Started ===");
@ -84,19 +102,7 @@ namespace XNet.Business
// 2. 异步收集+推送消息IO密集型提交到专用线程池不阻塞主循环 // 2. 异步收集+推送消息IO密集型提交到专用线程池不阻塞主循环
_ = Task.Run(async () => _ = Task.Run(SendSyncLocationMessage, stoppingToken);
{
List<AgentPositionSyncMsg> allSyncMsgs = [];
foreach (var instanceId in _instanceIds)
{
var syncMsgs = _sceneAgent.GetAgentsNeedSync(instanceId);
allSyncMsgs.AddRange(syncMsgs);
}
if (allSyncMsgs.Count > 0)
{
await _wsManager.SendAgentPositionBatchAsync(allSyncMsgs);
}
}, stoppingToken);
// 帧率控制 // 帧率控制

View File

@ -8,16 +8,16 @@ namespace XNet.Business.Net
/// <summary> /// <summary>
/// 自定义 Agent 消息列表池化策略(重写 Create/Return /// 自定义 Agent 消息列表池化策略(重写 Create/Return
/// </summary> /// </summary>
public sealed class SyncMsgListPolicy : DefaultPooledObjectPolicy<List<AgentPositionSyncMsg>> public sealed class SyncMsgListPolicy<T> : DefaultPooledObjectPolicy<List<T>>
{ {
// 重写 Create 方法:指定初始容量,减少扩容开销 // 重写 Create 方法:指定初始容量,减少扩容开销
public override List<AgentPositionSyncMsg> Create() public override List<T> Create()
{ {
return new List<AgentPositionSyncMsg>(100); // 初始容量100 return new List<T>(100); // 初始容量100
} }
// 重写 Return 方法:清空列表数据,保留容量 // 重写 Return 方法:清空列表数据,保留容量
public override bool Return(List<AgentPositionSyncMsg> obj) public override bool Return(List<T> obj)
{ {
obj.Clear(); // 关键:归还前清空数据,避免脏数据 obj.Clear(); // 关键:归还前清空数据,避免脏数据
return true; // 返回true表示可复用 return true; // 返回true表示可复用

View File

@ -2,8 +2,10 @@
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using NanoidDotNet; using NanoidDotNet;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text.Json; using System.Text.Json;
using XNet.Business.PathNavigation;
namespace XNet.Business.Net namespace XNet.Business.Net
{ {
@ -12,22 +14,24 @@ namespace XNet.Business.Net
// ========== 原有核心字段 ========== // ========== 原有核心字段 ==========
private readonly ConcurrentDictionary<string, WebSocket> _connections = new(); private readonly ConcurrentDictionary<string, WebSocket> _connections = new();
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, bool>> _instanceSubscribers = new(); private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, bool>> _instanceSubscribers = new();
private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; //private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
// ========== 新增:对象池配置 ========== // ========== 新增:对象池配置 ==========
// 对象池(使用自定义重写的策略) // 对象池(使用自定义重写的策略)
private readonly ObjectPool<List<AgentPositionSyncMsg>> _syncMsgListPool; //private readonly ObjectPool<List<RoomMsg>> _syncMsgListPool;
private readonly ObjectPool<List<string>> _deadConnListPool; private readonly ObjectPool<List<string>> _deadConnListPool;
private readonly ObjectPool<byte[]> _byteArrayPool; private readonly ObjectPool<byte[]> _byteArrayPool;
private readonly SceneAgent _sceneAgent = null;
public WsConnectionManager() public WsConnectionManager(SceneAgent sceneAgent)
{ {
_sceneAgent = sceneAgent;
// 配置对象池参数 // 配置对象池参数
var maximumRetained = 200; var maximumRetained = 200;
// 初始化对象池(传入自定义策略) // 初始化对象池(传入自定义策略)
_syncMsgListPool = new DefaultObjectPool<List<AgentPositionSyncMsg>>(new SyncMsgListPolicy(), maximumRetained); //_syncMsgListPool = new DefaultObjectPool<List<RoomMsg>>(new SyncMsgListPolicy<RoomMsg>(), maximumRetained);
_deadConnListPool = new DefaultObjectPool<List<string>>(new DeadConnListPolicy(), maximumRetained); _deadConnListPool = new DefaultObjectPool<List<string>>(new DeadConnListPolicy(), maximumRetained);
_byteArrayPool = new DefaultObjectPool<byte[]>(new ByteArrayPolicy(4096), maximumRetained); _byteArrayPool = new DefaultObjectPool<byte[]>(new ByteArrayPolicy(4096), maximumRetained);
} }
@ -48,12 +52,18 @@ namespace XNet.Business.Net
foreach (var instanceId in _instanceSubscribers.Keys) foreach (var instanceId in _instanceSubscribers.Keys)
{ {
_instanceSubscribers[instanceId].TryRemove(connId, out _); _instanceSubscribers[instanceId].TryRemove(connId, out _);
if (_instanceSubscribers[instanceId].IsEmpty)
{
_sceneAgent.RemoveInstance(instanceId);
_instanceSubscribers.TryRemove(instanceId, out _);
}
} }
Console.WriteLine($"[WS .NET 10] 连接断开:{connId},当前连接数:{_connections.Count}"); Console.WriteLine($"[WS .NET 10] 连接断开:{connId},当前连接数:{_connections.Count}");
} }
} }
public bool SubscribeInstance(string connId, string instanceId) public bool SubscribeInstance(string connId, string mapKey, ref string roomId)
{ {
if (!_connections.ContainsKey(connId)) if (!_connections.ContainsKey(connId))
{ {
@ -61,99 +71,129 @@ namespace XNet.Business.Net
return false; return false;
} }
_instanceSubscribers.GetOrAdd(instanceId, _ => new ConcurrentDictionary<string, bool>()); //roomId为空时新创建一个唯一房间ID
if (string.IsNullOrWhiteSpace(roomId))
{
roomId = Nanoid.Generate();
}
_instanceSubscribers[instanceId].TryAdd(connId, true);
bool isNewInstance = false;
_instanceSubscribers.GetOrAdd(roomId, (key) =>
{
isNewInstance = true;
return new ConcurrentDictionary<string, bool>();
});
_instanceSubscribers[roomId].TryAdd(connId, true);
if (isNewInstance)
{
_sceneAgent.CreateInstance(roomId, mapKey);
}
return true; return true;
} }
public async Task SendAgentPositionBatchAsync(List<AgentPositionSyncMsg> syncMsgs) public bool HasSubscribeInstance(string instanceId)
{
if (_instanceSubscribers.ContainsKey(instanceId))
{
return true;
}
return false;
}
public async Task SendMessageToRoomBatchAsync<T>(List<RoomMsg<T>> syncMsgs)
{ {
if (syncMsgs.Count == 0) return; if (syncMsgs.Count == 0) return;
foreach (var group in syncMsgs.GroupBy(m => m.InstanceId)) foreach (var group in syncMsgs.GroupBy(m => m.RoomId))
{ {
if (!_instanceSubscribers.TryGetValue(group.Key, out var subscriberConnIds)) continue; if (!_instanceSubscribers.TryGetValue(group.Key, out var subscriberConnIds)) continue;
// 1. 复用:从池获取消息列表 // 1. 复用:从池获取消息列表
var msgList = _syncMsgListPool.Get(); //var msgList = _syncMsgListPool.Get();
//try
//{
//msgList.AddRange(group);
//int msgBytesLength = CreateSerializeMessage(group.ToList(), out byte[] msgBytes);
// 封装WebSocket消息
int msgBytesLength = CreateSerializeMessage(group.ToList(), out byte[] msgBytes);
// 3. 复用:从池获取失效连接列表
var deadConnIds = _deadConnListPool.Get();
try try
{ {
msgList.AddRange(group); foreach (var connKv in subscriberConnIds)
// 2. 序列化逻辑优化(完全复用池化数组)
int msgBytesLength = 0; // 记录有效长度
byte[]? msgBytes = null;
// 2. 复用:从池获取字节数组(核心使用 _byteArrayPool
var pooledBytes = _byteArrayPool.Get();
bool needReturnPooledBytes = false;
try
{ {
using (var tempMs = new MemoryStream()) if (_connections.TryGetValue(connKv.Key, out var socket))
{ {
MessagePackSerializer.Serialize(tempMs, msgList); if (msgBytesLength != 0)
msgBytesLength = (int)tempMs.Position;
if (msgBytesLength <= pooledBytes.Length)
{ {
tempMs.Position = 0; // 3. 发送时传递有效长度
tempMs.Read(pooledBytes, 0, msgBytesLength); _ = SendToSingleConnAsync(socket, msgBytes, msgBytesLength, connKv.Key, deadConnIds);
msgBytes = pooledBytes; // 直接复用池化数组
needReturnPooledBytes = true;
} }
else else
{ {
msgBytes = tempMs.ToArray();// 超出池化数组长度,使用新分配的数组 // 3. 发送整串字节数组
needReturnPooledBytes = false; _ = SendToSingleConnAsync(socket, msgBytes, msgBytes.Length, connKv.Key, deadConnIds);
} }
} }
} }
finally
{
_byteArrayPool.Return(pooledBytes);
}
// 封装WebSocket消息 // 清理失效连接
var wsMsg = new WsMessage { Type = WsMsgType.AgentPositionSync, Data = msgBytes }; foreach (var deadConnId in deadConnIds)
byte[] sendBytes = MessagePackSerializer.Serialize(wsMsg);
// 3. 复用:从池获取失效连接列表
var deadConnIds = _deadConnListPool.Get();
try
{ {
foreach (var connKv in subscriberConnIds) RemoveConnection(deadConnId);
{
if (_connections.TryGetValue(connKv.Key, out var socket))
{
if (needReturnPooledBytes)
{
// 3. 发送时传递有效长度
_ = SendToSingleConnAsync(socket, msgBytes, msgBytesLength, connKv.Key, deadConnIds);
}
else
{
// 3. 发送整串字节数组
_ = SendToSingleConnAsync(socket, msgBytes, msgBytes.Length, connKv.Key, deadConnIds);
}
}
}
// 清理失效连接
foreach (var deadConnId in deadConnIds)
{
RemoveConnection(deadConnId);
}
}
finally
{
_deadConnListPool.Return(deadConnIds); // 归还,自动清空
} }
} }
finally finally
{ {
_syncMsgListPool.Return(msgList); // 归还,自动清空 _deadConnListPool.Return(deadConnIds); // 归还,自动清空
}
//}
//finally
//{
// _syncMsgListPool.Return(msgList); // 归还,自动清空
//}
}
}
private int CreateSerializeMessage<T>(T msg, out byte[] msgBytes)
{
// 2. 序列化逻辑优化(完全复用池化数组)
int msgBytesLength = 0; // 记录有效长度
// 2. 复用:从池获取字节数组(核心使用 _byteArrayPool
var pooledBytes = _byteArrayPool.Get();
try
{
using (var tempMs = new MemoryStream())
{
MessagePackSerializer.Serialize(tempMs, msg);
msgBytesLength = (int)tempMs.Position;
if (msgBytesLength <= pooledBytes.Length)
{
tempMs.Position = 0;
tempMs.Read(pooledBytes, 0, msgBytesLength);
msgBytes = pooledBytes; // 直接复用池化数组
}
else
{
msgBytes = tempMs.ToArray();// 超出池化数组长度,使用新分配的数组
msgBytesLength = 0;
}
} }
} }
finally
{
_byteArrayPool.Return(pooledBytes);
}
return msgBytesLength;
} }
// 1. 定义带长度的发送方法 // 1. 定义带长度的发送方法
@ -217,25 +257,4 @@ namespace XNet.Business.Net
_instanceSubscribers.Clear(); _instanceSubscribers.Clear();
} }
} }
// 补充 Vector3 的 MessagePack 序列化(否则会序列化失败)
[MessagePackObject]
public struct Vector3Msg
{
[Key("x")]
public float X { get; set; }
[Key("y")]
public float Y { get; set; }
[Key("z")]
public float Z { get; set; }
public Vector3Msg(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
}
} }

View File

@ -1,7 +1,5 @@
using MessagePack; using MessagePack;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace XNet.Business.Net namespace XNet.Business.Net
{ {
@ -38,17 +36,18 @@ namespace XNet.Business.Net
//byte[] data = new byte[result.Count]; //byte[] data = new byte[result.Count];
//Buffer.BlockCopy(buffer, 0, data, 0, result.Count); //Buffer.BlockCopy(buffer, 0, data, 0, result.Count);
//new ArraySegment<byte>(data, 0, dataLength) //new ArraySegment<byte>(data, 0, dataLength)
var wsMsg = MessagePackSerializer.Deserialize<WsMessage>(new ArraySegment<byte>(buffer, 0, result.Count)); var wsMsg = MessagePackSerializer.Deserialize<BaseMsg>(new ArraySegment<byte>(buffer, 0, result.Count));
if (wsMsg == null) continue; if (wsMsg == null) continue;
// 处理订阅实例请求 // 处理订阅实例请求
if (wsMsg.Type == WsMsgType.SubscribeInstance) if (wsMsg.Type == WsMsgType.CREATE_OR_JOIN_ROOM)
{ {
var subscribeReq = MessagePackSerializer.Deserialize<SubscribeInstanceReq>(wsMsg.Data); var subscribeReq = MessagePackSerializer.Deserialize<SubscribeInstanceReq>(wsMsg.Data);
if (subscribeReq != null) if (subscribeReq != null)
{ {
wsManager.SubscribeInstance(connId, subscribeReq.InstanceId); string createRoomId = string.Empty;
wsManager.SubscribeInstance(connId, subscribeReq.RoomId, ref createRoomId);
} }
} }
} }

View File

@ -120,20 +120,20 @@ namespace XNet.Business.PathNavigation
// 2. 副本实例管理 // 2. 副本实例管理
// ========================================== // ==========================================
public bool CreateInstance(string instanceId, string templateId) public bool CreateInstance(string roomId, string templateId)
{ {
if (!_templates.ContainsKey(templateId)) if (!_templates.ContainsKey(templateId))
{ {
Console.WriteLine($"[Error] 无法创建实例 {instanceId}, 资源 {templateId} 不存在。"); Console.WriteLine($"[Error] 无法创建实例 {roomId}, 资源 {templateId} 不存在。");
return false; return false;
} }
_instanceMap[instanceId] = templateId; _instanceMap[roomId] = templateId;
return true; return true;
} }
public void RemoveInstance(string instanceId) public void RemoveInstance(string roomId)
{ {
_instanceMap.TryRemove(instanceId, out _); _instanceMap.TryRemove(roomId, out _);
} }
// ========================================== // ==========================================

View File

@ -4,6 +4,7 @@ using DotRecast.Detour;
using DotRecast.Detour.Crowd; using DotRecast.Detour.Crowd;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Numerics; using System.Numerics;
using XNet.Business.Dto;
namespace XNet.Business.PathNavigation namespace XNet.Business.PathNavigation
{ {
@ -15,7 +16,7 @@ namespace XNet.Business.PathNavigation
{ {
public string InstanceId { get; } public string InstanceId { get; }
public DtCrowd Crowd { get; set; } public DtCrowd Crowd { get; set; }
//public object SyncRoot { get; } = new object(); // 线程锁 public object SyncRoot { get; } = new object(); // 线程锁
// 新增:存储该实例下所有 Agent 的索引(关键修复) // 新增:存储该实例下所有 Agent 的索引(关键修复)
public ConcurrentDictionary<int, bool> AgentIndices { get; } = new ConcurrentDictionary<int, bool>(); public ConcurrentDictionary<int, bool> AgentIndices { get; } = new ConcurrentDictionary<int, bool>();
@ -59,12 +60,15 @@ namespace XNet.Business.PathNavigation
// 同步阈值(可配置) // 同步阈值(可配置)
private const float POSITION_THRESHOLD = 0.01f; // 位置变化超过0.01米才同步 private const float POSITION_THRESHOLD = 0.01f; // 位置变化超过0.01米才同步
private const float ROTATION_THRESHOLD = 0.017f; // 旋转变化超过1度0.017弧度)才同步 private const float ROTATION_THRESHOLD = 0.017f; // 旋转变化超过1度0.017弧度)才同步
private readonly ConcurrentDictionary<string, bool> _instanceIds = new ConcurrentDictionary<string, bool>();
public ConcurrentDictionary<string, bool> InstanceIds => _instanceIds;
// Agent状态结构体 // Agent状态结构体
private struct AgentState private struct AgentState
{ {
public Vector3 Position; public Vector3 Position;
public float Rotation; // 绕Y轴旋转弧度 public Vector3 Rotation; // 绕Y轴旋转弧度
} }
@ -73,12 +77,28 @@ namespace XNet.Business.PathNavigation
_navMeshManager = navMeshManager; _navMeshManager = navMeshManager;
} }
public void CreateInstance(string roomId, string mapId)
{
_navMeshManager.CreateInstance(roomId, mapId);
InstanceIds.TryAdd(roomId, true);
}
public void RemoveInstance(string roomId)
{
_navMeshManager.RemoveInstance(roomId);
InstanceIds.TryRemove(roomId, out _);
}
// ========================================== // ==========================================
// 修复4GetAgentsNeedSync 遍历 AgentIndices // 修复4GetAgentsNeedSync 遍历 AgentIndices
// ========================================== // ==========================================
public List<AgentPositionSyncMsg> GetAgentsNeedSync(string instanceId) public List<RoomMsg<AgentLocationSyncMsg>> GetAgentsNeedSync(string instanceId)
{ {
List<AgentPositionSyncMsg> syncList = new(); List<RoomMsg<AgentLocationSyncMsg>> syncList = new();
if (!_crowdInstances.TryGetValue(instanceId, out var ci) || !_agentLastState.TryGetValue(instanceId, out var lastStates)) if (!_crowdInstances.TryGetValue(instanceId, out var ci) || !_agentLastState.TryGetValue(instanceId, out var lastStates))
{ {
return syncList; return syncList;
@ -98,24 +118,27 @@ namespace XNet.Business.PathNavigation
// 获取当前状态 // 获取当前状态
Vector3 currPos = GetAgentPosition(instanceId, agentIdx); Vector3 currPos = GetAgentPosition(instanceId, agentIdx);
float currRot = GetAgentRotation(instanceId, agentIdx); Vector3 currRot = GetAgentRotation(instanceId, agentIdx);
// 对比上一帧状态 // 对比上一帧状态
if (lastStates.TryGetValue(agentIdx, out var lastState)) if (lastStates.TryGetValue(agentIdx, out var lastState))
{ {
// 判断是否超过阈值 // 判断是否超过阈值
bool posChanged = Vector3.Distance(currPos, lastState.Position) > POSITION_THRESHOLD; bool posChanged = Vector3.Distance(currPos, lastState.Position) > POSITION_THRESHOLD;
bool rotChanged = Math.Abs(currRot - lastState.Rotation) > ROTATION_THRESHOLD; bool rotChanged = Vector3.Distance(currRot, lastState.Rotation) > ROTATION_THRESHOLD;
if (posChanged || rotChanged) if (posChanged || rotChanged)
{ {
// 加入同步列表 // 加入同步列表
syncList.Add(new AgentPositionSyncMsg syncList.Add(new RoomMsg<AgentLocationSyncMsg>
{ {
InstanceId = instanceId, RoomId = instanceId,
AgentIdx = agentIdx, Data = new AgentLocationSyncMsg
Position = new Vector3Msg { X = currPos.X, Y = currPos.Y, Z = currPos.Z }, {
Rotation = currRot AgentIdx = agentIdx,
Position = new Vec3 { X = currPos.X, Y = currPos.Y, Z = currPos.Z },
Rotation = new Vec3 { X = currRot.X, Y = currRot.Y, Z = currRot.Z },
}
}); });
// 更新缓存 // 更新缓存
lastStates[agentIdx] = new AgentState { Position = currPos, Rotation = currRot }; lastStates[agentIdx] = new AgentState { Position = currPos, Rotation = currRot };
@ -124,12 +147,15 @@ namespace XNet.Business.PathNavigation
else else
{ {
// 首次同步,直接加入并缓存 // 首次同步,直接加入并缓存
syncList.Add(new AgentPositionSyncMsg syncList.Add(new RoomMsg<AgentLocationSyncMsg>
{ {
InstanceId = instanceId, RoomId = instanceId,
AgentIdx = agentIdx, Data = new AgentLocationSyncMsg
Position = new Vector3Msg { X = currPos.X, Y = currPos.Y, Z = currPos.Z }, {
Rotation = currRot AgentIdx = agentIdx,
Position = new Vec3 { X = currPos.X, Y = currPos.Y, Z = currPos.Z },
Rotation = new Vec3 { X = currRot.X, Y = currRot.Y, Z = currRot.Z },
}
}); });
lastStates.TryAdd(agentIdx, new AgentState { Position = currPos, Rotation = currRot }); lastStates.TryAdd(agentIdx, new AgentState { Position = currPos, Rotation = currRot });
} }
@ -140,37 +166,48 @@ namespace XNet.Business.PathNavigation
} }
// =============================================== // ===============================================
// 修复2重写 GetAgentRotation通过速度计算朝向 // 完整版通过速度计算【完整三维欧拉角】X=俯仰 Y=偏航 Z=翻滚 全部齐全
// =============================================== // ===============================================
public float GetAgentRotation(string instanceId, int agentIdx) public Vector3 GetAgentRotation(string instanceId, int agentIdx)
{ {
if (!_crowdInstances.TryGetValue(instanceId, out var ci)) if (!_crowdInstances.TryGetValue(instanceId, out var ci))
{ {
return 0f; return new Vector3(0, 0, 0);
} }
//lock (ci.SyncRoot) lock (ci.SyncRoot)
//{
var agent = ci.Crowd.GetAgent(agentIdx);
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
{ {
return 0f; var agent = ci.Crowd.GetAgent(agentIdx);
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
{
return new Vector3(0, 0, 0);
}
float vx = agent.vel.X;
float vy = agent.vel.Y; // 垂直速度-上下移动
float vz = -agent.vel.Z; // 坐标转换保留,正确无需修改
float pitch = 0f; // X轴俯仰角-抬头低头
float yaw = 0f; // Y轴偏航角-左右转向
float roll = 0f; // ✅ 新增Z轴 翻滚角-左侧倾/右侧倾
const float VelocityThreshold = 0.001f;
// 三维速度防抖:任意轴速度达标,才计算所有旋转角
if (Math.Abs(vx) > VelocityThreshold || Math.Abs(vy) > VelocityThreshold || Math.Abs(vz) > VelocityThreshold)
{
// 1. 计算 Y轴 偏航角 (左右转向) - 原有逻辑不变
yaw = (float)Math.Atan2(vz, vx);
// 2. 计算 X轴 俯仰角 (抬头低头) - 原有逻辑不变
float horizontalSpeed = (float)Math.Sqrt(vx * vx + vz * vz);
pitch = (float)Math.Atan2(vy, horizontalSpeed);
// ✅ ✅ ✅ 核心新增:计算 Z轴 翻滚角 (侧身翻滚) 【推荐公式】
roll = (float)Math.Atan2(vx, vz);
}
// ✅ 完整三维欧拉角返回X俯仰、Y偏航、Z翻滚 全部赋值
return new Vector3(pitch, yaw, roll);
} }
// 核心逻辑通过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;
//}
} }
@ -215,45 +252,45 @@ namespace XNet.Business.PathNavigation
throw new ArgumentException($"实例 {instanceId} 不存在"); throw new ArgumentException($"实例 {instanceId} 不存在");
} }
//lock (ci.SyncRoot) lock (ci.SyncRoot)
//{ {
// 创建Agent原有逻辑 // 创建Agent原有逻辑
var agentParams = new DtCrowdAgentParams(); var agentParams = new DtCrowdAgentParams();
agentParams.radius = radius; agentParams.radius = radius;
agentParams.height = height; agentParams.height = height;
agentParams.maxAcceleration = 2.0f; agentParams.maxAcceleration = 2.0f;
agentParams.maxSpeed = 3.0f; agentParams.maxSpeed = 3.0f;
agentParams.collisionQueryRange = radius * 8.0f; agentParams.collisionQueryRange = radius * 8.0f;
agentParams.pathOptimizationRange = radius * 32.0f; agentParams.pathOptimizationRange = radius * 32.0f;
agentParams.separationWeight = 1.0f; agentParams.separationWeight = 1.0f;
var rcPos = new RcVec3f(position.X, position.Y, -position.Z); // 坐标转换(和之前一致) var rcPos = new RcVec3f(position.X, position.Y, -position.Z); // 坐标转换(和之前一致)
int agentIdx = ci.Crowd.AddAgent(rcPos, agentParams).idx; int agentIdx = ci.Crowd.AddAgent(rcPos, agentParams).idx;
// 关键记录Agent索引到实例的AgentIndices // 关键记录Agent索引到实例的AgentIndices
ci.AddAgentIndex(agentIdx); ci.AddAgentIndex(agentIdx);
// 初始化状态缓存 // 初始化状态缓存
_agentLastState.GetOrAdd(instanceId, _ => new ConcurrentDictionary<int, AgentState>()) _agentLastState.GetOrAdd(instanceId, _ => new ConcurrentDictionary<int, AgentState>())
.TryAdd(agentIdx, new AgentState { Position = position, Rotation = 0f }); .TryAdd(agentIdx, new AgentState { Position = position, Rotation = new Vector3(0, 0, 0) });
return agentIdx; return agentIdx;
//} }
} }
public void RemoveAgent(string instanceId, int agentIdx) public void RemoveAgent(string instanceId, int agentIdx)
{ {
if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return; if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return;
//lock (ci.SyncRoot) 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); 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) public bool AgentGoto(string instanceId, int agentIdx, Vector3 destination)
@ -268,14 +305,14 @@ namespace XNet.Business.PathNavigation
if (targetRef == 0) return false; if (targetRef == 0) return false;
//lock (ci.SyncRoot) lock (ci.SyncRoot)
//{
var agent = ci.Crowd.GetAgent(agentIdx);
if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
{ {
return ci.Crowd.RequestMoveTarget(agent, targetRef, realTargetPos); var agent = ci.Crowd.GetAgent(agentIdx);
if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
{
return ci.Crowd.RequestMoveTarget(agent, targetRef, realTargetPos);
}
} }
//}
return false; return false;
} }
@ -289,17 +326,17 @@ namespace XNet.Business.PathNavigation
return Vector3.Zero; return Vector3.Zero;
} }
//lock (ci.SyncRoot) lock (ci.SyncRoot)
//{
var agent = ci.Crowd.GetAgent(agentIdx);
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
{ {
return Vector3.Zero; var agent = ci.Crowd.GetAgent(agentIdx);
} if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
{
return Vector3.Zero;
}
// 坐标转换和AddAgent时一致Z轴取反 // 坐标转换和AddAgent时一致Z轴取反
return new Vector3(agent.npos.X, agent.npos.Y, -agent.npos.Z); return new Vector3(agent.npos.X, agent.npos.Y, -agent.npos.Z);
//} }
} }
public void UpdateAll(float deltaTime) public void UpdateAll(float deltaTime)
@ -309,10 +346,10 @@ namespace XNet.Business.PathNavigation
MaxDegreeOfParallelism = Environment.ProcessorCount // 限制并行数=CPU核心数避免过载 MaxDegreeOfParallelism = Environment.ProcessorCount // 限制并行数=CPU核心数避免过载
}, ci => }, ci =>
{ {
//lock (ci.SyncRoot) lock (ci.SyncRoot)
//{ {
ci.Crowd.Update(deltaTime, null); ci.Crowd.Update(deltaTime, null);
//} }
}); });
} }
} }

View File

@ -1,7 +1,5 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc; using XNet.Business.PathNavigation;
using System.Numerics;
using XNet.Business.Manager;
namespace XNet.Api.Controllers namespace XNet.Api.Controllers
{ {