From 901b189c5ac2698b0740c3bea33a5eea42f501a2 Mon Sep 17 00:00:00 2001 From: genxium Date: Thu, 10 Nov 2022 18:18:00 +0800 Subject: [PATCH] Improvements on using integer positioning. --- battle_srv/models/room.go | 77 +++++++-------- frontend/assets/scenes/login.fire | 2 +- frontend/assets/scripts/BasePlayer.js | 12 +-- frontend/assets/scripts/Map.js | 57 +++++++---- frontend/assets/scripts/TouchEventsManager.js | 95 +++++++++---------- 5 files changed, 128 insertions(+), 115 deletions(-) diff --git a/battle_srv/models/room.go b/battle_srv/models/room.go index 6388512..127c23e 100644 --- a/battle_srv/models/room.go +++ b/battle_srv/models/room.go @@ -60,36 +60,17 @@ const ( MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED = -2 ) +// These directions are chosen such that when speed is changed to "(speedX+delta, speedY+delta)" for any of them, the direction is unchanged. var DIRECTION_DECODER = [][]int32{ {0, 0}, - {0, +1}, - {0, -1}, + {0, +2}, + {0, -2}, {+2, 0}, {-2, 0}, - {+2, +1}, - {-2, -1}, - {+2, -1}, - {-2, +1}, - {+2, 0}, - {-2, 0}, - {0, +1}, - {0, -1}, -} - -var DIRECTION_DECODER_INVERSE_LENGTH = []float64{ - 0.0, - 1.0, - 1.0, - 0.5, - 0.5, - 0.44, // Actually it should be "0.4472", but truncated for better precision sync as well as a reduction of speed in diagonal direction - 0.44, - 0.44, - 0.44, - 0.5, - 0.5, - 1.0, - 1.0, + {+1, +1}, + {-1, -1}, + {+1, -1}, + {-1, +1}, } type RoomBattleState struct { @@ -802,7 +783,7 @@ func (pR *Room) OnDismissed() { // Always instantiates new HeapRAM blocks and let the old blocks die out due to not being retained by any root reference. pR.WorldToVirtualGridRatio = float64(10) pR.VirtualGridToWorldRatio = float64(1.0) / pR.WorldToVirtualGridRatio // this is a one-off computation, should avoid division in iterations - pR.PlayerDefaultSpeed = int32(3 * pR.WorldToVirtualGridRatio) // Hardcoded in virtual grids per frame + pR.PlayerDefaultSpeed = 10 // Hardcoded in virtual grids per frame pR.Players = make(map[int32]*Player) pR.PlayersArr = make([]*Player, pR.Capacity) pR.CollisionSysMap = make(map[int32]*resolv.Object) @@ -1209,24 +1190,37 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende inputList := delayedInputFrame.InputList // Ordered by joinIndex to guarantee determinism + // Move players according to inputs for _, player := range pR.PlayersArr { joinIndex := player.JoinIndex encodedInput := inputList[joinIndex-1] decodedInput := DIRECTION_DECODER[encodedInput] - decodedInputSpeedFactor := DIRECTION_DECODER_INVERSE_LENGTH[encodedInput] - if 0.0 == decodedInputSpeedFactor { + player.Dir.Dx = decodedInput[0] + player.Dir.Dy = decodedInput[1] + if 0 == decodedInput[0] && 0 == decodedInput[1] { continue } - baseChange := float64(player.Speed) * pR.VirtualGridToWorldRatio * decodedInputSpeedFactor - oldDx, oldDy := baseChange*float64(decodedInput[0]), baseChange*float64(decodedInput[1]) - dx, dy := oldDx, oldDy collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex playerCollider := pR.CollisionSysMap[collisionPlayerIndex] - // Reset playerCollider position from the "virtual grid position" - playerCollider.X, playerCollider.Y = pR.virtualGridToPlayerColliderPos(player.VirtualGridX, player.VirtualGridY, player) - if collision := playerCollider.Check(oldDx, oldDy, "Barrier"); collision != nil { + // Reset playerCollider position from the "virtual grid position" + newVx := (player.VirtualGridX + (decodedInput[0] + decodedInput[0]*player.Speed)) + newVy := (player.VirtualGridY + (decodedInput[1] + decodedInput[1]*player.Speed)) + playerCollider.X, playerCollider.Y = pR.virtualGridToPlayerColliderPos(newVx, newVy, player) + + // Update in "collision space" + playerCollider.Update() + } + + // handle pushbacks upon collision + for _, player := range pR.PlayersArr { + joinIndex := player.JoinIndex + collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex + playerCollider := pR.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) @@ -1242,16 +1236,15 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende playerCollider.X += dx playerCollider.Y += dy - // Update in "collision space" + // Update again in "collision space" playerCollider.Update() - player.Dir.Dx = decodedInput[0] - player.Dir.Dy = decodedInput[1] + // Update "virtual grid position" player.VirtualGridX, player.VirtualGridY = pR.playerColliderAnchorToVirtualGridPos(playerCollider.X, playerCollider.Y, player) } } - pbPlayers := toPbPlayers(pR.Players) + pbPlayers := toPbPlayers(pR.Players) newRenderFrame := RoomDownsyncFrame{ Id: collisionSysRenderFrameId + 1, @@ -1295,13 +1288,15 @@ func (pR *Room) printBarrier(barrierCollider *resolv.Object) { } func (pR *Room) worldToVirtualGridPos(wx, wy float64) (int32, int32) { + // [WARNING] Introduces loss of precision! // In JavaScript floating numbers suffer from seemingly non-deterministic arithmetics, and even if certain libs solved this issue by approaches such as fixed-point-number, they might not be used in other libs -- e.g. the "collision libs" we're interested in -- thus couldn't kill all pains. - var virtualGridX int32 = int32(wx * pR.WorldToVirtualGridRatio) - var virtualGridY int32 = int32(wy * pR.WorldToVirtualGridRatio) + var virtualGridX int32 = int32(math.Round(wx * pR.WorldToVirtualGridRatio)) + var virtualGridY int32 = int32(math.Round(wy * pR.WorldToVirtualGridRatio)) return virtualGridX, virtualGridY } func (pR *Room) virtualGridToWorldPos(vx, vy int32) (float64, float64) { + // No loss of precision var wx float64 = float64(vx) * pR.VirtualGridToWorldRatio var wy float64 = float64(vy) * pR.VirtualGridToWorldRatio return wx, wy diff --git a/frontend/assets/scenes/login.fire b/frontend/assets/scenes/login.fire index 0becc89..b8f78ce 100644 --- a/frontend/assets/scenes/login.fire +++ b/frontend/assets/scenes/login.fire @@ -440,7 +440,7 @@ "array": [ 0, 0, - 216.67592045656244, + 378.4531014537997, 0, 0, 0, diff --git a/frontend/assets/scripts/BasePlayer.js b/frontend/assets/scripts/BasePlayer.js index 72d2b53..8f1b5ea 100644 --- a/frontend/assets/scripts/BasePlayer.js +++ b/frontend/assets/scripts/BasePlayer.js @@ -44,14 +44,14 @@ module.export = cc.Class({ onLoad() { const self = this; self.clips = { - '01': 'Top', - '0-1': 'Bottom', + '02': 'Top', + '0-2': 'Bottom', '-20': 'Left', '20': 'Right', - '-21': 'TopLeft', - '21': 'TopRight', - '-2-1': 'BottomLeft', - '2-1': 'BottomRight' + '-11': 'TopLeft', + '11': 'TopRight', + '-1-1': 'BottomLeft', + '1-1': 'BottomRight' }; const canvasNode = self.mapNode.parent; self.mapIns = self.mapNode.getComponent("Map"); diff --git a/frontend/assets/scripts/Map.js b/frontend/assets/scripts/Map.js index 688603d..98440cc 100644 --- a/frontend/assets/scripts/Map.js +++ b/frontend/assets/scripts/Map.js @@ -111,7 +111,7 @@ cc.Class({ type: cc.Integer, default: 4 // implies (renderFrameIdLagTolerance >> inputScaleFrames) count of inputFrameIds }, - teleportEps1D: { + jigglingEps1D: { type: cc.Float, default: 1e-3 }, @@ -749,7 +749,8 @@ cc.Class({ newPlayerNode.setPosition(cc.v2(wpos[0], wpos[1])); newPlayerNode.getComponent("SelfPlayer").mapNode = self.node; const cpos = self.virtualGridToPlayerColliderPos(vx, vy, playerRichInfo); - const d = playerRichInfo.colliderRadius*2, x0 = cpos[0], + const d = playerRichInfo.colliderRadius * 2, + x0 = cpos[0], y0 = cpos[1]; let pts = [[0, 0], [d, 0], [d, d], [0, d]]; @@ -990,6 +991,23 @@ cc.Class({ rdf.id > self.lastAllConfirmedRenderFrameId ) { // We got a more up-to-date "all-confirmed-render-frame". + let predictedRdf = self.recentRenderCache.getByFrameId(rdf.id); + if (null != predictedRdf) { + let renderFrameCorrectlyPredicted = true; + for (let playerId in predictedRdf.players) { + const predictedPlayer = predictedRdf.players[playerId]; + const confirmedPlayer = rdf.players[playerId]; + if (predictedPlayer.virtualGridX != confirmedPlayer.virtualGridX || predictedPlayer.virtualGridY != confirmedPlayer.virtualGridY) { + renderFrameCorrectlyPredicted = false; + break; + } + } + + if (!renderFrameCorrectlyPredicted) { + // TODO: Can I also check whether the applied inputFrame on predictedRdf was "correctly predicted"? If it wasn't then a mismatch of positions is expected. + console.warn("render frame was incorrectly predicted\npredictedRdf=" + predictedRdf.toString() + "\nrdf=" + rdf.toString()); + } + } self.lastAllConfirmedRenderFrameId = rdf.id; if (rdf.id > self.chaserRenderFrameId) { // it must be true that "chaserRenderFrameId >= lastAllConfirmedRenderFrameId" @@ -1008,13 +1026,14 @@ cc.Class({ const wpos = self.virtualGridToWorldPos(immediatePlayerInfo.virtualGridX, immediatePlayerInfo.virtualGridY); const dx = (wpos[0] - playerRichInfo.node.x); const dy = (wpos[1] - playerRichInfo.node.y); - const justJiggling = (self.teleportEps1D >= Math.abs(dx) && self.teleportEps1D >= Math.abs(dy)); + const justJiggling = (self.jigglingEps1D >= Math.abs(dx) && self.jigglingEps1D >= Math.abs(dy)); if (!justJiggling) { - console.log("@renderFrameId=" + self.renderFrameId + ", teleporting playerId=" + playerId + ": '(" + playerRichInfo.node.x + ", " + playerRichInfo.node.y, ")' to '(" + wpos[0] + ", " + wpos[1] + ")'"); playerRichInfo.node.setPosition(wpos[0], wpos[1]); + playerRichInfo.virtualGridX = immediatePlayerInfo.virtualGridX; + playerRichInfo.virtualGridY = immediatePlayerInfo.virtualGridY; + playerRichInfo.scriptIns.scheduleNewDirection(immediatePlayerInfo.dir, false); + playerRichInfo.scriptIns.updateSpeed(immediatePlayerInfo.speed); } - playerRichInfo.scriptIns.scheduleNewDirection(immediatePlayerInfo.dir, false); - playerRichInfo.scriptIns.updateSpeed(immediatePlayerInfo.speed); }); }, @@ -1062,18 +1081,20 @@ cc.Class({ const playerCollider = collisionSysMap.get(collisionPlayerIndex); const player = renderFrame.players[playerId]; + const encodedInput = inputList[joinIndex - 1]; + const decodedInput = self.ctrl.decodeDirection(encodedInput); + if (0 == decodedInput.dx && 0 == decodedInput.dy) { + continue; + } + /* 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. */ - const cpos = self.virtualGridToPlayerColliderPos(player.virtualGridX, player.virtualGridY, self.playerRichInfoArr[j]); - playerCollider.x = cpos[0]; - playerCollider.y = cpos[1]; - - const encodedInput = inputList[joinIndex - 1]; - const decodedInput = self.ctrl.decodeDirection(encodedInput); - const baseChange = player.speed * self.virtualGridToWorldRatio * decodedInput.speedFactor; - playerCollider.x += baseChange * decodedInput.dx; - playerCollider.y += baseChange * decodedInput.dy; + const newVx = player.virtualGridX + (decodedInput.dx + player.speed * decodedInput.dx); + const newVy = player.virtualGridY + (decodedInput.dy + player.speed * decodedInput.dy); + const newCpos = self.virtualGridToPlayerColliderPos(newVx, newVy, self.playerRichInfoArr[j]); + playerCollider.x = newCpos[0]; + playerCollider.y = newCpos[1]; } collisionSys.update(); @@ -1154,14 +1175,16 @@ cc.Class({ }, worldToVirtualGridPos(x, y) { + // [WARNING] Introduces loss of precision! const self = this; // In JavaScript floating numbers suffer from seemingly non-deterministic arithmetics, and even if certain libs solved this issue by approaches such as fixed-point-number, they might not be used in other libs -- e.g. the "collision libs" we're interested in -- thus couldn't kill all pains. - let virtualGridX = parseInt(x * self.worldToVirtualGridRatio); - let virtualGridY = parseInt(y * self.worldToVirtualGridRatio); + let virtualGridX = Math.round(x * self.worldToVirtualGridRatio); + let virtualGridY = Math.round(y * self.worldToVirtualGridRatio); return [virtualGridX, virtualGridY]; }, virtualGridToWorldPos(vx, vy) { + // No loss of precision const self = this; let wx = parseFloat(vx) * self.virtualGridToWorldRatio; let wy = parseFloat(vy) * self.virtualGridToWorldRatio; diff --git a/frontend/assets/scripts/TouchEventsManager.js b/frontend/assets/scripts/TouchEventsManager.js index 4ae7832..7b3553b 100644 --- a/frontend/assets/scripts/TouchEventsManager.js +++ b/frontend/assets/scripts/TouchEventsManager.js @@ -1,18 +1,14 @@ window.DIRECTION_DECODER = [ - // The 3rd value matches low-precision constants in backend. - [0, 0, 0.0], - [0, +1, 1.0], - [0, -1, 1.0], - [+2, 0, 0.5], - [-2, 0, 0.5], - [+2, +1, 0.44], - [-2, -1, 0.44], - [+2, -1, 0.44], - [-2, +1, 0.44], - [+2, 0, 0.5], - [-2, 0, 0.5], - [0, +1, 1.0], - [0, -1, 1.0], + // The 3rd value matches low-precision constants in backend. + [0, 0], + [0, +2], + [0, -2], + [+2, 0], + [-2, 0], + [+1, +1], + [-1, -1], + [+1, -1], + [-1, +1], ]; cc.Class({ @@ -40,11 +36,11 @@ cc.Class({ type: cc.Float }, magicLeanLowerBound: { - default: 0.414, // Tangent of (PI/8). + default: 0.9, // Tangent of (PI/4) is 1.0. type: cc.Float }, magicLeanUpperBound: { - default: 2.414, // Tangent of (3*PI/8). + default: 1.1, type: cc.Float }, // For joystick ends. @@ -117,8 +113,8 @@ cc.Class({ _initTouchEvent() { const self = this; - const translationListenerNode = (self.translationListenerNode ? self.translationListenerNode : self.mapNode); - const zoomingListenerNode = (self.zoomingListenerNode ? self.zoomingListenerNode : self.mapNode); + const translationListenerNode = (self.translationListenerNode ? self.translationListenerNode : self.mapNode); + const zoomingListenerNode = (self.zoomingListenerNode ? self.zoomingListenerNode : self.mapNode); translationListenerNode.on(cc.Node.EventType.TOUCH_START, function(event) { self._touchStartEvent(event); @@ -132,7 +128,7 @@ cc.Class({ translationListenerNode.on(cc.Node.EventType.TOUCH_CANCEL, function(event) { self._touchEndEvent(event); }); - translationListenerNode.inTouchPoints = new Map(); + translationListenerNode.inTouchPoints = new Map(); zoomingListenerNode.on(cc.Node.EventType.TOUCH_START, function(event) { self._touchStartEvent(event); @@ -146,7 +142,7 @@ cc.Class({ zoomingListenerNode.on(cc.Node.EventType.TOUCH_CANCEL, function(event) { self._touchEndEvent(event); }); - zoomingListenerNode.inTouchPoints = new Map(); + zoomingListenerNode.inTouchPoints = new Map(); }, _isMapOverMoved(mapTargetPos) { @@ -155,7 +151,7 @@ cc.Class({ }, _touchStartEvent(event) { - const theListenerNode = event.target; + const theListenerNode = event.target; for (let touch of event._touches) { theListenerNode.inTouchPoints.set(touch._id, touch); } @@ -165,12 +161,12 @@ cc.Class({ if (ALL_MAP_STATES.VISUAL != this.mapScriptIns.state) { return; } - const theListenerNode = event.target; + const theListenerNode = event.target; const linearScaleFacBase = this.linearScaleFacBase; // Not used yet. if (1 != theListenerNode.inTouchPoints.size) { return; } - if (!theListenerNode.inTouchPoints.has(event.currentTouch._id)) { + if (!theListenerNode.inTouchPoints.has(event.currentTouch._id)) { return; } const diffVec = event.currentTouch._point.sub(event.currentTouch._startPoint); @@ -189,9 +185,9 @@ cc.Class({ if (ALL_MAP_STATES.VISUAL != this.mapScriptIns.state) { return; } - const theListenerNode = event.target; + const theListenerNode = event.target; if (2 != theListenerNode.inTouchPoints.size) { - return; + return; } if (2 == event._touches.length) { const firstTouch = event._touches[0]; @@ -219,13 +215,13 @@ cc.Class({ } this.mainCamera.zoomRatio = targetScale; for (let child of this.mainCameraNode.children) { - child.setScale(1/targetScale); + child.setScale(1 / targetScale); } } }, _touchEndEvent(event) { - const theListenerNode = event.target; + const theListenerNode = event.target; do { if (!theListenerNode.inTouchPoints.has(event.currentTouch._id)) { break; @@ -241,7 +237,7 @@ cc.Class({ break; } - // TODO: Handle single-finger-click event. + // TODO: Handle single-finger-click event. } while (false); this.cachedStickHeadPosition = cc.v2(0.0, 0.0); for (let touch of event._touches) { @@ -266,16 +262,16 @@ cc.Class({ encodedIdx: 0 }; if (Math.abs(continuousDx) < eps && Math.abs(continuousDy) < eps) { - return ret; + return ret; } if (Math.abs(continuousDx) < eps) { ret.dx = 0; if (0 < continuousDy) { - ret.dy = +1; // up + ret.dy = +2; // up ret.encodedIdx = 1; } else { - ret.dy = -1; // down + ret.dy = -2; // down ret.encodedIdx = 2; } } else if (Math.abs(continuousDy) < eps) { @@ -291,42 +287,42 @@ cc.Class({ const criticalRatio = continuousDy / continuousDx; if (criticalRatio > this.magicLeanLowerBound && criticalRatio < this.magicLeanUpperBound) { if (0 < continuousDx) { - ret.dx = +2; + ret.dx = +1; ret.dy = +1; ret.encodedIdx = 5; } else { - ret.dx = -2; + ret.dx = -1; ret.dy = -1; ret.encodedIdx = 6; } } else if (criticalRatio > -this.magicLeanUpperBound && criticalRatio < -this.magicLeanLowerBound) { if (0 < continuousDx) { - ret.dx = +2; + ret.dx = +1; ret.dy = -1; ret.encodedIdx = 7; } else { - ret.dx = -2; + ret.dx = -1; ret.dy = +1; ret.encodedIdx = 8; } } else { - if (Math.abs(criticalRatio) < 1) { + if (Math.abs(criticalRatio) < 0.1) { ret.dy = 0; if (0 < continuousDx) { - ret.dx = +2; - ret.encodedIdx = 9; + ret.dx = +2; // right + ret.encodedIdx = 3; } else { - ret.dx = -2; - ret.encodedIdx = 10; + ret.dx = -2; // left + ret.encodedIdx = 4; } - } else { + } else if (Math.abs(criticalRatio) > 0.9) { ret.dx = 0; if (0 < continuousDy) { - ret.dy = +1; - ret.encodedIdx = 11; + ret.dy = +2; // up + ret.encodedIdx = 1; } else { - ret.dy = -1; - ret.encodedIdx = 12; + ret.dy = -2; // down + ret.encodedIdx = 2; } } } @@ -337,16 +333,15 @@ cc.Class({ decodeDirection(encodedDirection) { const mapped = window.DIRECTION_DECODER[encodedDirection]; if (null == mapped) { - console.error("Unexpected encodedDirection = ", encodedDirection); + console.error("Unexpected encodedDirection = ", encodedDirection); } return { dx: mapped[0], - dy: mapped[1], - speedFactor: mapped[2], - } + dy: mapped[1], + }; }, getDiscretizedDirection() { - return this.discretizeDirection(this.cachedStickHeadPosition.x, this.cachedStickHeadPosition.y, this.joyStickEps); + return this.discretizeDirection(this.cachedStickHeadPosition.x, this.cachedStickHeadPosition.y, this.joyStickEps); } });