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 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;
//}
}

View File

@ -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);
// 帧率控制

View File

@ -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表示可复用

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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 _);
}
// ==========================================

View File

@ -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 _);
}
// ==========================================
// 修复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))
{
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);
}
});
}
}

View File

@ -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
{