From 15a062af108039233f0258030c578f94c24a1346 Mon Sep 17 00:00:00 2001 From: genxium Date: Fri, 11 Nov 2022 13:27:48 +0800 Subject: [PATCH] Completed frontend refactoring for more convenient unit testing. --- battle_srv/models/room.go | 39 ++++++++-------- frontend/assets/scenes/login.fire | 2 +- frontend/assets/scripts/Map.js | 77 ++++++++++++++++++------------- 3 files changed, 65 insertions(+), 53 deletions(-) diff --git a/battle_srv/models/room.go b/battle_srv/models/room.go index 6aca6ca..4ae10d5 100644 --- a/battle_srv/models/room.go +++ b/battle_srv/models/room.go @@ -1195,7 +1195,7 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende // [WARNING] It's possible that by now "allConfirmedMask != delayedInputFrame.ConfirmedList && delayedInputFrameId <= pR.LastAllConfirmedInputFrameId", we trust "pR.LastAllConfirmedInputFrameId" as the TOP AUTHORITY. atomic.StoreUint64(&(delayedInputFrame.ConfirmedList), allConfirmedMask) } - + nextRenderFrame := pR.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, pR.CollisionSysMap) // Update in the latest player pointers for playerId, playerDownsync := range nextRenderFrame.Players { @@ -1238,11 +1238,10 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF if nil != delayedInputFrame { inputList := delayedInputFrame.InputList - // Ordered by joinIndex to guarantee determinism? - // Move players according to inputs - for joinIndex := 1; joinIndex <= pR.Capacity; joinIndex++ { - player := pR.PlayersArr[joinIndex-1] - playerId := player.Id + effPushbacks := make([]Vec2D, pR.Capacity) // Guaranteed determinism regardless of traversal order + for playerId, player := range pR.Players { + joinIndex := player.JoinIndex + effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y = float64(0), float64(0) currPlayerDownsync := currRenderFrame.Players[playerId] encodedInput := inputList[joinIndex-1] if 0 == encodedInput { @@ -1252,42 +1251,42 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF newVx := (currPlayerDownsync.VirtualGridX + (decodedInput[0] + decodedInput[0]*currPlayerDownsync.Speed)) newVy := (currPlayerDownsync.VirtualGridY + (decodedInput[1] + decodedInput[1]*currPlayerDownsync.Speed)) // Reset playerCollider position from the "virtual grid position" - collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + currPlayerDownsync.JoinIndex + collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex playerCollider := collisionSysMap[collisionPlayerIndex] playerCollider.X, playerCollider.Y = pR.virtualGridToPlayerColliderPos(newVx, newVy, player) + + // Update in the collision system playerCollider.Update() } // handle pushbacks upon collision after all movements treated as simultaneous - for joinIndex := 1; joinIndex <= pR.Capacity; joinIndex++ { - player := pR.PlayersArr[joinIndex-1] - playerId := player.Id - collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + player.JoinIndex + for _, player := range pR.Players { + joinIndex := player.JoinIndex + collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex playerCollider := collisionSysMap[collisionPlayerIndex] oldDx, oldDy := float64(0), float64(0) - dx, dy := oldDx, oldDy if collision := playerCollider.Check(oldDx, oldDy); collision != nil { playerShape := playerCollider.Shape.(*resolv.ConvexPolygon) for _, obj := range collision.Objects { barrierShape := obj.Shape.(*resolv.ConvexPolygon) if overlapped, pushbackX, pushbackY := CalcPushbacks(oldDx, oldDy, playerShape, barrierShape); overlapped { Logger.Debug(fmt.Sprintf("Collided & overlapped: player.X=%v, player.Y=%v, oldDx=%v, oldDy=%v, playerShape=%v, toCheckBarrier=%v, pushbackX=%v, pushbackY=%v", playerCollider.X, playerCollider.Y, oldDx, oldDy, ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape), pushbackX, pushbackY)) - dx -= pushbackX - dy -= pushbackY + effPushbacks[joinIndex-1].X += pushbackX + effPushbacks[joinIndex-1].Y += pushbackY } else { Logger.Debug(fmt.Sprintf("Collided BUT not overlapped: player.X=%v, player.Y=%v, oldDx=%v, oldDy=%v, playerShape=%v, toCheckBarrier=%v", playerCollider.X, playerCollider.Y, oldDx, oldDy, ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape))) } } } + } - playerCollider.X += dx - playerCollider.Y += dy - - // Update again in "collision space" - playerCollider.Update() + for playerId, player := range pR.Players { + joinIndex := player.JoinIndex + collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex + playerCollider := collisionSysMap[collisionPlayerIndex] // Update "virtual grid position" - newVx, newVy := pR.playerColliderAnchorToVirtualGridPos(playerCollider.X, playerCollider.Y, player) + newVx, newVy := pR.playerColliderAnchorToVirtualGridPos(playerCollider.X-effPushbacks[joinIndex-1].X, playerCollider.Y-effPushbacks[joinIndex-1].Y, player) nextRenderFramePlayers[playerId].VirtualGridX = newVx nextRenderFramePlayers[playerId].VirtualGridY = newVy } diff --git a/frontend/assets/scenes/login.fire b/frontend/assets/scenes/login.fire index 2b9da7c..a527072 100644 --- a/frontend/assets/scenes/login.fire +++ b/frontend/assets/scenes/login.fire @@ -440,7 +440,7 @@ "array": [ 0, 0, - 344.6705889248102, + 377.5870760500153, 0, 0, 0, diff --git a/frontend/assets/scripts/Map.js b/frontend/assets/scripts/Map.js index dec50e3..f0969b0 100644 --- a/frontend/assets/scripts/Map.js +++ b/frontend/assets/scripts/Map.js @@ -324,12 +324,10 @@ cc.Class({ self.selfPlayerInfo = null; // This field is kept for distinguishing "self" and "others". self.recentInputCache = new RingBuffer(1024); - self.latestCollisionSys = new collisions.Collisions(); - self.chaserCollisionSys = new collisions.Collisions(); + self.collisionSys = new collisions.Collisions(); self.collisionBarrierIndexPrefix = (1 << 16); // For tracking the movements of barriers, though not yet actually used - self.latestCollisionSysMap = new Map(); - self.chaserCollisionSysMap = new Map(); + self.collisionSysMap = new Map(); self.transitToState(ALL_MAP_STATES.VISUAL); @@ -382,8 +380,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; @@ -474,13 +471,11 @@ cc.Class({ for (let i = 0; i < boundaryObj.length; ++i) { pts.push([boundaryObj[i].x - x0, boundaryObj[i].y - y0]); } - const newBarrierLatest = self.latestCollisionSys.createPolygon(x0, y0, pts); - // console.log("Created barrier: ", newBarrierLatest); - const newBarrierChaser = self.chaserCollisionSys.createPolygon(x0, y0, pts); + const newBarrier = self.collisionSys.createPolygon(x0, y0, pts); + // console.log("Created barrier: ", newBarrier); ++barrierIdCounter; const collisionBarrierIndex = (self.collisionBarrierIndexPrefix + barrierIdCounter); - self.latestCollisionSysMap.set(collisionBarrierIndex, newBarrierLatest); - self.chaserCollisionSysMap.set(collisionBarrierIndex, newBarrierChaser); + self.collisionSysMap.set(collisionBarrierIndex, newBarrier); } self.selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer')); @@ -587,7 +582,7 @@ cc.Class({ /* Don't change - lastAllConfirmedRenderFrameId, it's updated only in "rollbackAndChase" (except for when RING_BUFF_NON_CONSECUTIVE_SET) - - chaserRenderFrameId, it's updated only in "onInputFrameDownsyncBatch" (except for when RING_BUFF_NON_CONSECUTIVE_SET) + - chaserRenderFrameId, it's updated only in "rollbackAndChase & onInputFrameDownsyncBatch" (except for when RING_BUFF_NON_CONSECUTIVE_SET) */ return dumpRenderCacheRet; } @@ -755,11 +750,9 @@ cc.Class({ y0 = cpos[1]; let pts = [[0, 0], [d, 0], [d, d], [0, d]]; - const newPlayerColliderLatest = self.latestCollisionSys.createPolygon(x0, y0, pts); - const newPlayerColliderChaser = self.chaserCollisionSys.createPolygon(x0, y0, pts); + const newPlayerCollider = self.collisionSys.createPolygon(x0, y0, pts); const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; - self.latestCollisionSysMap.set(collisionPlayerIndex, newPlayerColliderLatest); - self.chaserCollisionSysMap.set(collisionPlayerIndex, newPlayerColliderChaser); + self.collisionSysMap.set(collisionPlayerIndex, newPlayerCollider); safelyAddChild(self.node, newPlayerNode); setLocalZOrder(newPlayerNode, 5); @@ -806,12 +799,11 @@ cc.Class({ if (nextChaserRenderFrameId > self.renderFrameId) { nextChaserRenderFrameId = self.renderFrameId; } - self.rollbackAndChase(prevChaserRenderFrameId, nextChaserRenderFrameId, self.chaserCollisionSys, self.chaserCollisionSysMap); - self.chaserRenderFrameId = nextChaserRenderFrameId; // Move the cursor "self.chaserRenderFrameId", keep in mind that "self.chaserRenderFrameId" is not monotonic! + self.rollbackAndChase(prevChaserRenderFrameId, nextChaserRenderFrameId, self.collisionSys, self.collisionSysMap, true); let t2 = performance.now(); - // Inside the following "self.rollbackAndChase" (which actually ROLLS FORWARD), the "self.latestCollisionSys" is ALWAYS "ROLLED BACK" to "self.recentRenderCache.get(self.renderFrameId)" before being applied dynamics from corresponding delayedInputFrame, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now. - const rdf = self.rollbackAndChase(self.renderFrameId, self.renderFrameId + 1, self.latestCollisionSys, self.latestCollisionSysMap); + // Inside the following "self.rollbackAndChase" actually ROLLS FORWARD w.r.t. the corresponding delayedInputFrame, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now. + const rdf = self.rollbackAndChase(self.renderFrameId, self.renderFrameId + 1, self.collisionSys, self.collisionSysMap, false); /* const nonTrivialChaseEnded = (prevChaserRenderFrameId < nextChaserRenderFrameId && nextChaserRenderFrameId == self.renderFrameId); if (nonTrivialChaseEnded) { @@ -981,6 +973,7 @@ cc.Class({ // TODO: Write unit-test for this function to compare with its backend counter part applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, collisionSys, collisionSysMap) { + const self = this; const nextRenderFramePlayers = {} for (let playerId in currRenderFrame.players) { const currPlayerDownsync = currRenderFrame.players[playerId]; @@ -1001,19 +994,19 @@ cc.Class({ } const toRet = { - id: currRenderFrame.id, + id: currRenderFrame.id + 1, players: nextRenderFramePlayers, }; if (null != delayedInputFrame) { const inputList = delayedInputFrame.inputList; - // [WARNING] Traverse in the order of joinIndices to guarantee determinism. + const effPushbacks = new Array(inputList.length).fill([0.0, 0.0]); // Guaranteed determinism regardless of traversal order for (let j in self.playerRichInfoArr) { const joinIndex = parseInt(j) + 1; const playerId = self.playerRichInfoArr[j].id; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); - const player = renderFrame.players[playerId]; + const player = currRenderFrame.players[playerId]; const encodedInput = inputList[joinIndex - 1]; const decodedInput = self.ctrl.decodeDirection(encodedInput); @@ -1021,6 +1014,7 @@ cc.Class({ continue; } + // console.log(`Got non-zero inputs for playerId=${playerId}, decodedInput=${JSON.stringify(decodedInput)} @currRenderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.id}`); /* Reset "position" of players in "collisionSys" according to "virtual grid position". The easy part is that we don't have path-dependent-integrals to worry about like that of thermal dynamics. */ @@ -1029,10 +1023,13 @@ cc.Class({ const newCpos = self.virtualGridToPlayerColliderPos(newVx, newVy, self.playerRichInfoArr[j]); playerCollider.x = newCpos[0]; playerCollider.y = newCpos[1]; + // Update directions and thus would eventually update moving animation accordingly + nextRenderFramePlayers[playerId].dir.dx = decodedInput.dx; + nextRenderFramePlayers[playerId].dir.dy = decodedInput.dy; } collisionSys.update(); - const result = collisionSys.createResult(); // Can I reuse a "self.latestCollisionSysResult" object throughout the whole battle? + const result = collisionSys.createResult(); // Can I reuse a "self.collisionSysResult" object throughout the whole battle? for (let j in self.playerRichInfoArr) { const joinIndex = parseInt(j) + 1; @@ -1044,11 +1041,19 @@ cc.Class({ // Test if the player collides with the wall if (!playerCollider.collides(potential, result)) continue; // Push the player out of the wall - playerCollider.x -= result.overlap * result.overlap_x; - playerCollider.y -= result.overlap * result.overlap_y; + effPushbacks[j][0] += result.overlap * result.overlap_x; + effPushbacks[j][1] += result.overlap * result.overlap_y; } const newVpos = self.playerColliderAnchorToVirtualGridPos(playerCollider.x, playerCollider.y, self.playerRichInfoArr[j]); + } + + for (let j in self.playerRichInfoArr) { + const joinIndex = parseInt(j) + 1; + const playerId = self.playerRichInfoArr[j].id; + const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; + const playerCollider = collisionSysMap.get(collisionPlayerIndex); + const newVpos = self.playerColliderAnchorToVirtualGridPos(playerCollider.x - effPushbacks[j][0], playerCollider.y - effPushbacks[j][1], self.playerRichInfoArr[j]); nextRenderFramePlayers[playerId].virtualGridX = newVpos[0]; nextRenderFramePlayers[playerId].virtualGridY = newVpos[1]; } @@ -1057,14 +1062,15 @@ cc.Class({ return toRet; }, - rollbackAndChase(renderFrameIdSt, renderFrameIdEd, collisionSys, collisionSysMap) { + rollbackAndChase(renderFrameIdSt, renderFrameIdEd, collisionSys, collisionSysMap, isChasing) { /* - This function eventually calculates a "RoomDownsyncFrame" where "RoomDownsyncFrame.id == renderFrameIdEd". + This function eventually calculates a "RoomDownsyncFrame" where "RoomDownsyncFrame.id == renderFrameIdEd" if not interruptted. */ const self = this; let latestRdf = self.recentRenderCache.getByFrameId(renderFrameIdSt); // typed "RoomDownsyncFrame" if (null == latestRdf) { console.error("Couldn't find renderFrameId=", renderFrameIdSt, " to rollback, lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false)); + return latestRdf; } if (renderFrameIdSt >= renderFrameIdEd) { @@ -1072,14 +1078,16 @@ cc.Class({ } for (let i = renderFrameIdSt; i < renderFrameIdEd; ++i) { - const currRenderFrame = self.recentRenderCache.getByFrameId(i); // typed "RoomDownsyncFrame"; FIXME: onRoomDownsyncFrame(rdf) might get called asynchronously and thus made this line return "null"! + const currRenderFrame = self.recentRenderCache.getByFrameId(i); // typed "RoomDownsyncFrame"; [WARNING] When "true == isChasing", this function can be interruptted by "onRoomDownsyncFrame(rdf)" asynchronously anytime, making this line return "null"! if (null == currRenderFrame) { - console.error("Couldn't find renderFrameId=", i, " to rollback, lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false)); + console.warn("Couldn't find renderFrame for i=", i, " to rollback, self.renderFrameId=", self.renderFrameId, ", lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", might've been interruptted by `onRoomDownsyncFrame`"); + return latestRdf; } const j = self._convertToInputFrameId(i, self.inputDelayFrames); const delayedInputFrame = self.getCachedInputFrameDownsyncWithPrediction(j); if (null == delayedInputFrame) { - console.error("Failed to get cached delayedInputFrame for renderFrameId=", i, ", inputFrameId=", j, "lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false)); + console.warn("Failed to get cached delayedInputFrame for i=", i, ", j=", j, ", self.renderFrameId=", self.renderFrameId, ", lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId); + return latestRdf; } latestRdf = self.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, collisionSys, collisionSysMap); @@ -1091,10 +1099,15 @@ cc.Class({ // We got a more up-to-date "all-confirmed-render-frame". self.lastAllConfirmedRenderFrameId = latestRdf.id; if (latestRdf.id > self.chaserRenderFrameId) { - // it must be true that "chaserRenderFrameId >= lastAllConfirmedRenderFrameId" + // it must be true that "chaserRenderFrameId >= lastAllConfirmedRenderFrameId", regardeless of the "isChasing" param self.chaserRenderFrameId = latestRdf.id; } } + + if (true == isChasing) { + // Move the cursor "self.chaserRenderFrameId", keep in mind that "self.chaserRenderFrameId" is not monotonic! + self.chaserRenderFrameId = latestRdf.id; + } self.dumpToRenderCache(latestRdf); }