MCP智能客服业务划分的架构设计与工程实践

核心内容摘要

导电阳极丝(CAF):原理、影响与应对策略
小程序异常监控实战:Sentry-mina集成指南

使用Xshell远程管理Qwen-Image-Edit-F2P模型服务器

前言NetcodeForEntites是Unity基于DOTs框架搭建的高性能网络框架相比NGO它除了可以容纳人数大于NGONGO支持十多人NFE支持上百人还有自己的预测回滚系统并支持回放系统等缺点是学习路径陡峭需要了解ECS系统且中文文档较少。

基础系统一个状态网络框架最基础的功能我认为有以下几点允许客户端接入将所需变量同步过去利用Rpc发送消息这些概念在NGO或虚幻的网络框架都有涉及允许客户端接入建立服务端与客户端世界在NFE中我们需要重写ClientServerBootstrap脚本来指定连接端口并连接[UnityEngine.Scripting.Preserve] public class GameBootstrap : ClientServerBootstrap { public override bool Initialize(string defaultWorldName) { AutoConnectPort 7979; return base.Initialize(defaultWorldName); } }这里能指定端口号同时创建两个World(服务端World与客户端World),World是ECS中的概念类似实体容器存储实体和系统等。

建立连接重写完这个脚本后我们需要建立连接因为我们只是创建了世界客户端和服务端名没有连接到一起。

建立连接主要依赖两个组件NetworkId与NetworkStreamInGame,NetworkId是每一个客户端世界中的唯一单例用于建立和服务端的连接而NetworkStreamInGame组件则是连接上的证明当客户端和服务端上NetworkId相同的实体上都有该组件则证明已经进入游戏。

至于客户端与服务端通信则需要通过Rpc组件即SendRpcCommandRequest和ReceiveRpcCommandRequest组件SendRpcCommandRequest的TargetConnection字段用于标记需要发信息的客户端通常我们放到带NetworkId的实体上而服务端通过ReceiveRpcCommandRequest的SourceConnection字段取到对应的客户端。

注意你在客户端创建Send之后会自动在服务端创建Receive具体流程如下找到客户端的NetworkId实体(以下简称客户端实体)-为它添加NetworkStreamInGame组件-创建一个实体为它加上SendRpcCommandRequest组件发Rpc,TargetConnection设为客户端实体-服务端一直监听带ReceiveRpcCommandRequest的实体-为ReceiveRpcCommandRequest的SourceConnection实体添加NetworkStreamInGame-销毁Rpc实体或组件防止多次触发具体代码如下//客户端请求进入游戏的命令这里用作标签也可以附加参数 public struct GoInGameRequest : IRpcCommand { }//客户端处理进入游戏的系统 [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation| WorldSystemFilterFlags.ThinClientSimulation)] partial struct GoInGameClientSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { //只在有NetworkId且没有NetworkStreamInGame组件的实体存在时更新 state.RequireForUpdateCubeSpawner(); var builder new EntityQueryBuilder(Allocator.Temp) .WithAllNetworkId() .WithNoneNetworkStreamInGame(); state.RequireForUpdate(state.GetEntityQuery(builder)); } [BurstCompile] public void OnUpdate(ref SystemState state) { var ecbnew EntityCommandBuffer(Allocator.Temp); //找到没有进入游戏的连接实体添加进入游戏组件并发送进入游戏请求 foreach (var(id,entity)in SystemAPI.QueryRefRONetworkId() .WithEntityAccess().WithNoneNetworkStreamInGame()) { ecb.AddComponentNetworkStreamInGame(entity); var reqecb.CreateEntity(); ecb.AddComponentGoInGameRequest(req); ecb.AddComponent(req,new SendRpcCommandRequest { TargetConnectionentity }); } ecb.Playback(state.EntityManager); } }//服务器处理进入游戏的系统 [BurstCompile] //只在服务器模拟世界中运行 [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] partial struct GoInGameServerSystem : ISystem { //用于从实体获取NetworkId组件 private ComponentLookupNetworkId networkIdFromEntity; [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdateCubeSpawner(); var builder new EntityQueryBuilder(Allocator.Temp) .WithAllGoInGameRequest() .WithAllReceiveRpcCommandRequest(); state.RequireForUpdate(state.GetEntityQuery(builder)); networkIdFromEntitystate.GetComponentLookupNetworkId(true); } public void OnUpdate(ref SystemState state) { var prefabSystemAPI.GetSingletonCubeSpawner().CubeToSpawner;//获取玩家实体 state.EntityManager.GetName(prefab,out var prefabName ); var worldNamestate.WorldUnmanaged.Name; var ecbnew EntityCommandBuffer(Allocator.Temp); networkIdFromEntity.Update(ref state); //处理所有收到进入游戏请求的实体 foreach (var(reqSrc,entity)in SystemAPI.QueryRefROReceiveRpcCommandRequest() .WithEntityAccess() .WithAllGoInGameRequest()) { //添加NetworkStreamInGame组件表示该连接已进入游戏状态 ecb.AddComponentNetworkStreamInGame(reqSrc.ValueRO.SourceConnection); var networkIdnetworkIdFromEntity[reqSrc.ValueRO.SourceConnection]; var playerecb.Instantiate(prefab); ecb.DestroyEntity(entity); } ecb.Playback(state.EntityManager); } }玩家的生成基本上在游戏中利于预制体生成玩家与ECS基本没有区别都是可以用ecb或EntityManager的Instantiate方法生成玩家不过在生成时我们需要对预制体进行处理为它添加GhostAuthoringComponent组件这会将它标记为在网络中同步的实体类似NGO的NetworkObject此外该组件还有一些变量可以设置比如将SupportedGhostModes改为预测(本地玩家)或插值(其他玩家)HasOwner(是否被某个玩家拥有)等当然一个可操作角色被创建出来既不会自动被玩家拥有也不会在断开连接时销毁我们只需要两行代码即可实现//设置GhostOwner组件以关联玩家和实体设置所有权 ecb.SetComponent(player,new GhostOwner() { NetworkIdnetworkId.Value}); //加入LinkedEntityGroup以确保传输整个实体及其子实体,断开连接时一并销毁 ecb.AppendToBuffer(reqSrc.ValueRO.SourceConnection,new LinkedEntityGroup{Valueplayer});将这两行代码添加到上面的服务器系统中的合适位置即可赋予玩家所有权和在退出游戏时销毁对应Ghost实体读取玩家的输入想要让玩家操作自己的角色主要分成两步用输入组件监听玩家按键输入用对应系统映射玩家输入并修改对应的值public struct PlayerInput : IInputComponentData { public float2 MoveInput; public InputEvent JumpEvent; public InputEvent FireEvent; }注意瞬时输入用InputEvent,按下时调用Set方法检查时用IsSet检查即可另外用一个输入系统监听玩家按键输入再用一个玩家移动系统改变玩家实体的位置(LocalTransform)即可基本上与ECS差不多唯一有区别的时你只能操作拥有的角色我们再上面展示了GhostOwner的创建当创建了这个组件在对应的客户端的实体上会加上GhostOwnerIsLocal的组件。

