local KCPTransport = {} local KCPNetConfig = { isStream = false; nodelay = 1; interval = 20; resend = 2; nc = 1; sndwnd = 32; rcvwnd = 32; mtu = 1024; minrto = 10; timeOut = 20000; } local private = { checkConnectTime = 0, -- 控制检测kcp是否连接的频率 onlinePlayerDic = {}, -- { [frame]=playerids,} 特记:在战斗内断线后,存储了玩家指令,也要存贮这个值(或者不要删除这个值) frame2CmdCountDic = {}, -- 统计frame有多少个cmd(目前只统计军团战共12分钟,此dic的总量是14400个) playerActiveDic = {}, --第n帧有哪些玩家需要active {[frame] = {playerID ,playerID } ,... } frameDataListCursor = 0, -- private.frameDataList 的有效值游标,<=它的值都有效 。缓存用的 } local EventSystem = require('Event/EventSystem') local LuaUpdater = require "Main/LuaUpdater" local Command = require "Battle/Logic/MainLoop/Command" local TriangleMath = require"Common/TriangleMath" local table_insert=table.insert local TablePool = require('Infrast/TablePool') local FrameDataPropertyCount = 8 -- cmd中的成员变量个数 local playerIDList_pool = TablePool( function(list) return list or {} end, function(list) for k, v in pairs(list) do list[k] = nil end end, 16, 16 ) --- 设置基础的kcp配置 (等三更包后才能用) ---BattleExt.SetKCPNetConfig(KCPNetConfig.isStream, KCPNetConfig.nodelay, KCPNetConfig.interval, KCPNetConfig.resend, KCPNetConfig.nc, --- KCPNetConfig.sndwnd, KCPNetConfig.rcvwnd, KCPNetConfig.mtu, KCPNetConfig.minrto, KCPNetConfig.timeOut) --新版本的回调函数的参数playerIds和frameDatas是作为buffer复用的,不可以持有他们的引用,需要将数据读取出来自行保存 function private.OnFrameCallback(frameNum, playerIdsBuffer, frameDatasBuffer) local playerIds = nil local frameDatas = nil if GameAppInfo.IsVersionGreaterThan113100() then playerIds = playerIDList_pool.TryGet() for i = 1, playerIdsBuffer[0] do playerIds[i] = playerIdsBuffer[i] end local newFrame = private.frameDataList[private.frameDataListCursor + 1] if not newFrame then frameDatas = {} newFrame = {FrameNum = frameNum,PlayerIds = playerIds,Framedatas = frameDatas} private.frameDataList[private.frameDataListCursor + 1] = newFrame else newFrame.FrameNum = frameNum newFrame.PlayerIds = playerIds frameDatas = newFrame.Framedatas end local length = frameDatasBuffer[0] * FrameDataPropertyCount for i = 1, length do frameDatas[i] = frameDatasBuffer[i] end private.frameDataListCursor = private.frameDataListCursor + 1 else playerIds = playerIdsBuffer frameDatas = frameDatasBuffer local newFrame = private.frameDataList[private.frameDataListCursor + 1] if not newFrame then newFrame = {FrameNum = frameNum,PlayerIds = playerIds,Framedatas = frameDatas} private.frameDataList[private.frameDataListCursor + 1] = newFrame else newFrame.FrameNum = frameNum newFrame.PlayerIds = playerIds -- 这个playerIds 也是个table 是否需要对象池? newFrame.Framedatas = frameDatas -- 这个frameDatas 也是个table 是否需要对象池? end private.frameDataListCursor = private.frameDataListCursor + 1 end end function private.Startup(newBattle) BattleExt.SetLuaKcpFrameCB(private.OnFrameCallback) private.frameDataList = {} -- 一次从服务器拿到的帧数据 private.frameDataListCursor = 0 -- private.frameDataList 的有效值游标,<=它的值都有效 。缓存用的 private.curServerFrame = 0 private.checkConnectTime = 0 if newBattle then private.onlinePlayerDic = {} -- { [frame]=playerids,} private.frame2CmdCountDic = {} private.playerActiveDic = {} -- 第n帧有哪些玩家需要active {[frame] = {playerID ,playerID } ,... } end end function private.Shutdown() BattleExt.SetLuaKcpFrameCB(nil) private.frameDataList = nil private.curServerFrame = 0 --private.onlinePlayerDic = nil 不清理原因:断网会closeKCP,会调用到这里,如果清理则会导致丢失数据 --private.playerActiveDic = nil 不清理原因:断网会closeKCP,会调用到这里,如果清理则会导致丢失数据 private.frameDataListCursor = 0 end ------------------ C - S ----------------- --绑定获取帧数据的回调函数 function KCPTransport.BindFrameCallBack() private.Startup(true) end function KCPTransport.UnBindFrameCallBack() BattleExt.SetLuaKcpFrameCB(nil) end --连接kcp服务器 function KCPTransport.Connect(ip ,port ,roomId ,playerId ,IsFirst) CTL.BattleAgent.TryStopPingAgent() -- 防止新的kcp被其他功能的kcp意外关闭 (目前只有这个战斗代理服) private.Startup(IsFirst) BattleExt.Connect(ip ,port ,roomId ,playerId) private.connectCheckTimer = 0 LuaUpdater.StartRealTimeUpdate(KCPTransport.CheckIsConnectSuccess) end -- 发送每帧的操作数据 function KCPTransport.SendC2BFrame(skillIndex , specialCommand ,moveType, moveDirAngle, useJoyStick ,skillJoystickPosX,skillJoystickPosZ) --movex movez 是整数 BattleExt.SendC2BFrameLua(skillIndex , specialCommand ,moveType ,moveDirAngle,useJoyStick ,skillJoystickPosX,skillJoystickPosZ) end --发送结束战斗 function KCPTransport.SendBattleEnd() BattleExt.SendBattleEnd() end --发送 ready 信号 function KCPTransport.SendReady() BattleExt.SendReady() end --发送关闭信号 (这个函数执行后 一定要有 KCPTransport.ClearKCPData() ) function KCPTransport.Close() BattleExt.Close() LuaUpdater.StopRealTimeUpdate(KCPTransport.CheckIsConnectSuccess) end ------------- S - C -------------- --没用| 服务器的信号类的处理(不能正常战斗) 1, 房间不可用, 2 服务器内部错误 function KCPTransport.OnB2CMsg(errorID) if errorID == 1 then KCPTransport.Close() logging.error(LTAG.ERROR, "房间不可用") elseif errorID == 2 then KCPTransport.Close() logging.error(LTAG.ERROR, "服务器内部错误") end end --没用| 接收 CmdB2CBattleBegin function KCPTransport.OnB2CBattleBegin(randoms) logging.error(LTAG.ERROR, "服务器不可能发这个协议 :CmdB2CBattleBegin") end -- 接收 每帧的数据 function KCPTransport.GetB2CFrameList(commandDic, isDealAll) --从缓存中拿去数据 BattleExt.LuaGetB2CFrameList() -- 存储在private.frameDataList中 --处理数据 private.SetSyncCommandDir(commandDic, isDealAll) end local OnceHandleToCmd = LogicUpdateManager.GetMaxFastPlayFrameNum() -- 一次最多转换cmd x个, function private.SetSyncCommandDir(commandDic, isDealAll) -- commandDic = 人-帧-所有命令 local count = private.frameDataListCursor if not isDealAll and not USE_LUA_STANDALONE and GameAppInfo.IsVersionGreaterThan113100() then if count > OnceHandleToCmd then count = OnceHandleToCmd end end for i= 1, count do local framedata = private.frameDataList[i] local frameNum = framedata.FrameNum --帧号,就是当前是第几帧的帧号 local playerIds = framedata.PlayerIds -- 数组第一个数为0,则表示都不在线;数组长度为0表示无变化 local frameDatas = framedata.Framedatas SaveSyncBattleInfoManager.TrySaveB2CFrame(frameNum, playerIds, frameDatas) --在线数据存储 private.onlinePlayerDic[frameNum] = playerIds if LevelManager.NeedStatisticFrameCmdLevel() then -- 目前只在军团战+大乱斗统计 if GameAppInfo.IsVersionGreaterThan113100() then private.frame2CmdCountDic[frameNum] = #frameDatas / 8 else private.frame2CmdCountDic[frameNum] = #frameDatas end end if GameAppInfo.IsVersionGreaterThan113100() then --处理帧命令数据 local frameCount = #frameDatas / 8 for j = 1, frameCount do local index = (j - 1) * 8 local playerId = frameDatas[index + 1] local command = private.ChangeCmdNotifyToCommand(frameDatas,frameNum ,playerId, index) local frameCommandDic = commandDic[playerId] if not frameCommandDic then frameCommandDic = {} --备注:这地方由于同步的人不会太多,所以这个不会频繁创建 commandDic[playerId] = frameCommandDic end local frameCommandList = frameCommandDic[frameNum] if not frameCommandList then--TODO:预测回滚可能会调整10这个大小,或者直接在内存快照的时候优化这部分的内存 frameCommandList = {} frameCommandDic[frameNum] = frameCommandList end table_insert(frameCommandList,command) end framedata.PlayerIds = nil for k,v in pairs(frameDatas) do frameDatas[k] = nil end else --处理帧命令数据 for j,data in ipairs(frameDatas) do local playerId = data[1] local command = private.ChangeCmdNotifyToCommand(data,frameNum ,playerId) local frameCommandDic = commandDic[playerId] if not frameCommandDic then frameCommandDic = {} --备注:这地方由于同步的人不会太多,所以这个不会频繁创建 commandDic[playerId] = frameCommandDic end local frameCommandList = frameCommandDic[frameNum] if not frameCommandList then--TODO:预测回滚可能会调整10这个大小,或者直接在内存快照的时候优化这部分的内存 frameCommandList = {} frameCommandDic[frameNum] = frameCommandList end table_insert(frameCommandList,command) end end end if not isDealAll and not USE_LUA_STANDALONE and GameAppInfo.IsVersionGreaterThan113100() then if private.frameDataListCursor > OnceHandleToCmd then private.frameDataListCursor = private.frameDataListCursor - OnceHandleToCmd for index =1, private.frameDataListCursor do private.frameDataList[index] = private.frameDataList[index + OnceHandleToCmd] private.frameDataList[index + OnceHandleToCmd] = nil -- 这里可以换成nil,目的是除了大补帧后容量不需要很大 保留几个就足够了 。 end else private.frameDataListCursor = 0 end else private.frameDataListCursor = 0 end end function private.ChangeCmdNotifyToCommand(frameData,serverFrame,playerId, index) -- 只有 113000版本后才有index local offset = index or 0 local skillIdx = frameData[offset + 2] -- local dirX = frameData[offset + 3]--为了缩小数据量该字段跟下面的字段合并为了1个short,代表角度 -- local dirZ = frameData[offset + 4]--为了缩小数据量该字段跟上面的字段合并为了1个short,代表角度 local moveType = frameData[offset + 3] -- 摇杆的幅度大小,0表示小圈 ,1 表示大圈 local angle = frameData[offset + 4] --角度,合并了dirX和dirZ的信息 ,精度为2 local spec = frameData[offset + 5] local useJoyStick = frameData[offset + 6] local skillJoystickPosX = frameData[offset + 7] local skillJoystickPosZ = frameData[offset + 8] local command = Command.TryGet() command.Frame = serverFrame --技能按键 command.SkillIndex = skillIdx local dirX = FP.Const.Zero local dirZ = FP.Const.Zero if angle < 181 then --约定小于181才是有效数据 dirX = TriangleMath.Cos(angle * 2) dirZ = TriangleMath.Sin(angle * 2) end command.DirAngle = angle command.MoveType = moveType command.MoveDir:Rawset(dirX[1], dirZ[1]) command.RotateDir:Rawset(dirX[1], dirZ[1]) command.SpecialCommand = spec command.UseJoyStick = useJoyStick --command.SkillJoystickDirAngle:Rawset(skillJoystickDirAngle*100) -- 上传的时候保留1位小数 command.SkillJoystickPosX:Rawset(skillJoystickPosX*10) -- 上传的时候保留了2位小数 command.SkillJoystickPosZ:Rawset(skillJoystickPosZ*10) --上传的时候保留了2位小数 --统计自我激活 if spec == LuaConst.SpecialCommandType.ActiveSelf then if not private.playerActiveDic[serverFrame] then private.playerActiveDic[serverFrame] = {playerId} else table_insert(private.playerActiveDic[serverFrame] ,playerId) end end -- logging.warn(LTAG.CRIT, string.format("接收到的信息:eid=%d, frame= %d, mx= %d ,mz= %d" , frameData[1] ,serverFrame,dirX,dirZ)) return command end -- -- 用自己的本地保存的文件回放时用到的接口 function KCPTransport.SetCmdByFrameFile(frameNum, playerIds, frameDatas) private.OnFrameCallback(frameNum, playerIds, frameDatas) end function KCPTransport.GetCurserverFrame() local serverframe = BattleExt.GetCurServerFrame() --logging.warn(LTAG.CRIT, "frame:", serverframe) return serverframe end function KCPTransport.GetOnlinePlayers(curFrame, outOnlinePlayerList) local errorCode = 0 if private.onlinePlayerDic == nil then errorCode = 1 return errorCode end local playerIDs = private.onlinePlayerDic[curFrame] if not playerIDs then local connectInfo = MultiplayManager.GetKCPConnectInfo() logging.error(LTAG.ERROR, "[sync]: cant find framedata, frame =", curFrame ,",groomID =",connectInfo.gRoomId) if USE_LUA_STANDALONE then BattleSimulator.LoseFrame = true end errorCode = 2 return errorCode end LogicUpdateManager._handleFrameCount = LogicUpdateManager._handleFrameCount + 1 for k,v in ipairs(playerIDs) do -- 拷贝数据 outOnlinePlayerList[k] = v end if GameAppInfo.IsVersionGreaterThan113100() then playerIDList_pool.Recycle(playerIDs) end private.onlinePlayerDic[curFrame] = nil -- 删除用过的信息 return 0 end function KCPTransport.GetFrame2CmdCountDic() return private.frame2CmdCountDic end function KCPTransport.ClearFrame2CmdDic() private.frame2CmdCountDic = {} end -- 这个函数之前一定要有 KCPTransport.Close() function KCPTransport.ClearKCPData() private.Shutdown() BattleExt.Clear() end function KCPTransport.GetPlayerActiveList(curFrame) local playerIDs = private.playerActiveDic[curFrame] private.playerActiveDic[curFrame] = nil -- 删除用过的信息 return playerIDs end function KCPTransport.SetSaveFrameData(val) BattleExt.SetIsSaveFrameData(val) end -- 这个update是在表现层 ,可以用表现代码 [TCP断开后15秒的心跳,11秒的菊花(3秒一次重连),然后弹出手动重连 。 kcp的timeout时间是20秒] function KCPTransport.OnExpUpdate(deltaTime) private.checkConnectTime = private.checkConnectTime + deltaTime if private.checkConnectTime > 1 then -- 每秒 检测一次 只是为了节约检测频率(TCP断开时长是15秒 ,如果TCP断开了是会弹出TCP断线的提示框,TCP不会自动重连) -- todo:等袁立 将客户端心跳检测加上后,这块再把30改小。 if LuaNetManager.IsOnline() then -- TCP 连接中 ,KCP 没有收到协议 ,说明KCP异常断开了 【这部分代码,适用的情况是,房间被服务器销毁时人还在战斗中】 ;但是也特殊的网络环境,TCP能连着,KCP就是不行。 local pongTimeSpan = BattleExt.GetKCPPongTimeSpan() -- 现在到最近一次pong的时间 if pongTimeSpan > 30 then -- 发现已经30秒没有收到pong协议了,则认为已经断开了kcp,且断线自动重连也失败了 。 30秒的时间足够检测到TCP断线了 ;正常来说kcp断开10秒就可以的弹了 EventSystem.Dispatch(EventID.BI_LevelMiddle,BIManager.BIType.SyncBattle,"[sync]KcpReconnectNoRoom") KCPTransport.reloginStop = true LevelManager.CurLevelFunc:StopLevel() -- 这个stoplevel 会关闭FightBegin,KCPTransport.OnExpUpdate 不再执行的 UTL.MessageBox.ShowOnlyOkNoCloseBox(Const_IDS.IDS_WARNING, Const_IDS.IDS_3v3_NetworkInterruption , LuaNetManager.RebootGame ,Const_IDS.IDS_BUTTON_CONFIRM) end end private.checkConnectTime = 0 end end -- kcp断线 ,心跳也发不出 弹框的 房间也存在时 的处理 。 local function ReconnectKCP() if MultiplayManager.CurConnectType == LuaConst.SyncConnectType.TCPReconnectInBattle then EventSystem.Dispatch(EventID.BI_LevelMiddle,BIManager.BIType.SyncBattle,"[sync]KCPConnect_DropByTCP") logging.warn(LTAG.WARN,"【gws】点击KCP重连按钮,TCP收到有房间,但此时TCP正在重连中,为防止打断TCP重连,需要阻断kcp新建重连 ") return -- 当断线时 ,先kcp断开弹框,然后TCP断开。点击kcp重连则请求房间,TCP连上后开始TCP重连,但是这个请求房间也会立刻收到,导致开启KCP重连,重连流程混乱了。 end local connectInfo = MultiplayManager.GetKCPConnectInfo() KCPTransport.Connect(connectInfo.ip ,connectInfo.port ,connectInfo.roomId ,connectInfo.playerId) end local function BtnReConnectKCP() local pongTimeSpan = BattleExt.GetKCPPongTimeSpan() if pongTimeSpan < 2 then -- 后台也没有重连上 EventSystem.Dispatch(EventID.BI_LevelMiddle,BIManager.BIType.SyncBattle,"[sync]KcpDisConnect_Rec_AutoOk", pongTimeSpan) return end EventSystem.Dispatch(EventID.BI_LevelMiddle,BIManager.BIType.SyncBattle,"[sync]KcpDisConnect_ReConnectKCP") EventSystem.Dispatch(EventID.BI_ClientNetState, NetManager.NetStateCode.Normal, NetManager.NetStateCode.Reconnect, NetManager.DisconnectReason.None) MultiplayManager.CheckBattleRoom(ReconnectKCP) --询问房间是否还存在,存在则重连kcp end -- kcp断线 ,心跳也发不出 弹框的 回城按钮 local function BtnBackLaunch() EventSystem.Dispatch(EventID.BI_LevelMiddle,BIManager.BIType.SyncBattle,"[sync]KcpDisConnect_BtnHome") LuaNetManager.RebootGame() end function KCPTransport.CheckIsConnectSuccess(deltaTime, timeSinceUpdate) private.connectCheckTimer = private.connectCheckTimer + deltaTime if private.connectCheckTimer >= 3 then -- 连接IP后 3秒检查下。pong有没有过来 local pongTimeSpan = BattleExt.GetKCPPongTimeSpan() -- 现在到最近一次pong的时间 if pongTimeSpan > 2 then -- 2秒了都没收到pong且ping值都没更新,则认为kcp没有连接上 EventSystem.Dispatch(EventID.BI_LevelMiddle,BIManager.BIType.SyncBattle,"[sync]KcpConnectFail", pongTimeSpan) LuaUpdater.StopRealTimeUpdate(KCPTransport.CheckIsConnectSuccess) if LuaNetManager.IsOnline() then -- 只有在TCP连着是才弹出kcp无心跳的框 ,如果TCP断开了是会弹出TCP断线的提示框,TCP断开的框需要优先级最高 EventSystem.Dispatch(EventID.BI_ClientNetState, NetManager.NetStateCode.Normal, NetManager.NetStateCode.Disconnect, NetManager.DisconnectReason.KcpPingTimeout) if MDL.Loading.GetIsLoadingState() then --主要是ios对tcp网络断开不敏感,导致 logging.crit(LTAG.CRIT, "在loading进入同步副本的时候,断线,检查房间是否存在,存在则回登录") MultiplayManager.CheckBattleRoom(NetManager.ManualBackLaunch) --询问房间是否还存在,存在则 踢回登录 -- 袁立以后可能要改成重走重连逻辑 else UTL.MessageBox.ShowOKCancelNoCloseBox(Const_IDS.IDS_WARNING, Const_IDS.IDS_TIPS_WIFI_CHECK, BtnReConnectKCP, BtnBackLaunch, Const_IDS.IDS_RECONTACT, Const_IDS.IDS_BACKLOADINGUI, UTL.MessageBox.DisplayType.KCPReconnectTip) end end end private.connectCheckTimer = 0 end end --region kcp连接代理服需要的方法。 只要目的是获取与agent的ping值,故不需cmd的解析 function KCPTransport.ConnectBattleAgent(ip ,port) BattleExt.Connect(ip, port, 1, 1) -- roomId 和 playerid 写死为1 end function KCPTransport.CloseBattleAgent() BattleExt.Close() BattleExt.Clear() end --endregion kcp连接代理服需要的方法 return KCPTransport