From d148681135c08d8bf40d0d23ac684b69157ef655 Mon Sep 17 00:00:00 2001 From: wuyanchen <307378529@qq.com> Date: Thu, 25 Dec 2025 14:32:23 +0800 Subject: [PATCH] ` --- XNet.Business/Dto/SyncMsg.cs | 45 ++++ XNet.Business/GameLoopService.cs | 24 +- XNet.Business/Properties/launchSettings.json | 12 + XNet.Business/SceneAgent.cs | 236 ++++++++++++++++--- XNet.Business/WsConnectionManager.cs | 140 +++++++++++ XNet.Business/WsServer.cs | 69 ++++++ XNet.Business/XNet.Business.csproj | 7 +- XNet/Program.cs | 6 +- 8 files changed, 493 insertions(+), 46 deletions(-) create mode 100644 XNet.Business/Dto/SyncMsg.cs create mode 100644 XNet.Business/Properties/launchSettings.json create mode 100644 XNet.Business/WsConnectionManager.cs create mode 100644 XNet.Business/WsServer.cs diff --git a/XNet.Business/Dto/SyncMsg.cs b/XNet.Business/Dto/SyncMsg.cs new file mode 100644 index 0000000..99e9a42 --- /dev/null +++ b/XNet.Business/Dto/SyncMsg.cs @@ -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; + } +} \ No newline at end of file diff --git a/XNet.Business/GameLoopService.cs b/XNet.Business/GameLoopService.cs index cadf6d6..41df02c 100644 --- a/XNet.Business/GameLoopService.cs +++ b/XNet.Business/GameLoopService.cs @@ -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 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; diff --git a/XNet.Business/Properties/launchSettings.json b/XNet.Business/Properties/launchSettings.json new file mode 100644 index 0000000..315b7e1 --- /dev/null +++ b/XNet.Business/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "XNet.Business": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:52803;http://localhost:52804" + } + } +} \ No newline at end of file diff --git a/XNet.Business/SceneAgent.cs b/XNet.Business/SceneAgent.cs index 18a7a19..2c20544 100644 --- a/XNet.Business/SceneAgent.cs +++ b/XNet.Business/SceneAgent.cs @@ -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 AgentIndices { get; } = new List(); + + // 构造函数 + 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 _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> _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 GetAgentsNeedSync(string instanceId) + { + List 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()); + + 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()) + .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) diff --git a/XNet.Business/WsConnectionManager.cs b/XNet.Business/WsConnectionManager.cs new file mode 100644 index 0000000..fbbac1e --- /dev/null +++ b/XNet.Business/WsConnectionManager.cs @@ -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 _connections = new(); + // 存储:实例ID -> 订阅该实例的连接ID列表 + private readonly ConcurrentDictionary> _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()); + + lock (_instanceSubscribers[instanceId]) + { + _instanceSubscribers[instanceId].Add(connId); + } + Console.WriteLine($"[WS] 连接 {connId} 订阅实例 {instanceId}"); + return true; + } + + // 批量发送Agent位置同步消息(异步,不阻塞游戏循环) + public async Task SendAgentPositionBatchAsync(List 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 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 deadConnIds) + { + try + { + if (socket.State == WebSocketState.Open) + { + await socket.SendAsync(new ArraySegment(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)}"; + } + } +} \ No newline at end of file diff --git a/XNet.Business/WsServer.cs b/XNet.Business/WsServer.cs new file mode 100644 index 0000000..b063951 --- /dev/null +++ b/XNet.Business/WsServer.cs @@ -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(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(data); + + if (wsMsg == null) continue; + + // 处理订阅实例请求 + if (wsMsg.Type == WsMsgType.SubscribeInstance) + { + var subscribeReq = MessagePackSerializer.Deserialize(wsMsg.Data); + if (subscribeReq != null) + { + wsManager.SubscribeInstance(connId, subscribeReq.InstanceId); + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[WS] 连接 {connId} 异常:{ex.Message}"); + } + finally + { + wsManager.RemoveConnection(connId); + } + }); + } + } +} \ No newline at end of file diff --git a/XNet.Business/XNet.Business.csproj b/XNet.Business/XNet.Business.csproj index ab61126..cddbb56 100644 --- a/XNet.Business/XNet.Business.csproj +++ b/XNet.Business/XNet.Business.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -9,8 +9,13 @@ + + + + + diff --git a/XNet/Program.cs b/XNet/Program.cs index 46c8aae..4c0c337 100644 --- a/XNet/Program.cs +++ b/XNet/Program.cs @@ -13,11 +13,15 @@ builder.Services.AddOpenApi(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + // 2. ע SimulationLoop Ϊ HostedService (̨) builder.Services.AddHostedService(); - var app = builder.Build(); +// WebSocket +app.MapWebSocketServer(app.Services.GetRequiredService()); + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) {