进阶部分零碎知识WorldSystemFilter时用来区分世界的添加到系统上用于区分只在客户端上执行的系统只在服务端上执行的系统。

各系统组执行顺序GhostInputSystemGroup → PredictedSimulationSystemGroup → ServerSimulation → PresentationSystemGroup第一个用于采集输入第二个用于预测第三个用于服务端做权威判断第四个用于处理表现层GhostField用于添加到需要同步的组件变量上同步服务端和客户端的值预测回滚系统预测回滚是为了处理玩家输入的延迟问题一般情况下只有服务端能够修改玩家的位置等权威变量而预测回滚是直接在客户端先预测玩家位置并修改随后服务端再计算一次如果差距不大就忽视否则就进行回滚即偷偷将变量修改成服务端算的值。

该系统需要依赖NFE的NetworkTime单例组件它其中有几个重要变量InputTargetTick,比当前服务端早几帧提前预测位置ServerTick服务端权威帧算出来的即为权威位置IsFirstTimeFullyPredictingTick,bool变量判断是否是第一次传送数据过来因为服务端可能在几帧内都传数据过来导致多次播放音效或动画当然既然需要预输入帧与权威帧进行比较我们当然需要一个容器来保存输入这时我们可以用ICommandData来存储,ECS会自动创建对应的缓冲区DynamicBuffer我们只需要添加或移除每帧数据即可。

