diff --git a/battle_srv/models/room.go b/battle_srv/models/room.go index e7fa634..465d28d 100644 --- a/battle_srv/models/room.go +++ b/battle_srv/models/room.go @@ -168,7 +168,8 @@ type Room struct { LastRenderFrameIdTriggeredAt int64 PlayerDefaultSpeed int32 - BulletBattleLocalIdCounter int32 + BulletBattleLocalIdCounter int32 + dilutedRollbackEstimatedDtNanos int64 BattleColliderInfo // Compositing to send centralized magic numbers } @@ -465,7 +466,7 @@ func (pR *Room) StartBattle() { dynamicsDuration = utils.UnixtimeNano() - dynamicsStartedAt } if 0 < unconfirmedMask { - Logger.Warn(fmt.Sprintf("roomId=%v, room.RenderFrameId=%v, room.CurDynamicsRenderFrameId=%v, room.LastAllConfirmedInputFrameId=%v, unconfirmedMask=%v", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, pR.LastAllConfirmedInputFrameId, unconfirmedMask)) + Logger.Debug(fmt.Sprintf("roomId=%v, room.RenderFrameId=%v, room.CurDynamicsRenderFrameId=%v, room.LastAllConfirmedInputFrameId=%v, unconfirmedMask=%v", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, pR.LastAllConfirmedInputFrameId, unconfirmedMask)) // Otherwise no need to downsync immediately pR.downsyncToAllPlayers(pR.LastAllConfirmedInputFrameId, unconfirmedMask, false) } @@ -477,7 +478,7 @@ func (pR *Room) StartBattle() { if elapsedInCalculation > pR.RollbackEstimatedDtNanos { Logger.Warn(fmt.Sprintf("SLOW FRAME! Elapsed time statistics: roomId=%v, room.RenderFrameId=%v, elapsedInCalculation=%v ns, dynamicsDuration=%v ns, RollbackEstimatedDtNanos=%v", pR.Id, pR.RenderFrameId, elapsedInCalculation, dynamicsDuration, pR.RollbackEstimatedDtNanos)) } - time.Sleep(time.Duration(pR.RollbackEstimatedDtNanos - elapsedInCalculation)) + time.Sleep(time.Duration(pR.dilutedRollbackEstimatedDtNanos - elapsedInCalculation)) } } @@ -714,6 +715,8 @@ func (pR *Room) OnDismissed() { pR.ServerFps = 60 pR.RollbackEstimatedDtMillis = 16.667 // Use fixed-and-low-precision to mitigate the inconsistent floating-point-number issue between Golang and JavaScript pR.RollbackEstimatedDtNanos = 16666666 // A little smaller than the actual per frame time, just for logging FAST FRAME + dilutedServerFps := int64(58) + pR.dilutedRollbackEstimatedDtNanos = pR.RollbackEstimatedDtNanos * (int64(pR.ServerFps) / dilutedServerFps) pR.BattleDurationFrames = 30 * pR.ServerFps pR.BattleDurationNanos = int64(pR.BattleDurationFrames) * (pR.RollbackEstimatedDtNanos + 1) pR.InputFrameUpsyncDelayTolerance = 2 @@ -921,7 +924,7 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool { This function is triggered by an upsync message via WebSocket, thus downsync sending is also available by now. */ thatPlayerBattleState := atomic.LoadInt32(&(thatPlayer.BattleState)) - Logger.Info(fmt.Sprintf("OnPlayerBattleColliderAcked-middle: roomId=%v, roomState=%v, targetPlayerId=%v, targetPlayerBattleState=%v, thatPlayerId=%v, thatPlayerBattleState=%v", pR.Id, pR.State, targetPlayer.Id, targetPlayer.BattleState, thatPlayer.Id, thatPlayerBattleState)) + Logger.Debug(fmt.Sprintf("OnPlayerBattleColliderAcked-middle: roomId=%v, roomState=%v, targetPlayerId=%v, targetPlayerBattleState=%v, thatPlayerId=%v, thatPlayerBattleState=%v", pR.Id, pR.State, targetPlayer.Id, targetPlayer.BattleState, thatPlayer.Id, thatPlayerBattleState)) if thatPlayerId == targetPlayer.Id || (PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK == thatPlayerBattleState || PlayerBattleStateIns.ACTIVE == thatPlayerBattleState) { Logger.Debug(fmt.Sprintf("OnPlayerBattleColliderAcked-sending DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED: roomId=%v, roomState=%v, targetPlayerId=%v, targetPlayerBattleState=%v, capacity=%v, EffectivePlayerCount=%v", pR.Id, pR.State, targetPlayer.Id, targetPlayer.BattleState, pR.Capacity, pR.EffectivePlayerCount)) pR.sendSafely(playerAckedFrame, nil, DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED, thatPlayer.Id) @@ -1447,27 +1450,29 @@ func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, upperToSe // [WARNING] Websocket is TCP-based, thus no need to re-send a previously sent inputFrame to a same player! lowerToSentInputFrameId := player.LastSentInputFrameId + 1 /* - [WARNING] - Upon resynced on frontend, "refRenderFrameId" MUST BE CAPPED somehow by "upperToSendInputFrameId", if frontend resyncs itself to a more advanced value than given below, upon the next renderFrame tick on the frontend it might generate non-consecutive "nextInputFrameId > frontend.recentInputCache.edFrameId+1". + [WARNING] + Upon resynced on frontend, "refRenderFrameId" is now set to as advanced as possible, and it's the frontend's responsibility now to pave way for the "gap inputFrames" - If "NstDelayFrames" becomes larger, "pR.RenderFrameId - refRenderFrameId" possibly becomes larger because the force confirmation is delayed more. + If "NstDelayFrames" becomes larger, "pR.RenderFrameId - refRenderFrameId" possibly becomes larger because the force confirmation is delayed more. - Upon resync, it's still possible that "refRenderFrameId < frontend.chaserRenderFrameId" -- and this is allowed. + Upon resync, it's still possible that "refRenderFrameId < frontend.chaserRenderFrameId" -- and this is allowed. */ - refRenderFrameId := pR.ConvertToGeneratingRenderFrameId(upperToSendInputFrameId + 1) // for the frontend to jump immediately into generating & upsyncing the next input frame, thus getting rid of "resync avalanche" - if refRenderFrameId > pR.RenderFrameId { - refRenderFrameId = pR.RenderFrameId - } + refRenderFrameId := pR.CurDynamicsRenderFrameId - 1 - if MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId { + shouldResync1 := (MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId) + shouldResync2 := (0 < (unconfirmedMask & uint64(1< pR.InputsBuffer.StFrameId" now? } if lowerToSentInputFrameId > upperToSendInputFrameId { - Logger.Warn(fmt.Sprintf("Not sending due to potentially empty toSendInputFrameDownsyncs: roomId=%v, playerId=%v, refRenderFrameId=%v, lowerToSentInputFrameId=%v, upperToSendInputFrameId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, refRenderFrameId, lowerToSentInputFrameId, upperToSendInputFrameId, player.LastSentInputFrameId, player.AckingInputFrameId)) + Logger.Debug(fmt.Sprintf("Not sending due to potentially empty toSendInputFrameDownsyncs: roomId=%v, playerId=%v, playerLastSentInputFrameId=%v, refRenderFrameId=%v, lowerToSentInputFrameId=%v, upperToSendInputFrameId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, player.LastSentInputFrameId, refRenderFrameId, lowerToSentInputFrameId, upperToSendInputFrameId, player.LastSentInputFrameId, player.AckingInputFrameId)) return } @@ -1481,7 +1486,7 @@ func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, upperToSe j, toSendInputFrameDownsyncs := pR.InputsBuffer.cloneInputFrameDownsyncsByFrameIdRange(lowerToSentInputFrameId, upperToSendInputFrameId+1, theInputsBufferLockToUse) if 0 >= len(toSendInputFrameDownsyncs) { - Logger.Debug(fmt.Sprintf("Not sending due to actually empty toSendInputFrameDownsyncs: roomId=%v, playerId=%v, refRenderFrameId=%v, lowerToSentInputFrameId=%v, upperToSendInputFrameId=%v, j=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, refRenderFrameId, lowerToSentInputFrameId, upperToSendInputFrameId, j, player.LastSentInputFrameId, player.AckingInputFrameId)) + Logger.Debug(fmt.Sprintf("Not sending due to actually empty toSendInputFrameDownsyncs: roomId=%v, playerId=%v, playerLastSentInputFrameId=%v, refRenderFrameId=%v, lowerToSentInputFrameId=%v, upperToSendInputFrameId=%v, j=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, player.LastSentInputFrameId, refRenderFrameId, lowerToSentInputFrameId, upperToSendInputFrameId, j, player.LastSentInputFrameId, player.AckingInputFrameId)) return } @@ -1490,17 +1495,17 @@ func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, upperToSe 1. when player with a slower frontend clock lags significantly behind and thus wouldn't get its inputUpsync recognized due to faster "forceConfirmation" 2. reconnection */ - shouldResync1 := (MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId) - shouldResync2 := (0 < (unconfirmedMask & uint64(1< self.lastAllConfirmedInputFrameId) { - minToKeepInputFrameId = self.lastAllConfirmedInputFrameId; - } - while (0 < self.recentInputCache.cnt && self.recentInputCache.stFrameId < minToKeepInputFrameId) { - self.recentInputCache.pop(); - } - const [ret, oldStFrameId, oldEdFrameId] = self.recentInputCache.setByFrameId(inputFrameDownsync, inputFrameDownsync.inputFrameId); - if (window.RING_BUFF_FAILED_TO_SET == ret) { - throw `Failed to dump input cache (maybe recentInputCache too small)! inputFrameDownsync.inputFrameId=${inputFrameDownsync.inputFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`; - } - return ret; - }, - _convertToInputFrameId(renderFrameId, inputDelayFrames) { if (renderFrameId < inputDelayFrames) return 0; return ((renderFrameId - inputDelayFrames) >> this.inputScaleFrames); @@ -156,29 +130,31 @@ cc.Class({ throw `noDelayInputFrameId=${inputFrameId} couldn't be generated: recentInputCache=${self._stringifyRecentInputCache(false)}`; } + let previousSelfInput = null, + currSelfInput = null; const joinIndex = self.selfPlayerInfo.joinIndex; - const previousInputFrameDownsyncWithPrediction = self.getCachedInputFrameDownsyncWithPrediction(inputFrameId); - const previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex - 1]); + // [WARNING] The while-loop here handles a situation where the "resync rdf & accompaniedInputFrameDownsyncBatch" mismatched and we have to predict some "gap-inputFrames"! + while (self.recentInputCache.edFrameId <= inputFrameId) { + // TODO: find some kind of synchronization mechanism against "onInputFrameDownsyncBatch"! + const previousInputFrameDownsyncWithPrediction = self.getCachedInputFrameDownsyncWithPrediction(inputFrameId - 1); + previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex - 1]); - // If "forceConfirmation" is active on backend, we shouldn't override the already downsynced "inputFrameDownsync"s. - const existingInputFrame = self.recentInputCache.getByFrameId(inputFrameId); - if (null != existingInputFrame && self._allConfirmed(existingInputFrame.confirmedList)) { - console.log(`noDelayInputFrameId=${inputFrameId} already exists in recentInputCache and is all-confirmed: recentInputCache=${self._stringifyRecentInputCache(false)}`); - return [previousSelfInput, existingInputFrame.inputList[joinIndex - 1]]; - } - const prefabbedInputList = (null == previousInputFrameDownsyncWithPrediction ? new Array(self.playerRichInfoDict.size).fill(0) : previousInputFrameDownsyncWithPrediction.inputList.slice()); - const currSelfInput = self.ctrl.getEncodedInput(); - prefabbedInputList[(joinIndex - 1)] = currSelfInput; - const prefabbedInputFrameDownsync = window.pb.protos.InputFrameDownsync.create({ - inputFrameId: inputFrameId, - inputList: prefabbedInputList, - confirmedList: (1 << (self.selfPlayerInfo.joinIndex - 1)) - }); + // If "forceConfirmation" is active on backend, there's a chance that the already downsynced "inputFrameDownsync"s are ahead of a locally generating inputFrameId, in this case we respect the downsynced one. + const existingInputFrame = self.recentInputCache.getByFrameId(inputFrameId); + if (null != existingInputFrame && self._allConfirmed(existingInputFrame.confirmedList)) { + console.log(`noDelayInputFrameId=${inputFrameId} already exists in recentInputCache and is all-confirmed: recentInputCache=${self._stringifyRecentInputCache(false)}`); + return [previousSelfInput, existingInputFrame.inputList[joinIndex - 1]]; + } + const prefabbedInputList = (null == previousInputFrameDownsyncWithPrediction ? new Array(self.playerRichInfoDict.size).fill(0) : previousInputFrameDownsyncWithPrediction.inputList.slice()); + currSelfInput = self.ctrl.getEncodedInput(); + prefabbedInputList[(joinIndex - 1)] = currSelfInput; + const prefabbedInputFrameDownsync = window.pb.protos.InputFrameDownsync.create({ + inputFrameId: self.recentInputCache.edFrameId, + inputList: prefabbedInputList, + confirmedList: (1 << (self.selfPlayerInfo.joinIndex - 1)) + }); - self.dumpToInputCache(prefabbedInputFrameDownsync); // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames" - - if (inputFrameId >= self.recentInputCache.edFrameId) { - throw `noDelayInputFrameId=${inputFrameId} seems not properly dumped #1: recentInputCache=${self._stringifyRecentInputCache(false)}`; + self.recentInputCache.put(prefabbedInputFrameDownsync); // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames" } return [previousSelfInput, currSelfInput]; @@ -328,6 +304,8 @@ cc.Class({ self.collisionBulletIndexPrefix = (1 << 15); // For tracking the movements of bullets self.collisionSysMap = new Map(); + console.log(`collisionSys & collisionSysMap reset`); + self.transitToState(ALL_MAP_STATES.VISUAL); self.battleState = ALL_BATTLE_STATES.WAITING; @@ -389,8 +367,7 @@ cc.Class({ window.clearBoundRoomIdInBothVolatileAndPersistentStorage(); window.initPersistentSessionClient(self.initAfterWSConnected, null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ ); }; - resultPanelScriptIns.onCloseDelegate = () => { - }; + resultPanelScriptIns.onCloseDelegate = () => {}; self.gameRuleNode = cc.instantiate(self.gameRulePrefab); self.gameRuleNode.width = self.canvasNode.width; @@ -422,6 +399,7 @@ cc.Class({ /** Init required prefab ended. */ window.handleBattleColliderInfo = function(parsedBattleColliderInfo) { + console.log(`Received parsedBattleColliderInfo via ws`); // TODO: Upon reconnection, the backend might have already been sending down data that'd trigger "onRoomDownsyncFrame & onInputFrameDownsyncBatch", but frontend could reject those data due to "battleState != PlayerBattleState.ACTIVE". Object.assign(self, parsedBattleColliderInfo); self.tooFastDtIntervalMillis = 0.5 * self.rollbackEstimatedDtMillis; @@ -469,7 +447,7 @@ cc.Class({ const x0 = boundaryObj.anchor.x, y0 = boundaryObj.anchor.y; - const newBarrier = self.collisionSys.createPolygon(x0, y0, Array.from(boundaryObj, p => { + const newBarrierCollider = self.collisionSys.createPolygon(x0, y0, Array.from(boundaryObj, p => { return [p.x, p.y]; })); @@ -504,10 +482,10 @@ cc.Class({ } } - // console.log("Created barrier: ", newBarrier); ++barrierIdCounter; const collisionBarrierIndex = (self.collisionBarrierIndexPrefix + barrierIdCounter); - self.collisionSysMap.set(collisionBarrierIndex, newBarrier); + self.collisionSysMap.set(collisionBarrierIndex, newBarrierCollider); + console.log(`Created new barrier collider: ${newBarrierCollider}`); } self.selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer')); @@ -520,6 +498,7 @@ cc.Class({ act: window.UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK, }).finish(); window.sendSafely(reqData); + console.log(`Sent UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK via ws`); }); }; @@ -594,7 +573,7 @@ cc.Class({ const shouldForceDumping1 = (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id); const shouldForceDumping2 = (rdf.id > self.renderFrameId + self.renderFrameIdLagTolerance); - const dumpRenderCacheRet = (shouldForceDumping1 || shouldForceDumping2) ? self.dumpToRenderCache(rdf) : window.RING_BUFF_CONSECUTIVE_SET; + const [dumpRenderCacheRet, oldStRenderFrameId, oldEdRenderFrameId] = (shouldForceDumping1 || shouldForceDumping2) ? self.recentRenderCache.setByFrameId(rdf, rdf.id) : [window.RING_BUFF_CONSECUTIVE_SET, null, null]; if (window.RING_BUFF_FAILED_TO_SET == dumpRenderCacheRet) { throw `Failed to dump render cache#1 (maybe recentRenderCache too small)! rdf.id=${rdf.id}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`; } @@ -625,6 +604,7 @@ cc.Class({ if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id) { console.log('On battle started! renderFrameId=', rdf.id); } else { + self.hideFindingPlayersGUI(rdf); self.onInputFrameDownsyncBatch(accompaniedInputFrameDownsyncBatch); // Important to do this step before setting IN_BATTLE console.warn(`Got resync@localRenderFrameId=${self.renderFrameId} -> rdf.id=${rdf.id} & rdf.backendUnconfirmedMask=${rdf.backendUnconfirmedMask}, @lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, @lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, @chaserRenderFrameId=${self.chaserRenderFrameId}, @localRecentInputCache=${mapIns._stringifyRecentInputCache(false)}`); } @@ -671,6 +651,7 @@ cc.Class({ }, onInputFrameDownsyncBatch(batch) { + // TODO: find some kind of synchronization mechanism against "_generateInputFrameUpsync"! const self = this; if (!self.recentInputCache) { return; @@ -698,7 +679,10 @@ cc.Class({ } // [WARNING] Take all "inputFrameDownsync" from backend as all-confirmed, it'll be later checked by "rollbackAndChase". inputFrameDownsync.confirmedList = (1 << self.playerRichInfoDict.size) - 1; - self.dumpToInputCache(inputFrameDownsync); + const [ret, oldStFrameId, oldEdFrameId] = self.recentInputCache.setByFrameId(inputFrameDownsync, inputFrameDownsync.inputFrameId); + if (window.RING_BUFF_FAILED_TO_SET == ret) { + throw `Failed to dump input cache (maybe recentInputCache too small)! inputFrameDownsync.inputFrameId=${inputFrameDownsync.inputFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`; + } } if (null == firstPredictedYetIncorrectInputFrameId) return; @@ -794,6 +778,8 @@ cc.Class({ newPlayerCollider.data = playerDownsyncInfo; self.collisionSysMap.set(collisionPlayerIndex, newPlayerCollider); + console.log(`Created new player collider: joinIndex=${joinIndex}, colliderRadius=${playerDownsyncInfo.colliderRadius}`); + safelyAddChild(self.node, newPlayerNode); setLocalZOrder(newPlayerNode, 5); @@ -1266,7 +1252,7 @@ cc.Class({ // Move the cursor "self.chaserRenderFrameId", keep in mind that "self.chaserRenderFrameId" is not monotonic! self.chaserRenderFrameId = latestRdf.id; } - self.dumpToRenderCache(latestRdf); + self.recentRenderCache.setByFrameId(latestRdf, latestRdf.id); ++i; } while (i < renderFrameIdEd); diff --git a/frontend/assets/scripts/OfflineMap.js b/frontend/assets/scripts/OfflineMap.js index 3c07076..4780d2f 100644 --- a/frontend/assets/scripts/OfflineMap.js +++ b/frontend/assets/scripts/OfflineMap.js @@ -33,6 +33,7 @@ cc.Class({ self.inputScaleFrames = 2; self.inputFrameUpsyncDelayTolerance = 2; + self.renderCacheSize = 1024; self.rollbackEstimatedDt = 0.016667; self.rollbackEstimatedDtMillis = 16.667; self.rollbackEstimatedDtNanos = 16666666; diff --git a/frontend/assets/scripts/WsSessionMgr.js b/frontend/assets/scripts/WsSessionMgr.js index 3b93592..231dc46 100644 --- a/frontend/assets/scripts/WsSessionMgr.js +++ b/frontend/assets/scripts/WsSessionMgr.js @@ -134,7 +134,7 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) { clientSession.binaryType = 'arraybuffer'; // Make 'event.data' of 'onmessage' an "ArrayBuffer" instead of a "Blob" clientSession.onopen = function(evt) { - console.log("The WS clientSession is opened. clientSession.id=", clientSession.id); + console.log("The WS clientSession is opened."); window.clientSession = clientSession; if (null == onopenCb) return; onopenCb(); @@ -171,7 +171,6 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) { ${JSON.stringify(resp, null, 2)}`); return; } - mapIns.hideFindingPlayersGUI(resp.rdf); const inputFrameIdConsecutive = (resp.inputFrameDownsyncBatch[0].inputFrameId == mapIns.lastAllConfirmedInputFrameId + 1); mapIns.onRoomDownsyncFrame(resp.rdf, resp.inputFrameDownsyncBatch); break;