`
This commit is contained in:
parent
a2399cd5f0
commit
6b76b48cf8
@ -1,44 +1,167 @@
|
||||
using MessagePack;
|
||||
using XNet.Business.Dto;
|
||||
using XNet.Business.Net;
|
||||
|
||||
namespace XNet.Business
|
||||
{
|
||||
// WebSocket消息类型枚举
|
||||
public enum WsMsgType
|
||||
public enum WsMsgType : ushort
|
||||
{
|
||||
SubscribeInstance = 1, // 客户端订阅实例
|
||||
AgentPositionSync = 2 // 服务端推送Agent位置
|
||||
SUBSCRIBE_ROOM = 0x0000, // 客户端订阅实例
|
||||
/// <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]
|
||||
public class SubscribeInstanceReq
|
||||
{
|
||||
[Key("instanceId")]
|
||||
public string InstanceId { get; set; } = string.Empty;
|
||||
[Key("roomId")]
|
||||
public string RoomId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// Agent位置同步消息(服务端推送)
|
||||
[MessagePackObject]
|
||||
public class AgentPositionSyncMsg
|
||||
public class AgentLocationSyncMsg
|
||||
{
|
||||
[Key("instanceId")]
|
||||
public string InstanceId { get; set; } = string.Empty;
|
||||
[Key("agentIdx")]
|
||||
public int AgentIdx { get; set; }
|
||||
[Key("position")]
|
||||
public Vector3Msg Position { get; set; }
|
||||
public Vec3? Position { get; set; }
|
||||
[Key("rotation")]
|
||||
public float Rotation { get; set; } // 简化为绕Y轴旋转(弧度)
|
||||
public Vec3? Rotation { get; set; }
|
||||
}
|
||||
|
||||
// WebSocket通用消息包装
|
||||
/// <summary>
|
||||
/// 房间信息
|
||||
/// </summary>
|
||||
[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")]
|
||||
public WsMsgType Type { get; set; } = WsMsgType.SubscribeInstance;
|
||||
public WsMsgType Type { get; set; }
|
||||
[Key("senderId")]
|
||||
public string SenderId { get; set; } = string.Empty;
|
||||
[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;
|
||||
//}
|
||||
}
|
||||
@ -13,7 +13,6 @@ namespace XNet.Business
|
||||
|
||||
private const int TARGET_FPS = 30;
|
||||
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)
|
||||
{
|
||||
_navMeshManager = navMeshManager;
|
||||
@ -21,18 +20,27 @@ namespace XNet.Business
|
||||
_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);
|
||||
_instanceIds.Remove(instanceId);
|
||||
}
|
||||
List<RoomMsg<AgentLocationSyncMsg>> allSyncMsgs = [];
|
||||
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)
|
||||
{
|
||||
@ -49,16 +57,26 @@ namespace XNet.Business
|
||||
string instanceGuid_B = "Instance_TeamB_" + Guid.NewGuid();
|
||||
|
||||
// 注册 NavMesh 映射
|
||||
CreateInstance(instanceGuid_A, "Map_Forest");
|
||||
CreateInstance(instanceGuid_B, "Map_Forest"); // 复用同一份 NavMesh 内存
|
||||
_sceneAgent.CreateInstance(instanceGuid_A, "Map_Forest");
|
||||
_sceneAgent.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));
|
||||
//// 添加一些测试怪物
|
||||
//for(int i = 0; i < 1000; i++)
|
||||
//{
|
||||
// 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 ===");
|
||||
|
||||
@ -84,19 +102,7 @@ namespace XNet.Business
|
||||
|
||||
|
||||
// 2. 异步收集+推送消息(IO密集型,提交到专用线程池,不阻塞主循环)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
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);
|
||||
_ = Task.Run(SendSyncLocationMessage, stoppingToken);
|
||||
|
||||
|
||||
// 帧率控制
|
||||
|
||||
@ -8,16 +8,16 @@ namespace XNet.Business.Net
|
||||
/// <summary>
|
||||
/// 自定义 Agent 消息列表池化策略(重写 Create/Return)
|
||||
/// </summary>
|
||||
public sealed class SyncMsgListPolicy : DefaultPooledObjectPolicy<List<AgentPositionSyncMsg>>
|
||||
public sealed class SyncMsgListPolicy<T> : DefaultPooledObjectPolicy<List<T>>
|
||||
{
|
||||
// 重写 Create 方法:指定初始容量,减少扩容开销
|
||||
public override List<AgentPositionSyncMsg> Create()
|
||||
public override List<T> Create()
|
||||
{
|
||||
return new List<AgentPositionSyncMsg>(100); // 初始容量100
|
||||
return new List<T>(100); // 初始容量100
|
||||
}
|
||||
|
||||
// 重写 Return 方法:清空列表数据,保留容量
|
||||
public override bool Return(List<AgentPositionSyncMsg> obj)
|
||||
public override bool Return(List<T> obj)
|
||||
{
|
||||
obj.Clear(); // 关键:归还前清空数据,避免脏数据
|
||||
return true; // 返回true表示可复用
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using NanoidDotNet;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
using XNet.Business.PathNavigation;
|
||||
|
||||
namespace XNet.Business.Net
|
||||
{
|
||||
@ -12,22 +14,24 @@ namespace XNet.Business.Net
|
||||
// ========== 原有核心字段 ==========
|
||||
private readonly ConcurrentDictionary<string, WebSocket> _connections = 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<byte[]> _byteArrayPool;
|
||||
|
||||
private readonly SceneAgent _sceneAgent = null;
|
||||
|
||||
public WsConnectionManager()
|
||||
public WsConnectionManager(SceneAgent sceneAgent)
|
||||
{
|
||||
_sceneAgent = sceneAgent;
|
||||
// 配置对象池参数
|
||||
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);
|
||||
_byteArrayPool = new DefaultObjectPool<byte[]>(new ByteArrayPolicy(4096), maximumRetained);
|
||||
}
|
||||
@ -48,12 +52,18 @@ namespace XNet.Business.Net
|
||||
foreach (var instanceId in _instanceSubscribers.Keys)
|
||||
{
|
||||
_instanceSubscribers[instanceId].TryRemove(connId, out _);
|
||||
|
||||
if (_instanceSubscribers[instanceId].IsEmpty)
|
||||
{
|
||||
_sceneAgent.RemoveInstance(instanceId);
|
||||
_instanceSubscribers.TryRemove(instanceId, out _);
|
||||
}
|
||||
}
|
||||
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))
|
||||
{
|
||||
@ -61,99 +71,129 @@ namespace XNet.Business.Net
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
// 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
|
||||
{
|
||||
msgList.AddRange(group);
|
||||
|
||||
// 2. 序列化逻辑优化(完全复用池化数组)
|
||||
int msgBytesLength = 0; // 记录有效长度
|
||||
byte[]? msgBytes = null;
|
||||
// 2. 复用:从池获取字节数组(核心使用 _byteArrayPool)
|
||||
var pooledBytes = _byteArrayPool.Get();
|
||||
bool needReturnPooledBytes = false;
|
||||
try
|
||||
foreach (var connKv in subscriberConnIds)
|
||||
{
|
||||
using (var tempMs = new MemoryStream())
|
||||
if (_connections.TryGetValue(connKv.Key, out var socket))
|
||||
{
|
||||
MessagePackSerializer.Serialize(tempMs, msgList);
|
||||
msgBytesLength = (int)tempMs.Position;
|
||||
|
||||
if (msgBytesLength <= pooledBytes.Length)
|
||||
if (msgBytesLength != 0)
|
||||
{
|
||||
tempMs.Position = 0;
|
||||
tempMs.Read(pooledBytes, 0, msgBytesLength);
|
||||
msgBytes = pooledBytes; // 直接复用池化数组
|
||||
needReturnPooledBytes = true;
|
||||
// 3. 发送时传递有效长度
|
||||
_ = SendToSingleConnAsync(socket, msgBytes, msgBytesLength, connKv.Key, deadConnIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
msgBytes = tempMs.ToArray();// 超出池化数组长度,使用新分配的数组
|
||||
needReturnPooledBytes = false;
|
||||
// 3. 发送整串字节数组
|
||||
_ = SendToSingleConnAsync(socket, msgBytes, msgBytes.Length, connKv.Key, deadConnIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_byteArrayPool.Return(pooledBytes);
|
||||
}
|
||||
|
||||
// 封装WebSocket消息
|
||||
var wsMsg = new WsMessage { Type = WsMsgType.AgentPositionSync, Data = msgBytes };
|
||||
byte[] sendBytes = MessagePackSerializer.Serialize(wsMsg);
|
||||
|
||||
// 3. 复用:从池获取失效连接列表
|
||||
var deadConnIds = _deadConnListPool.Get();
|
||||
try
|
||||
// 清理失效连接
|
||||
foreach (var deadConnId in deadConnIds)
|
||||
{
|
||||
foreach (var connKv in subscriberConnIds)
|
||||
{
|
||||
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); // 归还,自动清空
|
||||
RemoveConnection(deadConnId);
|
||||
}
|
||||
}
|
||||
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. 定义带长度的发送方法
|
||||
@ -217,25 +257,4 @@ namespace XNet.Business.Net
|
||||
_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,5 @@
|
||||
using MessagePack;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace XNet.Business.Net
|
||||
{
|
||||
@ -38,17 +36,18 @@ namespace XNet.Business.Net
|
||||
//byte[] data = new byte[result.Count];
|
||||
//Buffer.BlockCopy(buffer, 0, data, 0, result.Count);
|
||||
//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.Type == WsMsgType.SubscribeInstance)
|
||||
if (wsMsg.Type == WsMsgType.CREATE_OR_JOIN_ROOM)
|
||||
{
|
||||
var subscribeReq = MessagePackSerializer.Deserialize<SubscribeInstanceReq>(wsMsg.Data);
|
||||
if (subscribeReq != null)
|
||||
{
|
||||
wsManager.SubscribeInstance(connId, subscribeReq.InstanceId);
|
||||
string createRoomId = string.Empty;
|
||||
wsManager.SubscribeInstance(connId, subscribeReq.RoomId, ref createRoomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,20 +120,20 @@ namespace XNet.Business.PathNavigation
|
||||
// 2. 副本实例管理
|
||||
// ==========================================
|
||||
|
||||
public bool CreateInstance(string instanceId, string templateId)
|
||||
public bool CreateInstance(string roomId, string templateId)
|
||||
{
|
||||
if (!_templates.ContainsKey(templateId))
|
||||
{
|
||||
Console.WriteLine($"[Error] 无法创建实例 {instanceId}, 资源 {templateId} 不存在。");
|
||||
Console.WriteLine($"[Error] 无法创建实例 {roomId}, 资源 {templateId} 不存在。");
|
||||
return false;
|
||||
}
|
||||
_instanceMap[instanceId] = templateId;
|
||||
_instanceMap[roomId] = templateId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RemoveInstance(string instanceId)
|
||||
public void RemoveInstance(string roomId)
|
||||
{
|
||||
_instanceMap.TryRemove(instanceId, out _);
|
||||
_instanceMap.TryRemove(roomId, out _);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
|
||||
@ -4,6 +4,7 @@ using DotRecast.Detour;
|
||||
using DotRecast.Detour.Crowd;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Numerics;
|
||||
using XNet.Business.Dto;
|
||||
|
||||
namespace XNet.Business.PathNavigation
|
||||
{
|
||||
@ -15,7 +16,7 @@ namespace XNet.Business.PathNavigation
|
||||
{
|
||||
public string InstanceId { get; }
|
||||
public DtCrowd Crowd { get; set; }
|
||||
//public object SyncRoot { get; } = new object(); // 线程锁
|
||||
public object SyncRoot { get; } = new object(); // 线程锁
|
||||
|
||||
// 新增:存储该实例下所有 Agent 的索引(关键修复)
|
||||
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 ROTATION_THRESHOLD = 0.017f; // 旋转变化超过1度(0.017弧度)才同步
|
||||
private readonly ConcurrentDictionary<string, bool> _instanceIds = new ConcurrentDictionary<string, bool>();
|
||||
|
||||
public ConcurrentDictionary<string, bool> InstanceIds => _instanceIds;
|
||||
|
||||
// Agent状态结构体
|
||||
private struct AgentState
|
||||
{
|
||||
public Vector3 Position;
|
||||
public float Rotation; // 绕Y轴旋转(弧度)
|
||||
public Vector3 Rotation; // 绕Y轴旋转(弧度)
|
||||
}
|
||||
|
||||
|
||||
@ -73,12 +77,28 @@ namespace XNet.Business.PathNavigation
|
||||
_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 _);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==========================================
|
||||
// 修复4:GetAgentsNeedSync 遍历 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))
|
||||
{
|
||||
return syncList;
|
||||
@ -98,24 +118,27 @@ namespace XNet.Business.PathNavigation
|
||||
|
||||
// 获取当前状态
|
||||
Vector3 currPos = GetAgentPosition(instanceId, agentIdx);
|
||||
float currRot = GetAgentRotation(instanceId, agentIdx);
|
||||
Vector3 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;
|
||||
bool rotChanged = Vector3.Distance(currRot, lastState.Rotation) > ROTATION_THRESHOLD;
|
||||
|
||||
if (posChanged || rotChanged)
|
||||
{
|
||||
// 加入同步列表
|
||||
syncList.Add(new AgentPositionSyncMsg
|
||||
syncList.Add(new RoomMsg<AgentLocationSyncMsg>
|
||||
{
|
||||
InstanceId = instanceId,
|
||||
AgentIdx = agentIdx,
|
||||
Position = new Vector3Msg { X = currPos.X, Y = currPos.Y, Z = currPos.Z },
|
||||
Rotation = currRot
|
||||
RoomId = instanceId,
|
||||
Data = new AgentLocationSyncMsg
|
||||
{
|
||||
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 };
|
||||
@ -124,12 +147,15 @@ namespace XNet.Business.PathNavigation
|
||||
else
|
||||
{
|
||||
// 首次同步,直接加入并缓存
|
||||
syncList.Add(new AgentPositionSyncMsg
|
||||
syncList.Add(new RoomMsg<AgentLocationSyncMsg>
|
||||
{
|
||||
InstanceId = instanceId,
|
||||
AgentIdx = agentIdx,
|
||||
Position = new Vector3Msg { X = currPos.X, Y = currPos.Y, Z = currPos.Z },
|
||||
Rotation = currRot
|
||||
RoomId = instanceId,
|
||||
Data = new AgentLocationSyncMsg
|
||||
{
|
||||
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 });
|
||||
}
|
||||
@ -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))
|
||||
{
|
||||
return 0f;
|
||||
return new Vector3(0, 0, 0);
|
||||
}
|
||||
|
||||
//lock (ci.SyncRoot)
|
||||
//{
|
||||
var agent = ci.Crowd.GetAgent(agentIdx);
|
||||
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
||||
lock (ci.SyncRoot)
|
||||
{
|
||||
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} 不存在");
|
||||
}
|
||||
|
||||
//lock (ci.SyncRoot)
|
||||
//{
|
||||
// 创建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;
|
||||
lock (ci.SyncRoot)
|
||||
{
|
||||
// 创建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;
|
||||
|
||||
var rcPos = new RcVec3f(position.X, position.Y, -position.Z); // 坐标转换(和之前一致)
|
||||
int agentIdx = ci.Crowd.AddAgent(rcPos, agentParams).idx;
|
||||
var rcPos = new RcVec3f(position.X, position.Y, -position.Z); // 坐标转换(和之前一致)
|
||||
int agentIdx = ci.Crowd.AddAgent(rcPos, agentParams).idx;
|
||||
|
||||
// 关键:记录Agent索引到实例的AgentIndices
|
||||
ci.AddAgentIndex(agentIdx);
|
||||
// 关键:记录Agent索引到实例的AgentIndices
|
||||
ci.AddAgentIndex(agentIdx);
|
||||
|
||||
// 初始化状态缓存
|
||||
_agentLastState.GetOrAdd(instanceId, _ => new ConcurrentDictionary<int, AgentState>())
|
||||
.TryAdd(agentIdx, new AgentState { Position = position, Rotation = 0f });
|
||||
// 初始化状态缓存
|
||||
_agentLastState.GetOrAdd(instanceId, _ => new ConcurrentDictionary<int, AgentState>())
|
||||
.TryAdd(agentIdx, new AgentState { Position = position, Rotation = new Vector3(0, 0, 0) });
|
||||
|
||||
return agentIdx;
|
||||
//}
|
||||
return agentIdx;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
lock (ci.SyncRoot)
|
||||
{
|
||||
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)
|
||||
@ -268,14 +305,14 @@ namespace XNet.Business.PathNavigation
|
||||
|
||||
if (targetRef == 0) return false;
|
||||
|
||||
//lock (ci.SyncRoot)
|
||||
//{
|
||||
var agent = ci.Crowd.GetAgent(agentIdx);
|
||||
if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
||||
lock (ci.SyncRoot)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@ -289,17 +326,17 @@ namespace XNet.Business.PathNavigation
|
||||
return Vector3.Zero;
|
||||
}
|
||||
|
||||
//lock (ci.SyncRoot)
|
||||
//{
|
||||
var agent = ci.Crowd.GetAgent(agentIdx);
|
||||
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
||||
lock (ci.SyncRoot)
|
||||
{
|
||||
return Vector3.Zero;
|
||||
}
|
||||
var agent = ci.Crowd.GetAgent(agentIdx);
|
||||
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
||||
{
|
||||
return Vector3.Zero;
|
||||
}
|
||||
|
||||
// 坐标转换(和AddAgent时一致:Z轴取反)
|
||||
return new Vector3(agent.npos.X, agent.npos.Y, -agent.npos.Z);
|
||||
//}
|
||||
// 坐标转换(和AddAgent时一致:Z轴取反)
|
||||
return new Vector3(agent.npos.X, agent.npos.Y, -agent.npos.Z);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateAll(float deltaTime)
|
||||
@ -309,10 +346,10 @@ namespace XNet.Business.PathNavigation
|
||||
MaxDegreeOfParallelism = Environment.ProcessorCount // 限制并行数=CPU核心数,避免过载
|
||||
}, ci =>
|
||||
{
|
||||
//lock (ci.SyncRoot)
|
||||
//{
|
||||
ci.Crowd.Update(deltaTime, null);
|
||||
//}
|
||||
lock (ci.SyncRoot)
|
||||
{
|
||||
ci.Crowd.Update(deltaTime, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Numerics;
|
||||
using XNet.Business.Manager;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using XNet.Business.PathNavigation;
|
||||
|
||||
namespace XNet.Api.Controllers
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user