下面展示一个简单的移动预测回滚所需组件如下//CommandData存储每帧的结果用于实现环形缓冲区预测和回滚 public struct MoveCommand : ICommandData { //必须实现 ICommandData 接口 public NetworkTick Tick { get;set; } public float2 MoveInput; public float3 PredictedPosition; //预测后的位置 public float3 PredictedVelocity; //预测后的速度 } public struct PlayerInput : IInputComponentData { public float2 MoveInput; public InputEvent JumpEvent; public InputEvent FireEvent; } //玩家历史输入组件用于记录之前的输入 public struct PlayerInputHistory :IBufferElementData { public NetworkTick Tick; //输入对应的网络Tick public float2 MoveInput; //移动输入 public InputEvent JumpEvent; public InputEvent FireEvent; } public struct PlayerPosition : IComponentData { [GhostField] public float3 Value; } public struct PlayerVelocity : IComponentData { //保留2位小数同步 [GhostField(Quantization

] public float3 Value; }之后是对应的系统[UpdateInGroup(typeof(GhostInputSystemGroup))] partial struct PlayerInputSystem : ISystem { public void OnUpdate(ref SystemState state) { var networkTime SystemAPI.GetSingletonNetworkTime(); var targetTick networkTime.InputTargetTick; foreach(var (historyBuffer,input) in SystemAPI.Query DynamicBufferPlayerInputHistory,RefRWPlayerInput() .WithAllGhostOwnerIsLocal()) { var moveInputnew float2 ( Input.GetAxis(Horizontal), Input.GetAxis(Vertical) ); bool isFirePressedInput.GetMouseButtonDown(

; bool isJumpPressedInput.GetKeyDown(KeyCode.Space); input.ValueRW.MoveInputmoveInput; if (isFirePressed) { input.ValueRW.FireEvent.Set(); } if (isJumpPressed) { input.ValueRW.JumpEvent.Set(); } PlayerInputHistory history new PlayerInputHistory() { Tick targetTick, MoveInput moveInput, }; if (isFirePressed) { history.FireEvent.Set(); } if (isJumpPressed) { history.JumpEvent.Set(); } historyBuffer.Add(history); } }这个系统做的事情很简单记录在InputTargetTick帧的输入并同时写入玩家输入与历史输入[UpdateInGroup(typeof(PredictedSimulationSystemGroup))] partial struct PlayerMovePredictSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdatePlayerPosition(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var networkTime SystemAPI.GetSingletonNetworkTime(); var currentTicknetworkTime.InputTargetTick;//预测的帧 var ecbSingletonSystemAPI.GetSingletonBeginSimulationEntityCommandBufferSystem.Singleton(); var ecbecbSingleton.CreateCommandBuffer(state.WorldUnmanaged); foreach(var (position,velocity,inputBuffer,commandBuffer,entity)in SystemAPI.Query RefRWPlayerPosition, RefRWPlayerVelocity, DynamicBufferPlayerInputHistory, DynamicBufferMoveCommand ().WithAllGhostOwnerIsLocal().WithEntityAccess()) { float2 currentInput float

zero; bool hasInput false; foreach(var input in inputBuffer) { if(input.TickcurrentTick) { currentInputinput.MoveInput; hasInputtrue; break; } } //没有找到对应的输入保持速度 float deltaTime SystemAPI.Time.DeltaTime; float speed 5f; float3 newVelocitynew float3(currentInput.x * speed,0,currentInput.y * speed); float3 newPositionposition.ValueRO.Value newVelocity * deltaTime; if(newPosition.y

{ newPosition.y0; } velocity.ValueRW.ValuenewVelocity; position.ValueRW.ValuenewPosition; StoreCommand(commandBuffer,currentTick,currentInput,newVelocity,newPosition); if(networkTime.IsFirstTimeFullyPredictingTickhasInput math.lengthsq(currentInput)

{ //第一次预测这个帧时才播放音效避免多次播放 PlayFootstepEffect(newPosition); } } } private void StoreCommand(DynamicBufferMoveCommand buffer, NetworkTick tick, float2 input, float3 velocity, float3 position) { for(int i0;ibuffer.Length;i) { if (buffer[i].Ticktick) { buffer[i]new MoveCommand { Ticktick, MoveInputinput, PredictedPositionposition, PredictedVelocityvelocity }; return; } } buffer.Add(new MoveCommand { Ticktick, MoveInputinput, PredictedPositionposition, PredictedVelocityvelocity }); //限制buffer长度 if (buffer.Length

{ buffer.RemoveAt(

; } } private void PlayFootstepEffect(float3 newPosition) { } }这个是预测系统主要检查在对应的TargetInputTick时有没有输入并将输入写入缓冲区同时限制缓冲区的大小,同时修改玩家的速度和位置等起到预测位置的作用[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] partial struct PlayerMoveServerSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdatePlayerInput(); // ✅ 同步来的输入 state.RequireForUpdateNetworkTime(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var networkTime SystemAPI.GetSingletonNetworkTime(); var currentTick networkTime.ServerTick; // 权威帧 float deltaTime SystemAPI.Time.DeltaTime; float speed 5f; // 查询 PlayerInputNetCode 同步的不是 PlayerInputHistory foreach (var (input, position, velocity) in SystemAPI.QueryRefROPlayerInput, RefRWPlayerPosition, RefRWPlayerVelocity() .WithAllGhostOwner()) { float2 currentInput input.ValueRO.MoveInput; // 权威模拟 float3 newVelocity new float3(currentInput.x * speed, 0, currentInput.y * speed); float3 newPosition position.ValueRO.Value newVelocity * deltaTime; // 简单地面碰撞 if (newPosition.y

newPosition.y 0; velocity.ValueRW.Value newVelocity; position.ValueRW.Value newPosition; } } }接下来是服务端的权威判断注意这里的时间是ServerTick,直接修改玩家的坐标带有GhostField的字段会平滑处理[UpdateInGroup(typeof(PredictedSimulationSystemGroup))] partial struct PlayerRollbackSystem : ISystem { private NetworkTick lastServerTick; [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdateNetworkTime(); state.RequireForUpdatePlayerPosition(); lastServerTick default; } [BurstCompile] public void OnUpdate(ref SystemState state) { var networkTime SystemAPI.GetSingletonNetworkTime(); var currentTick networkTime.ServerTick; // 检测是否发生了回滚tick倒退或正在重模拟 bool isRollingBack lastServerTick ! default !currentTick.IsNewerThan(lastServerTick); bool isResimulating !networkTime.IsFirstTimeFullyPredictingTick; // 只在回滚或重模拟时处理 if (!isRollingBack !isResimulating) { lastServerTick currentTick; return; } foreach (var (position, velocity, commandBuffer) in SystemAPI.QueryRefRWPlayerPosition, RefRWPlayerVelocity, DynamicBufferMoveCommand() .WithAllGhostOwnerIsLocal, PredictedGhost()) { // 查找当前tick的预测记录 MoveCommand? currentCommand null; for (int i 0; i commandBuffer.Length; i) { if (commandBuffer[i].Tick currentTick) { currentCommand commandBuffer[i]; break; } } // 如果没找到记录清理后续历史并跳过数据缺失 if (!currentCommand.HasValue) { CleanupFutureHistory(commandBuffer, currentTick); continue; } float3 serverPos position.ValueRO.Value; // NetCode已应用快照 float3 predictedPos currentCommand.Value.PredictedPosition; float error math.distance(predictedPos, serverPos); // 误差分级处理 if (error

1f) { // 小误差接受服务器值只需清理历史 CleanupFutureHistory(commandBuffer, currentTick); } else if (error

0f) { // 大误差瞬移并清空历史 position.ValueRW.Value serverPos; velocity.ValueRW.Value float

zero; commandBuffer.Clear(); } else { // 中等误差瞬移但保留有效历史 position.ValueRW.Value serverPos; CleanupFutureHistory(commandBuffer, currentTick); } } lastServerTick currentTick; } private void CleanupFutureHistory(DynamicBufferMoveCommand buffer, NetworkTick tick) { // 删除比当前tick更新的所有记录 for (int i buffer.Length - 1; i 0; i--) { if (buffer[i].Tick.IsNewerThan(tick)) { buffer.RemoveAt(i); } } } }之后是回滚系统将数据历史与服务端权威进行判断如果差距过大就自行插值修改//GhostInputSystemGroup → PredictedSimulationSystemGroup → ServerSimulation → PresentationSystemGroup //在Presentation阶段更新玩家视觉表现 [UpdateInGroup(typeof(PresentationSystemGroup))] partial struct PlayerVisualSystem : ISystem { public void OnUpdate(ref SystemState state) { var networkTime SystemAPI.GetSingletonNetworkTime(); //

远程玩家插值表现 foreach (var (position, ghostOwner) in SystemAPI.QueryRefROPlayerPosition, RefROGhostOwner() .WithNoneGhostOwnerIsLocal()) // 非本地玩家 { // 使用 GhostSnapshotSystem 自动插值的位置 // 或者手动实现平滑跟随 SmoothVisualUpdate(position.ValueRO.Value); } //

本地玩家预测表现需要平滑处理 foreach (var position in SystemAPI.QueryRefROPlayerPosition() .WithAllGhostOwnerIsLocal()) { // 使用预测位置但相机跟随需要平滑 SmoothCameraFollow(position.ValueRO.Value); } } private void SmoothCameraFollow(float3 value) { } private void SmoothVisualUpdate(float3 value) { } }最后的表现系统只有空壳你可以在这里根据PlayerPosition组件的值确定玩家的位置等结语NFE的功能强大但是学习曲线陡峭请酌情考虑

s8sp加密路线1rc.免费在线观看.中国-s8sp加密路线1rc.免费在线观看.中国应用

百度百家号客服电话人工服务

123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123