diff --git a/battle_srv/models/pb_type_convert.go b/battle_srv/models/pb_type_convert.go index e854df8..ae42687 100644 --- a/battle_srv/models/pb_type_convert.go +++ b/battle_srv/models/pb_type_convert.go @@ -17,12 +17,16 @@ func toPbPlayers(modelInstances map[int32]*Player, withMetaInfo bool) map[int32] VirtualGridY: last.VirtualGridY, DirX: last.DirX, DirY: last.DirY, - ColliderRadius: last.ColliderRadius, + VelX: last.VelX, + VelY: last.VelY, Speed: last.Speed, BattleState: last.BattleState, + CharacterState: last.CharacterState, + InAir: last.InAir, + JoinIndex: last.JoinIndex, + ColliderRadius: last.ColliderRadius, Score: last.Score, Removed: last.Removed, - JoinIndex: last.JoinIndex, } if withMetaInfo { toRet[k].Name = last.Name diff --git a/battle_srv/models/room.go b/battle_srv/models/room.go index 68b13ff..35c9df1 100644 --- a/battle_srv/models/room.go +++ b/battle_srv/models/room.go @@ -201,6 +201,7 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocke pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK pPlayerFromDbInit.Speed = pR.PlayerDefaultSpeed // Hardcoded pPlayerFromDbInit.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded + pPlayerFromDbInit.InAir = true // Hardcoded pR.Players[playerId] = pPlayerFromDbInit pR.PlayerDownsyncSessionDict[playerId] = session @@ -232,6 +233,7 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso pEffectiveInRoomPlayerInstance.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK pEffectiveInRoomPlayerInstance.Speed = pR.PlayerDefaultSpeed // Hardcoded pEffectiveInRoomPlayerInstance.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded + pEffectiveInRoomPlayerInstance.InAir = true // Hardcoded pR.PlayerDownsyncSessionDict[playerId] = session pR.PlayerSignalToCloseDict[playerId] = signalToCloseConnOfThisPlayer @@ -422,7 +424,7 @@ func (pR *Room) StartBattle() { stCalculation := utils.UnixtimeNano() elapsedNanosSinceLastFrameIdTriggered := stCalculation - pR.LastRenderFrameIdTriggeredAt if elapsedNanosSinceLastFrameIdTriggered < pR.RollbackEstimatedDtNanos { - Logger.Warn(fmt.Sprintf("renderFrameId=%v@roomId=%v: Is backend running too fast? elapsedNanosSinceLastFrameIdTriggered=%v", pR.RenderFrameId, pR.Id, elapsedNanosSinceLastFrameIdTriggered)) + Logger.Debug(fmt.Sprintf("renderFrameId=%v@roomId=%v: Is backend running too fast? elapsedNanosSinceLastFrameIdTriggered=%v", pR.RenderFrameId, pR.Id, elapsedNanosSinceLastFrameIdTriggered)) } if pR.RenderFrameId > pR.BattleDurationFrames { @@ -566,12 +568,12 @@ func (pR *Room) OnBattleCmdReceived(pReq *WsReq) { atomic.StoreInt32(&(player.AckingFrameId), ackingFrameId) atomic.StoreInt32(&(player.AckingInputFrameId), ackingInputFrameId) - Logger.Debug(fmt.Sprintf("OnBattleCmdReceived-InputsBufferLock about to lock: roomId=%v, fromPlayerId=%v", pR.Id, playerId)) + //Logger.Debug(fmt.Sprintf("OnBattleCmdReceived-InputsBufferLock about to lock: roomId=%v, fromPlayerId=%v", pR.Id, playerId)) pR.InputsBufferLock.Lock() - Logger.Debug(fmt.Sprintf("OnBattleCmdReceived-InputsBufferLock locked: roomId=%v, fromPlayerId=%v", pR.Id, playerId)) + //Logger.Debug(fmt.Sprintf("OnBattleCmdReceived-InputsBufferLock locked: roomId=%v, fromPlayerId=%v", pR.Id, playerId)) defer func() { pR.InputsBufferLock.Unlock() - Logger.Debug(fmt.Sprintf("OnBattleCmdReceived-InputsBufferLock unlocked: roomId=%v, fromPlayerId=%v", pR.Id, playerId)) + //Logger.Debug(fmt.Sprintf("OnBattleCmdReceived-InputsBufferLock unlocked: roomId=%v, fromPlayerId=%v", pR.Id, playerId)) }() inputsBufferSnapshot := pR.markConfirmationIfApplicable(inputFrameUpsyncBatch, playerId, player) @@ -757,7 +759,7 @@ func (pR *Room) OnDismissed() { pR.InputFrameUpsyncDelayTolerance = 2 pR.MaxChasingRenderFramesPerUpdate = 8 - pR.BackendDynamicsEnabled = false // [WARNING] When "false", recovery upon reconnection wouldn't work! + pR.BackendDynamicsEnabled = true // [WARNING] When "false", recovery upon reconnection wouldn't work! punchSkillId := int32(1) pR.MeleeSkillConfig = make(map[int32]*MeleeBullet, 0) pR.MeleeSkillConfig[punchSkillId] = &MeleeBullet{ @@ -1286,7 +1288,10 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF VirtualGridY: currPlayerDownsync.VirtualGridY, DirX: currPlayerDownsync.DirX, DirY: currPlayerDownsync.DirY, + VelX: currPlayerDownsync.VelX, + VelY: currPlayerDownsync.VelY, CharacterState: currPlayerDownsync.CharacterState, + InAir: true, Speed: currPlayerDownsync.Speed, BattleState: currPlayerDownsync.BattleState, Score: currPlayerDownsync.Score, @@ -1301,47 +1306,106 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF } } - toRet := &RoomDownsyncFrame{ - Id: currRenderFrame.Id + 1, - Players: nextRenderFramePlayers, - CountdownNanos: (pR.BattleDurationNanos - int64(currRenderFrame.Id)*pR.RollbackEstimatedDtNanos), - MeleeBullets: make([]*MeleeBullet, 0), // Is there any better way to reduce malloc/free impact, e.g. smart prediction for fixed memory allocation? - } + nextRenderFrameMeleeBullets := make([]*MeleeBullet, 0, len(currRenderFrame.MeleeBullets)) // Is there any better way to reduce malloc/free impact, e.g. smart prediction for fixed memory allocation? // Guaranteed determinism regardless of traversal order - jumpTriggered := make([]bool, pR.Capacity) - movements := make([]Vec2D, pR.Capacity) - bulletPushbacks := make([]Vec2D, pR.Capacity) effPushbacks := make([]Vec2D, pR.Capacity) + hardPushbackNorms := make([][]Vec2D, pR.Capacity) - // Reset playerCollider position from the "virtual grid position" + // 1. Process player inputs + if nil != delayedInputFrame { + var delayedInputFrameForPrevRenderFrame *InputFrameDownsync = nil + tmp := pR.InputsBuffer.GetByFrameId(pR.ConvertToInputFrameId(currRenderFrame.Id-1, pR.InputDelayFrames)) + if nil != tmp { + delayedInputFrameForPrevRenderFrame = tmp.(*InputFrameDownsync) + } + inputList := delayedInputFrame.InputList + for _, player := range pR.PlayersArr { + playerId := player.Id + joinIndex := player.JoinIndex + currPlayerDownsync, thatPlayerInNextFrame := currRenderFrame.Players[playerId], nextRenderFramePlayers[playerId] + if 0 < thatPlayerInNextFrame.FramesToRecover { + continue + } + decodedInput := pR.decodeInput(inputList[joinIndex-1]) + prevBtnALevel, prevBtnBLevel := int32(0), int32(0) + if nil != delayedInputFrameForPrevRenderFrame { + prevDecodedInput := pR.decodeInput(delayedInputFrameForPrevRenderFrame.InputList[joinIndex-1]) + prevBtnALevel = prevDecodedInput.BtnALevel + prevBtnBLevel = prevDecodedInput.BtnBLevel + } + + if decodedInput.BtnBLevel > prevBtnBLevel { + characStateAlreadyInAir := false + if ATK_CHARACTER_STATE_INAIR_IDLE1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_ATK1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_ATKED1 == thatPlayerInNextFrame.CharacterState { + characStateAlreadyInAir = true + } + characStateIsInterruptWaivable := false + if ATK_CHARACTER_STATE_IDLE1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_WALKING == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_IDLE1 == thatPlayerInNextFrame.CharacterState { + characStateIsInterruptWaivable = true + } + if !characStateAlreadyInAir && characStateIsInterruptWaivable { + thatPlayerInNextFrame.VelY = pR.JumpingInitVelY + Logger.Info(fmt.Sprintf("playerId=%v, joinIndex=%v triggered a jump at renderFrame.id=%v, delayedInputFrame.id=%v, nextVelY=%v", playerId, joinIndex, currRenderFrame.Id, delayedInputFrame.InputFrameId, thatPlayerInNextFrame.VelY)) + } + } + + if decodedInput.BtnALevel > prevBtnALevel { + punchSkillId := int32(1) + punchConfig := pR.MeleeSkillConfig[punchSkillId] + var newMeleeBullet MeleeBullet = *punchConfig + newMeleeBullet.BattleLocalId = pR.BulletBattleLocalIdCounter + pR.BulletBattleLocalIdCounter += 1 + newMeleeBullet.OffenderJoinIndex = joinIndex + newMeleeBullet.OffenderPlayerId = playerId + newMeleeBullet.OriginatedRenderFrameId = currRenderFrame.Id + nextRenderFrameMeleeBullets = append(nextRenderFrameMeleeBullets, &newMeleeBullet) + thatPlayerInNextFrame.FramesToRecover = newMeleeBullet.RecoveryFrames + thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_ATK1 + if false == currPlayerDownsync.InAir { + thatPlayerInNextFrame.VelX = 0 + } + Logger.Debug(fmt.Sprintf("roomId=%v, playerId=%v triggered a rising-edge of btnA at currRenderFrame.id=%v, delayedInputFrame.id=%v", pR.Id, playerId, currRenderFrame.Id, delayedInputFrame.InputFrameId)) + + } else if decodedInput.BtnALevel < prevBtnALevel { + Logger.Debug(fmt.Sprintf("roomId=%v, playerId=%v triggered a falling-edge of btnA at currRenderFrame.id=%v, delayedInputFrame.id=%v", pR.Id, playerId, currRenderFrame.Id, delayedInputFrame.InputFrameId)) + } else { + // No bullet trigger, process movement inputs + // Note that by now "0 == thatPlayerInNextFrame.FramesToRecover", we should change "CharacterState" to "WALKING" or "IDLE" depending on player inputs + if 0 != decodedInput.Dx || 0 != decodedInput.Dy { + thatPlayerInNextFrame.DirX = decodedInput.Dx + thatPlayerInNextFrame.DirY = decodedInput.Dy + thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_WALKING + thatPlayerInNextFrame.VelX = decodedInput.Dx * currPlayerDownsync.Speed + } else { + thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1 + thatPlayerInNextFrame.VelX = 0 + } + } + } + } + + // 2. Process player movement for _, player := range pR.PlayersArr { playerId := player.Id joinIndex := player.JoinIndex - jumpTriggered[joinIndex-1] = false - movements[joinIndex-1].X, movements[joinIndex-1].Y = float64(0), float64(0) - bulletPushbacks[joinIndex-1].X, bulletPushbacks[joinIndex-1].Y = float64(0), float64(0) - effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y = float64(0), float64(0) - currPlayerDownsync := currRenderFrame.Players[playerId] - thatPlayerInNextFrame := nextRenderFramePlayers[playerId] - newVx, newVy := currPlayerDownsync.VirtualGridX, currPlayerDownsync.VirtualGridY collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex playerCollider := collisionSysMap[collisionPlayerIndex] - playerCollider.X, playerCollider.Y = VirtualGridToPolygonColliderAnchorPos(newVx, newVy, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.VirtualGridToWorldRatio) + currPlayerDownsync, thatPlayerInNextFrame := currRenderFrame.Players[playerId], nextRenderFramePlayers[playerId] + // Reset playerCollider position from the "virtual grid position" + newVx, newVy := currPlayerDownsync.VirtualGridX+currPlayerDownsync.VelX, currPlayerDownsync.VirtualGridY+currPlayerDownsync.VelY + + playerCollider.X, playerCollider.Y = VirtualGridToPolygonColliderAnchorPos(newVx, newVy, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.VirtualGridToWorldRatio) + // Update in the collision system + playerCollider.Update() - movements[joinIndex-1].X, movements[joinIndex-1].Y = VirtualGridToWorldPos(currPlayerDownsync.VelX, currPlayerDownsync.VelY, pR.VirtualGridToWorldRatio) - playerCollider.X += movements[joinIndex-1].X - playerCollider.Y += movements[joinIndex-1].Y if currPlayerDownsync.InAir { thatPlayerInNextFrame.VelX += pR.GravityX thatPlayerInNextFrame.VelY += pR.GravityY } - - // Update in the collision system - playerCollider.Update() } - // Check bullet-anything collisions first, because the pushbacks caused by bullets might later be reverted by player-barrier collision + // 3. Add bullet colliders into collision system bulletColliders := make(map[int32]*resolv.Object, 0) // Will all be removed at the end of `applyInputFrameDownsyncDynamicsOnSingleRenderFrame` due to the need for being rollback-compatible removedBulletsAtCurrFrame := make(map[int32]int32, 0) for _, meleeBullet := range currRenderFrame.MeleeBullets { @@ -1368,141 +1432,70 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF } } - for _, bulletCollider := range bulletColliders { - shouldRemove := false - meleeBullet := bulletCollider.Data.(*MeleeBullet) - collisionBulletIndex := COLLISION_BULLET_INDEX_PREFIX + meleeBullet.BattleLocalId - bulletShape := bulletCollider.Shape.(*resolv.ConvexPolygon) - if collision := bulletCollider.Check(0, 0); collision != nil { - offender := currRenderFrame.Players[meleeBullet.OffenderPlayerId] + // 4. Invoke collision system stepping (no-op for backend collision lib) + + // 5. Calc pushbacks for each player (after its movement) w/o bullets + for _, player := range pR.PlayersArr { + joinIndex := player.JoinIndex + playerId := player.Id + collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex + playerCollider := collisionSysMap[collisionPlayerIndex] + playerShape := playerCollider.Shape.(*resolv.ConvexPolygon) + hardPushbackNorms[joinIndex-1] = pR.calcHardPushbacksNorms(playerCollider, playerShape, pR.SnapIntoPlatformOverlap, &(effPushbacks[joinIndex-1])) + if 0 < len(hardPushbackNorms[joinIndex-1]) { + Logger.Debug(fmt.Sprintf("playerId=%d, joinIndex=%d got %d non-empty hardPushbacks at renderFrame.id=%d", playerId, joinIndex, len(hardPushbackNorms), currRenderFrame.Id)) + } + currPlayerDownsync, thatPlayerInNextFrame := currRenderFrame.Players[playerId], nextRenderFramePlayers[playerId] + fallStopping := false + possiblyFallStoppedOnAnotherPlayer := false + if collision := playerCollider.Check(0, 0); collision != nil { for _, obj := range collision.Objects { - defenderShape := obj.Shape.(*resolv.ConvexPolygon) - switch t := obj.Data.(type) { + isBarrier, isAnotherPlayer, isBullet := false, false, false + switch obj.Data.(type) { + case *Barrier: + isBarrier = true case *Player: - if meleeBullet.OffenderPlayerId != t.Id { - if overlapped, _, _, _ := CalcPushbacks(0, 0, bulletShape, defenderShape); overlapped { - xfac := float64(1.0) // By now, straight Punch offset doesn't respect "y-axis" - if 0 > offender.DirX { - xfac = float64(-1.0) - } - bulletPushbacks[t.JoinIndex-1].X += xfac * meleeBullet.Pushback - thatAckedPlayerInCurFrame := currRenderFrame.Players[t.Id] - thatAckedPlayerInNextFrame := nextRenderFramePlayers[t.Id] - thatAckedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_ATKED1 - if thatAckedPlayerInCurFrame.InAir { - thatAckedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_INAIR_ATKED1 - } - oldFramesToRecover := nextRenderFramePlayers[t.Id].FramesToRecover - if meleeBullet.HitStunFrames > oldFramesToRecover { - nextRenderFramePlayers[t.Id].FramesToRecover = meleeBullet.HitStunFrames - } - Logger.Debug(fmt.Sprintf("roomId=%v, a meleeBullet collides w/ player at currRenderFrame.id=%v: b=%v, p=%v", pR.Id, currRenderFrame.Id, ConvexPolygonStr(bulletShape), ConvexPolygonStr(defenderShape))) + isAnotherPlayer = true + case *MeleeBullet: + isBullet = true + } + if isBullet { + // ignore bullets for this step + continue + } + bShape := obj.Shape.(*resolv.ConvexPolygon) + if overlapped, pushbackX, pushbackY, overlapResult := CalcPushbacks(0, 0, playerShape, bShape); overlapped { + normAlignmentWithGravity := (overlapResult.OverlapX*float64(0) + overlapResult.OverlapY*float64(-1.0)) + landedOnGravityPushback := (pR.SnapIntoPlatformThreshold < normAlignmentWithGravity) // prevents false snapping on the lateral sides + if landedOnGravityPushback { + // kindly note that one player might land on top of another player, and snapping is also required in such case + pushbackX, pushbackY = (overlapResult.Overlap-pR.SnapIntoPlatformOverlap)*overlapResult.OverlapX, (overlapResult.Overlap-pR.SnapIntoPlatformOverlap)*overlapResult.OverlapY + thatPlayerInNextFrame.InAir = false + } + for _, hardPushbackNorm := range hardPushbackNorms[joinIndex-1] { + projectedMagnitude := pushbackX*hardPushbackNorm.X + pushbackY*hardPushbackNorm.Y + if isBarrier || (0 > projectedMagnitude && isAnotherPlayer) { + pushbackX -= projectedMagnitude * hardPushbackNorm.X + pushbackY -= projectedMagnitude * hardPushbackNorm.Y } } - default: - Logger.Debug(fmt.Sprintf("Bullet %v collided with non-player %v: roomId=%v, currRenderFrame.Id=%v, delayedInputFrame.Id=%v, objDataType=%t, objData=%v", ConvexPolygonStr(bulletShape), ConvexPolygonStr(defenderShape), pR.Id, currRenderFrame.Id, delayedInputFrame.InputFrameId, obj.Data, obj.Data)) + if currPlayerDownsync.InAir && landedOnGravityPushback { + fallStopping = true + if isAnotherPlayer { + possiblyFallStoppedOnAnotherPlayer = true + } + } + effPushbacks[joinIndex-1].X += pushbackX + effPushbacks[joinIndex-1].Y += pushbackY } } - shouldRemove = true - } - if shouldRemove { - removedBulletsAtCurrFrame[collisionBulletIndex] = 1 - } - } - - for _, meleeBullet := range currRenderFrame.MeleeBullets { - collisionBulletIndex := COLLISION_BULLET_INDEX_PREFIX + meleeBullet.BattleLocalId - if bulletCollider, existent := collisionSysMap[collisionBulletIndex]; existent { - bulletCollider.Space.Remove(bulletCollider) - delete(collisionSysMap, collisionBulletIndex) - } - if _, existent := removedBulletsAtCurrFrame[collisionBulletIndex]; existent { - continue - } - toRet.MeleeBullets = append(toRet.MeleeBullets, meleeBullet) - } - - if nil != delayedInputFrame { - var delayedInputFrameForPrevRenderFrame *InputFrameDownsync = nil - tmp := pR.InputsBuffer.GetByFrameId(pR.ConvertToInputFrameId(currRenderFrame.Id-1, pR.InputDelayFrames)) - if nil != tmp { - delayedInputFrameForPrevRenderFrame = tmp.(*InputFrameDownsync) - } - inputList := delayedInputFrame.InputList - // Process player inputs - for _, player := range pR.PlayersArr { - playerId := player.Id - joinIndex := player.JoinIndex - collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex - playerCollider := collisionSysMap[collisionPlayerIndex] - thatPlayerInNextFrame := nextRenderFramePlayers[playerId] - if 0 < thatPlayerInNextFrame.FramesToRecover { - // No need to process inputs for this player, but there might be bullet pushbacks on this player - // Also note that in this case we keep "CharacterState" of this player from last render frame - playerCollider.X += bulletPushbacks[joinIndex-1].X - playerCollider.Y += bulletPushbacks[joinIndex-1].Y - // Update in the collision system - playerCollider.Update() - if 0 != bulletPushbacks[joinIndex-1].X || 0 != bulletPushbacks[joinIndex-1].Y { - Logger.Debug(fmt.Sprintf("roomId=%v, playerId=%v is pushed back by (%.2f, %.2f) by bullet impacts, now its framesToRecover is %d at currRenderFrame.id=%v", pR.Id, playerId, bulletPushbacks[joinIndex-1].X, bulletPushbacks[joinIndex-1].Y, thatPlayerInNextFrame.FramesToRecover, currRenderFrame.Id)) - } - continue - } - currPlayerDownsync := currRenderFrame.Players[playerId] - decodedInput := pR.decodeInput(inputList[joinIndex-1]) - prevBtnALevel, prevBtnBLevel := int32(0), int32(0) - if nil != delayedInputFrameForPrevRenderFrame { - prevDecodedInput := pR.decodeInput(delayedInputFrameForPrevRenderFrame.InputList[joinIndex-1]) - prevBtnALevel = prevDecodedInput.BtnALevel - prevBtnBLevel = prevDecodedInput.BtnBLevel - } - - if decodedInput.BtnBLevel > prevBtnBLevel { - characStateAlreadyInAir := false - if ATK_CHARACTER_STATE_INAIR_IDLE1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_ATK1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_ATKED1 == thatPlayerInNextFrame.CharacterState { - characStateAlreadyInAir = true - } - characStateIsInterruptWaivable := false - if ATK_CHARACTER_STATE_IDLE1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_WALKING == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_IDLE1 == thatPlayerInNextFrame.CharacterState { - characStateIsInterruptWaivable = true - } - if !characStateAlreadyInAir && characStateIsInterruptWaivable { - thatPlayerInNextFrame.VelY = pR.JumpingInitVelY - jumpTriggered[joinIndex-1] = true - Logger.Info(fmt.Sprintf("playerId=%v, joinIndex=%v triggered a rising-edge of btnB at renderFrame.id=%v, delayedInputFrame.id=%v, nextVelY=%v, characStateAlreadyInAir=%v, characStateIsInterruptWaivable=%v", playerId, joinIndex, currRenderFrame.Id, delayedInputFrame.InputFrameId, thatPlayerInNextFrame.VelY, characStateAlreadyInAir, characStateIsInterruptWaivable)) - } - } - - if decodedInput.BtnALevel > prevBtnALevel { - punchSkillId := int32(1) - punchConfig := pR.MeleeSkillConfig[punchSkillId] - var newMeleeBullet MeleeBullet = *punchConfig - newMeleeBullet.BattleLocalId = pR.BulletBattleLocalIdCounter - pR.BulletBattleLocalIdCounter += 1 - newMeleeBullet.OffenderJoinIndex = joinIndex - newMeleeBullet.OffenderPlayerId = playerId - newMeleeBullet.OriginatedRenderFrameId = currRenderFrame.Id - toRet.MeleeBullets = append(toRet.MeleeBullets, &newMeleeBullet) - thatPlayerInNextFrame.FramesToRecover = newMeleeBullet.RecoveryFrames - thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_ATK1 - if false == currPlayerDownsync.InAir { - thatPlayerInNextFrame.VelX = 0 - } - Logger.Debug(fmt.Sprintf("roomId=%v, playerId=%v triggered a rising-edge of btnA at currRenderFrame.id=%v, delayedInputFrame.id=%v", pR.Id, playerId, currRenderFrame.Id, delayedInputFrame.InputFrameId)) - - } else if decodedInput.BtnALevel < prevBtnALevel { - Logger.Debug(fmt.Sprintf("roomId=%v, playerId=%v triggered a falling-edge of btnA at currRenderFrame.id=%v, delayedInputFrame.id=%v", pR.Id, playerId, currRenderFrame.Id, delayedInputFrame.InputFrameId)) - } else { - // No bullet trigger, process movement inputs - // Note that by now "0 == thatPlayerInNextFrame.FramesToRecover", we should change "CharacterState" to "WALKING" or "IDLE" depending on player inputs - if 0 != decodedInput.Dx || 0 != decodedInput.Dy { - thatPlayerInNextFrame.DirX = decodedInput.Dx - thatPlayerInNextFrame.DirY = decodedInput.Dy - thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_WALKING - thatPlayerInNextFrame.VelX = decodedInput.Dx * currPlayerDownsync.Speed - } else { - thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1 - thatPlayerInNextFrame.VelX = 0 + if fallStopping { + thatPlayerInNextFrame.VelX = 0 + thatPlayerInNextFrame.VelY = 0 + thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1 + thatPlayerInNextFrame.FramesToRecover = 0 + if possiblyFallStoppedOnAnotherPlayer { + Logger.Info(fmt.Sprintf("playerId=%d, joinIndex=%d possiblyFallStoppedOnAnotherPlayer with effPushback={%.2f, %.2f} at renderFrame.id=%d", playerId, joinIndex, effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y, currRenderFrame.Id)) } } if currPlayerDownsync.InAir { @@ -1518,62 +1511,93 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF } } } - - // handle pushbacks upon collision after all movements treated as simultaneous - for _, player := range pR.PlayersArr { - joinIndex := player.JoinIndex - collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex - playerCollider := collisionSysMap[collisionPlayerIndex] - currPlayerDownsync := currRenderFrame.Players[playerId] - thatPlayerInNextFrame := nextRenderFramePlayers[playerId] - fallStopping := false - snappedIntoPlatformEx, snappedIntoPlatformEy := float64(0), float64(0) - if collision := playerCollider.Check(0, 0); collision != nil { - playerShape := playerCollider.Shape.(*resolv.ConvexPolygon) - for _, obj := range collision.Objects { - barrierShape := obj.Shape.(*resolv.ConvexPolygon) - if overlapped, pushbackX, pushbackY, overlapResult := CalcPushbacks(0, 0, playerShape, barrierShape); overlapped { - if nil == obj.Data { - // "nil == obj.Data" implies a barrier - const flatEnough = (self.snapIntoPlatformThreshold < normAlignmentWithGravity); // prevents false snapping on the lateral sides - const remainsNotInAir = (!currPlayerDownsync.inAir && flatEnough); - const localFallStopping = (currPlayerDownsync.inAir && flatEnough); - if (remainsNotInAir || localFallStopping) { - fallStopping |= localFallStopping; - [pushbackX, pushbackY] = [(result2.overlap - self.snapIntoPlatformOverlap) * result2.overlap_x, (result2.overlap - self.snapIntoPlatformOverlap) * result2.overlap_y] - snappedIntoPlatformEx = -result2.overlap_y; - snappedIntoPlatformEy = result2.overlap_x; - if (snappedIntoPlatformEx * currPlayerDownsync.dirX + snappedIntoPlatformEy * currPlayerDownsync.dirY) { - [snappedIntoPlatformEx, snappedIntoPlatformEy] = [-snappedIntoPlatformEx, -snappedIntoPlatformEy]; - } - } - } - Logger.Debug(fmt.Sprintf("Overlapped: a=%v, b=%v, pushbackX=%v, pushbackY=%v", ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape), pushbackX, pushbackY)) - effPushbacks[joinIndex-1].X += pushbackX - effPushbacks[joinIndex-1].Y += pushbackY - } else { - Logger.Debug(fmt.Sprintf("Collided BUT not overlapped: a=%v, b=%v, overlapResult=%v", ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape), overlapResult)) - } - } - } - } - - for _, player := range pR.PlayersArr { - playerId := player.Id - joinIndex := player.JoinIndex - collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex - playerCollider := collisionSysMap[collisionPlayerIndex] - - // Update "virtual grid position" - newVx, newVy := PolygonColliderAnchorToVirtualGridPos(playerCollider.X-effPushbacks[joinIndex-1].X, playerCollider.Y-effPushbacks[joinIndex-1].Y, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.WorldToVirtualGridRatio) - thatPlayerInNextFrame := nextRenderFramePlayers[playerId] - thatPlayerInNextFrame.VirtualGridX, thatPlayerInNextFrame.VirtualGridY = newVx, newVy - } - - Logger.Debug(fmt.Sprintf("After applyInputFrameDownsyncDynamicsOnSingleRenderFrame: currRenderFrame.Id=%v, inputList=%v, currRenderFrame.Players=%v, nextRenderFramePlayers=%v", currRenderFrame.Id, inputList, currRenderFrame.Players, nextRenderFramePlayers)) } - return toRet + // 6. Check bullet-anything collisions + for _, bulletCollider := range bulletColliders { + shouldRemove := false + meleeBullet := bulletCollider.Data.(*MeleeBullet) + collisionBulletIndex := COLLISION_BULLET_INDEX_PREFIX + meleeBullet.BattleLocalId + bulletShape := bulletCollider.Shape.(*resolv.ConvexPolygon) + if collision := bulletCollider.Check(0, 0); collision != nil { + offender := currRenderFrame.Players[meleeBullet.OffenderPlayerId] + for _, obj := range collision.Objects { + defenderShape := obj.Shape.(*resolv.ConvexPolygon) + switch t := obj.Data.(type) { + case *Player: + if meleeBullet.OffenderPlayerId != t.Id { + if overlapped, _, _, _ := CalcPushbacks(0, 0, bulletShape, defenderShape); overlapped { + joinIndex := t.JoinIndex + xfac := float64(1.0) // By now, straight Punch offset doesn't respect "y-axis" + if 0 > offender.DirX { + xfac = float64(-1.0) + } + pushbackX, pushbackY := -xfac*meleeBullet.Pushback, float64(0) + + for _, hardPushbackNorm := range hardPushbackNorms[joinIndex-1] { + projectedMagnitude := pushbackX*hardPushbackNorm.X + pushbackY*hardPushbackNorm.Y + if 0 > projectedMagnitude { + Logger.Info(fmt.Sprintf("defenderPlayerId=%d, joinIndex=%d reducing bullet pushback={%.2f, %.2f} by {%.2f, %.2f} where hardPushbackNorm={%.2f, %.2f}, projectedMagnitude=%.2f at renderFrame.id=%d", t.Id, joinIndex, pushbackX, pushbackY, projectedMagnitude*hardPushbackNorm.X, projectedMagnitude*hardPushbackNorm.Y, hardPushbackNorm.X, hardPushbackNorm.Y, projectedMagnitude, currRenderFrame.Id)) + pushbackX -= projectedMagnitude * hardPushbackNorm.X + pushbackY -= projectedMagnitude * hardPushbackNorm.Y + } + } + + effPushbacks[joinIndex-1].X += pushbackX + effPushbacks[joinIndex-1].Y += pushbackY + atkedPlayerInCurFrame, atkedPlayerInNextFrame := currRenderFrame.Players[t.Id], nextRenderFramePlayers[t.Id] + atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_ATKED1 + if atkedPlayerInCurFrame.InAir { + atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_INAIR_ATKED1 + } + oldFramesToRecover := nextRenderFramePlayers[t.Id].FramesToRecover + if meleeBullet.HitStunFrames > oldFramesToRecover { + atkedPlayerInNextFrame.FramesToRecover = meleeBullet.HitStunFrames + } + Logger.Debug(fmt.Sprintf("roomId=%v, a meleeBullet collides w/ player at currRenderFrame.id=%v: b=%v, p=%v", pR.Id, currRenderFrame.Id, ConvexPolygonStr(bulletShape), ConvexPolygonStr(defenderShape))) + } + } + default: + Logger.Debug(fmt.Sprintf("Bullet %v collided with non-player %v: roomId=%v, currRenderFrame.Id=%v, delayedInputFrame.Id=%v, objDataType=%t, objData=%v", ConvexPolygonStr(bulletShape), ConvexPolygonStr(defenderShape), pR.Id, currRenderFrame.Id, delayedInputFrame.InputFrameId, obj.Data, obj.Data)) + } + } + shouldRemove = true + } + if shouldRemove { + removedBulletsAtCurrFrame[collisionBulletIndex] = 1 + } + } + + // [WARNING] Remove bullets from collisionSys ANYWAY for the convenience of rollback + for _, meleeBullet := range currRenderFrame.MeleeBullets { + collisionBulletIndex := COLLISION_BULLET_INDEX_PREFIX + meleeBullet.BattleLocalId + if bulletCollider, existent := collisionSysMap[collisionBulletIndex]; existent { + bulletCollider.Space.Remove(bulletCollider) + delete(collisionSysMap, collisionBulletIndex) + } + if _, existent := removedBulletsAtCurrFrame[collisionBulletIndex]; existent { + continue + } + nextRenderFrameMeleeBullets = append(nextRenderFrameMeleeBullets, meleeBullet) + } + + // 7. Get players out of stuck barriers if there's any + for _, player := range pR.PlayersArr { + joinIndex := player.JoinIndex + playerId := player.Id + collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex + playerCollider := collisionSysMap[collisionPlayerIndex] + // Update "virtual grid position" + thatPlayerInNextFrame := nextRenderFramePlayers[playerId] + thatPlayerInNextFrame.VirtualGridX, thatPlayerInNextFrame.VirtualGridY = PolygonColliderAnchorToVirtualGridPos(playerCollider.X-effPushbacks[joinIndex-1].X, playerCollider.Y-effPushbacks[joinIndex-1].Y, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.WorldToVirtualGridRatio) + } + + return &RoomDownsyncFrame{ + Id: currRenderFrame.Id + 1, + Players: nextRenderFramePlayers, + CountdownNanos: (pR.BattleDurationNanos - int64(currRenderFrame.Id)*pR.RollbackEstimatedDtNanos), + MeleeBullets: nextRenderFrameMeleeBullets, + } } func (pR *Room) decodeInput(encodedInput uint64) *InputFrameDecoded { @@ -1613,6 +1637,7 @@ func (pR *Room) refreshColliders(spaceW, spaceH int32) { for _, barrier := range pR.Barriers { boundaryUnaligned := barrier.Boundary barrierCollider := GenerateConvexPolygonCollider(boundaryUnaligned, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Barrier") + barrierCollider.Data = barrier pR.Space.Add(barrierCollider) } } @@ -1776,3 +1801,25 @@ func (pR *Room) cloneInputsBuffer(stFrameId, edFrameId int32) []*InputFrameDowns return cloned } + +func (pR *Room) calcHardPushbacksNorms(playerCollider *resolv.Object, playerShape *resolv.ConvexPolygon, snapIntoPlatformOverlap float64, pEffPushback *Vec2D) []Vec2D { + ret := make([]Vec2D, 0, 10) // no one would simultaneously have more than 5 hardPushbacks + if collision := playerCollider.Check(0, 0); collision != nil { + for _, obj := range collision.Objects { + switch obj.Data.(type) { + case *Barrier: + barrierShape := obj.Shape.(*resolv.ConvexPolygon) + if overlapped, pushbackX, pushbackY, overlapResult := CalcPushbacks(0, 0, playerShape, barrierShape); overlapped { + // ALWAY snap into hardPushbacks! + // [OverlapX, OverlapY] is the unit vector that points into the platform + pushbackX, pushbackY = (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapX, (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapY + ret = append(ret, Vec2D{X: overlapResult.OverlapX, Y: overlapResult.OverlapY}) + pEffPushback.X += pushbackX + pEffPushback.Y += pushbackY + } + } + + } + } + return ret +} diff --git a/dnmshared/resolv_helper.go b/dnmshared/resolv_helper.go index 50516bb..480360e 100644 --- a/dnmshared/resolv_helper.go +++ b/dnmshared/resolv_helper.go @@ -60,43 +60,6 @@ func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceO return collider } -func CalcPushbacksWithGravitySnapping(oldDx, oldDy float64, playerShape, barrierShape *resolv.ConvexPolygon, currentInAir bool, snapIntoPlatformOverlap, snapIntoPlatformThreshold float64) (bool, float64, float64, *SatResult, float64, float64, bool) { - origX, origY := playerShape.Position() - snappedIntoPlatformEx, snappedIntoPlatformEy := float64(0), float64(0) - localFallStopping := false - defer func() { - playerShape.SetPosition(origX, origY) - }() - playerShape.SetPosition(origX+oldDx, origY+oldDy) - overlapResult := &SatResult{ - Overlap: 0, - OverlapX: 0, - OverlapY: 0, - AContainedInB: true, - BContainedInA: true, - Axis: vector.Vector{0, 0}, - } - if overlapped := IsPolygonPairOverlapped(playerShape, barrierShape, overlapResult); overlapped { - pushbackX, pushbackY := overlapResult.Overlap*overlapResult.OverlapX, overlapResult.Overlap*overlapResult.OverlapY - normAlignmentWithGravity := (overlapResult.OverlapX * 0 + overlapResult.OverlapX * (-1.0)) - flatEnough := (snapIntoPlatformThreshold < normAlignmentWithGravity) // prevents false snapping on the lateral sides - remainsNotInAir := (!currentInAir && flatEnough) - localFallStopping = (currentInAir && flatEnough) - if remainsNotInAir || localFallStopping { - // [OverlayX, OverlapY] is the unit vector that points into the platform; FIXME: Should only assign to [snappedIntoPlatformEx, snappedIntoPlatformEy] at most once! - snappedIntoPlatformEx, snappedIntoPlatformEy = -overlapResult.OverlapY, overlapResult.OverlapX - pushbackX, pushbackY = (overlapResult.Overlap - snapIntoPlatformOverlap) * overlapResult.OverlapX, (overlapResult.Overlap - snapIntoPlatformOverlap) * overlapResult.OverlapY - if (snappedIntoPlatformEx * currPlayerDownsync.dirX + snappedIntoPlatformEy * currPlayerDownsync.dirY < 0) { - // snapped dir should have a positive projection from player facing dir - snappedIntoPlatformEx, snappedIntoPlatformEy = -snappedIntoPlatformEx, -snappedIntoPlatformEy - } - } - return true, pushbackX, pushbackY, overlapResult, snappedIntoPlatformEx, snappedIntoPlatformEy, localFallStopping - } else { - return false, 0, 0, overlapResult, 0, 0, false - } -} - func CalcPushbacks(oldDx, oldDy float64, playerShape, barrierShape *resolv.ConvexPolygon) (bool, float64, float64, *SatResult) { origX, origY := playerShape.Position() defer func() { diff --git a/frontend/assets/resources/map/dungeon/map.tmx b/frontend/assets/resources/map/dungeon/map.tmx index ed64c28..bbda482 100644 --- a/frontend/assets/resources/map/dungeon/map.tmx +++ b/frontend/assets/resources/map/dungeon/map.tmx @@ -1,5 +1,5 @@ - + @@ -26,12 +26,6 @@ - - - - - - @@ -274,27 +268,22 @@ - + - + + - + - - - - - - - + diff --git a/frontend/assets/scenes/login.fire b/frontend/assets/scenes/login.fire index 8e744bd..f2acb21 100644 --- a/frontend/assets/scenes/login.fire +++ b/frontend/assets/scenes/login.fire @@ -440,7 +440,7 @@ "array": [ 0, 0, - 215.64032554232523, + 216.67520680312998, 0, 0, 0, diff --git a/frontend/assets/scenes/offline_map_1.fire b/frontend/assets/scenes/offline_map_1.fire index 81ac671..354c327 100644 --- a/frontend/assets/scenes/offline_map_1.fire +++ b/frontend/assets/scenes/offline_map_1.fire @@ -454,7 +454,7 @@ "array": [ 0, 0, - 209.57814771583418, + 216.67520680312998, 0, 0, 0, diff --git a/frontend/assets/scripts/AttackingCharacter.js b/frontend/assets/scripts/AttackingCharacter.js index b4db4e6..b7a44c5 100644 --- a/frontend/assets/scripts/AttackingCharacter.js +++ b/frontend/assets/scripts/AttackingCharacter.js @@ -128,7 +128,7 @@ cc.Class({ _interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName) { if (window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(newCharacterState)) { // No "framesToRecover" - console.warn(`#DragonBones JoinIndex=${rdfPlayer.joinIndex}, ${playingAnimName} -> ${newAnimName}`); + // console.warn(`#DragonBones JoinIndex=${rdfPlayer.joinIndex}, ${playingAnimName} -> ${newAnimName}`); underlyingAnimationCtrl.gotoAndPlayByFrame(newAnimName, 0, -1); } else { const animationData = underlyingAnimationCtrl._animations[newAnimName]; diff --git a/frontend/assets/scripts/Map.js b/frontend/assets/scripts/Map.js index af72ad9..29ee2f7 100644 --- a/frontend/assets/scripts/Map.js +++ b/frontend/assets/scripts/Map.js @@ -1079,7 +1079,7 @@ cc.Class({ velX: currPlayerDownsync.velX, velY: currPlayerDownsync.velY, characterState: currPlayerDownsync.characterState, - inAir: true, // will be updated if collided with a barrier with "0 > pushbackY" + inAir: true, speed: currPlayerDownsync.speed, battleState: currPlayerDownsync.battleState, score: currPlayerDownsync.score, @@ -1106,7 +1106,6 @@ cc.Class({ const joinIndex = parseInt(j) + 1; const playerRichInfo = self.playerRichInfoArr[j]; const playerId = playerRichInfo.id; - const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const [currPlayerDownsync, thatPlayerInNextFrame] = [currRenderFrame.players[playerId], nextRenderFramePlayers[playerId]]; if (0 < thatPlayerInNextFrame.framesToRecover) { // No need to process inputs for this player, but there might be bullet pushbacks on this player @@ -1131,23 +1130,19 @@ cc.Class({ } if (1 == decodedInput.btnALevel && 0 == prevBtnALevel) { - // console.log(`playerId=${playerId} triggered a rising-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); - if (self.bulletTriggerEnabled) { - const punchSkillId = 1; - const punch = window.pb.protos.MeleeBullet.create(self.meleeSkillConfig[punchSkillId]); - thatPlayerInNextFrame.framesToRecover = punch.recoveryFrames; - punch.battleLocalId = self.bulletBattleLocalIdCounter++; - punch.offenderJoinIndex = joinIndex; - punch.offenderPlayerId = playerId; - punch.originatedRenderFrameId = currRenderFrame.id; - nextRenderFrameMeleeBullets.push(punch); - // console.log(`A rising-edge of meleeBullet is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}: ${self._stringifyRecentInputCache(true)}`); - console.log(`A rising-edge of meleeBullet is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); + const punchSkillId = 1; + const punch = window.pb.protos.MeleeBullet.create(self.meleeSkillConfig[punchSkillId]); + thatPlayerInNextFrame.framesToRecover = punch.recoveryFrames; + punch.battleLocalId = self.bulletBattleLocalIdCounter++; + punch.offenderJoinIndex = joinIndex; + punch.offenderPlayerId = playerId; + punch.originatedRenderFrameId = currRenderFrame.id; + nextRenderFrameMeleeBullets.push(punch); + // console.log(`playerId=${playerId}, joinIndex=${joinIndex} triggered a rising-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); - thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atk1[0]; - if (false == currPlayerDownsync.inAir) { - thatPlayerInNextFrame.velX = 0; // prohibits simultaneous movement with Atk1 on the ground - } + thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atk1[0]; + if (false == currPlayerDownsync.inAir) { + thatPlayerInNextFrame.velX = 0; // prohibits simultaneous movement with Atk1 on the ground } } else if (0 == decodedInput.btnALevel && 1 == prevBtnALevel) { // console.log(`playerId=${playerId} triggered a falling-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); @@ -1215,7 +1210,6 @@ cc.Class({ newBulletCollider.data = meleeBullet; collisionSysMap.set(collisionBulletIndex, newBulletCollider); bulletColliders.set(collisionBulletIndex, newBulletCollider); - // console.log(`A meleeBullet is added to collisionSys at currRenderFrame.id=${currRenderFrame.id} as start-up frames ended and active frame is not yet ended: ${JSON.stringify(meleeBullet)}`); } } @@ -1231,35 +1225,48 @@ cc.Class({ const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); const potentials = playerCollider.potentials(); - hardPushbackNorms[joinIndex - 1] = self.calcHardPushbacksNorms(playerCollider, potentials, result, self.snapIntoPlatformThreshold, self.snapIntoPlatformOverlap, joinIndex, effPushbacks); + hardPushbackNorms[joinIndex - 1] = self.calcHardPushbacksNorms(playerCollider, potentials, result, self.snapIntoPlatformOverlap, effPushbacks[joinIndex - 1]); const [currPlayerDownsync, thatPlayerInNextFrame] = [currRenderFrame.players[playerId], nextRenderFramePlayers[playerId]]; let fallStopping = false; + let possiblyFallStoppedOnAnotherPlayer = false; for (const potential of potentials) { + let [isBarrier, isAnotherPlayer, isBullet] = [true == potential.data.hardPushback, null != potential.data.joinIndex, null != potential.data.offenderJoinIndex]; // ignore bullets for this step - if (null != potential.data && null != potential.data.offenderJoinIndex) continue; + if (isBullet) continue; // Test if the player collides with the wall/another player if (!playerCollider.collides(potential, result)) continue; const normAlignmentWithGravity = (result.overlap_x * 0 + result.overlap_y * (-1.0)); const landedOnGravityPushback = (self.snapIntoPlatformThreshold < normAlignmentWithGravity); // prevents false snapping on the lateral sides - // Push the player out of the wall/another player - let [pushbackX, pushbackY] = [result.overlap * result.overlap_x, result.overlap * result.overlap_y]; + let pushback = [result.overlap * result.overlap_x, result.overlap * result.overlap_y]; if (landedOnGravityPushback) { - // kindly note that one player might land on top of another player, and snapping is also required in such case - [pushbackX, pushbackY] = [(result.overlap - self.snapIntoPlatformOverlap) * result.overlap_x, (result.overlap - self.snapIntoPlatformOverlap) * result.overlap_y]; - } + // kindly note that one player might land on top of another player, and snapping is also required in such case + pushback = [(result.overlap - self.snapIntoPlatformOverlap) * result.overlap_x, (result.overlap - self.snapIntoPlatformOverlap) * result.overlap_y]; + thatPlayerInNextFrame.inAir = false; + } for (let hardPushbackNorm of hardPushbackNorms[joinIndex - 1]) { // remove pushback component on the directions of "hardPushbackNorms[joinIndex-1]" (by now those hardPushbacks are already accounted in "effPushbacks[joinIndex-1]") - const projectedMagnitude = pushbackX * hardPushbackNorm[0] + pushbackY * hardPushbackNorm[1]; - pushbackX -= projectedMagnitude * hardPushbackNorm[0]; - pushbackY -= projectedMagnitude * hardPushbackNorm[1]; + const projectedMagnitude = pushback[0] * hardPushbackNorm[0] + pushback[1] * hardPushbackNorm[1]; + if (isBarrier + || + (isAnotherPlayer && 0 > projectedMagnitude) + ) { + // [WARNING] Pushing by another player is different from pushing by barrier! + // Otherwise the player couldn't be pushed by another player to opposite dir of a side wall + pushback[0] -= projectedMagnitude * hardPushbackNorm[0]; + pushback[1] -= projectedMagnitude * hardPushbackNorm[1]; + } + } + if (currPlayerDownsync.inAir && landedOnGravityPushback) { + fallStopping = true; + if (isAnotherPlayer) { + possiblyFallStoppedOnAnotherPlayer = true; + } } - thatPlayerInNextFrame.inAir &= !landedOnGravityPushback; - fallStopping |= (currPlayerDownsync.inAir && landedOnGravityPushback); - effPushbacks[joinIndex - 1][0] += pushbackX; - effPushbacks[joinIndex - 1][1] += pushbackY; + effPushbacks[joinIndex - 1][0] += pushback[0]; + effPushbacks[joinIndex - 1][1] += pushback[1]; } if (fallStopping) { @@ -1267,6 +1274,9 @@ cc.Class({ thatPlayerInNextFrame.velY = 0; thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Idle1[0]; thatPlayerInNextFrame.framesToRecover = 0; + if (possiblyFallStoppedOnAnotherPlayer) { + console.log(`playerId=${playerId}, joinIndex=${joinIndex} possiblyFallStoppedOnAnotherPlayer with effPushback=${effPushbacks[joinIndex - 1]} at renderFrame.id=${currRenderFrame.id}`); + } } if (currPlayerDownsync.inAir) { thatPlayerInNextFrame.characterState = window.toInAirConjugate(thatPlayerInNextFrame.characterState); @@ -1282,21 +1292,27 @@ cc.Class({ if (null != potential.data && potential.data.joinIndex == bulletCollider.data.offenderJoinIndex) continue; if (!bulletCollider.collides(potential, result)) continue; if (null != potential.data && null != potential.data.joinIndex) { + const playerId = potential.data.id; const joinIndex = potential.data.joinIndex; let xfac = 1; if (0 > offender.dirX) { xfac = -1; } - let [pushbackX, pushbackY] = [-xfac * bulletCollider.data.pushback, 0]; // Only for straight punch, there's no y-pushback + // Only for straight punch, there's no y-pushback + let bulletPushback = [-xfac * bulletCollider.data.pushback, 0]; + // console.log(`playerId=${playerId}, joinIndex=${joinIndex} is supposed to be pushed back by meleeBullet for bulletPushback=${JSON.stringify(bulletPushback)} at renderFrame.id=${currRenderFrame.id}`); for (let hardPushbackNorm of hardPushbackNorms[joinIndex - 1]) { - // remove pushback component on the directions of "hardPushbackNorms[joinIndex-1]" (by now those hardPushbacks are already accounted in "effPushbacks[joinIndex-1]") - const projectedMagnitude = pushbackX * hardPushbackNorm[0] + pushbackY * hardPushbackNorm[1]; - pushbackX -= projectedMagnitude * hardPushbackNorm[0]; - pushbackY -= projectedMagnitude * hardPushbackNorm[1]; - // TODO: What if a bullet knocks down the attacked player into ground? + const projectedMagnitude = bulletPushback[0] * hardPushbackNorm[0] + bulletPushback[1] * hardPushbackNorm[1]; + if (0 > projectedMagnitude) { + // Otherwise when smashing into a wall the atked player would be pushed into the wall first and only got back in the next renderFrame, not what I want here + bulletPushback[0] -= (projectedMagnitude * hardPushbackNorm[0]); + bulletPushback[1] -= (projectedMagnitude * hardPushbackNorm[1]); + // console.log(`playerId=${playerId}, joinIndex=${joinIndex} reducing bulletPushback=${JSON.stringify(bulletPushback)} by ${JSON.stringify([projectedMagnitude * hardPushbackNorm[0], projectedMagnitude * hardPushbackNorm[1]])} where hardPushbackNorm=${JSON.stringify(hardPushbackNorm)}, projectedMagnitude=${projectedMagnitude} at renderFrame.id=${currRenderFrame.id}`); + } } - effPushbacks[joinIndex - 1][0] += pushbackX; - effPushbacks[joinIndex - 1][1] += pushbackY; + // console.log(`playerId=${playerId}, joinIndex=${joinIndex} is actually pushed back by meleeBullet for bulletPushback=${JSON.stringify(bulletPushback)} at renderFrame.id=${currRenderFrame.id}`); + effPushbacks[joinIndex - 1][0] += bulletPushback[0]; + effPushbacks[joinIndex - 1][1] += bulletPushback[1]; const [atkedPlayerInCurFrame, atkedPlayerInNextFrame] = [currRenderFrame.players[potential.data.id], nextRenderFramePlayers[potential.data.id]]; atkedPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atked1[0]; if (atkedPlayerInCurFrame.inAir) { @@ -1331,6 +1347,7 @@ cc.Class({ const playerId = self.playerRichInfoArr[j].id; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); + // Update "virtual grid position" const thatPlayerInNextFrame = nextRenderFramePlayers[playerId]; [thatPlayerInNextFrame.virtualGridX, thatPlayerInNextFrame.virtualGridY] = self.polygonColliderAnchorToVirtualGridPos(playerCollider.x - effPushbacks[joinIndex - 1][0], playerCollider.y - effPushbacks[joinIndex - 1][1], self.playerRichInfoArr[j].colliderRadius, self.playerRichInfoArr[j].colliderRadius); } @@ -1478,21 +1495,20 @@ cc.Class({ return self.worldToPolygonColliderAnchorPos(wx, wy, halfBoundingW, halfBoundingH) }, - calcHardPushbacksNorms(collider, potentials, result, snapIntoPlatformThreshold, snapIntoPlatformOverlap, joinIndex, effPushbacks) { - let hardPushbackNorms = []; - let fallStopping = false; + calcHardPushbacksNorms(collider, potentials, result, snapIntoPlatformOverlap, effPushback) { + let ret = []; for (const potential of potentials) { if (null == potential.data || !(true == potential.data.hardPushback)) continue; if (!collider.collides(potential, result)) continue; - // allow snapping into all colliders with {hardPushback: true} + // ALWAY snap into hardPushbacks! + // [overlay_x, overlap_y] is the unit vector that points into the platform const [pushbackX, pushbackY] = [(result.overlap - snapIntoPlatformOverlap) * result.overlap_x, (result.overlap - snapIntoPlatformOverlap) * result.overlap_y]; - // [overlay_x, overlap_y] is the unit vector that points into the platform; FIXME: Should only assign to [snappedIntoPlatformEx, snappedIntoPlatformEy] at most once! - hardPushbackNorms.push([result.overlap_x, result.overlap_y]); - effPushbacks[joinIndex - 1][0] += pushbackX; - effPushbacks[joinIndex - 1][1] += pushbackY; + ret.push([result.overlap_x, result.overlap_y]); + effPushback[0] += pushbackX; + effPushback[1] += pushbackY; } - return hardPushbackNorms; + return ret; }, }); diff --git a/frontend/assets/scripts/OfflineMap.js b/frontend/assets/scripts/OfflineMap.js index 0db08cd..5dcfb3e 100644 --- a/frontend/assets/scripts/OfflineMap.js +++ b/frontend/assets/scripts/OfflineMap.js @@ -75,7 +75,7 @@ cc.Class({ Moreover, "snapIntoPlatformOverlap" should be small enough such that the walking "velX" or jumping initial "velY" can escape from it by 1 renderFrame (when jumping is triggered, the character is waived from snappig for 1 renderFrame). */ - self.snapIntoPlatformOverlap = 0.1; + self.snapIntoPlatformOverlap = 0.01; self.snapIntoPlatformThreshold = 0.5; // a platform must be "horizontal enough" for a character to "stand on" self.jumpingInitVelY = 6 * self.worldToVirtualGridRatio; // unit: (virtual grid length/renderFrame) [self.gravityX, self.gravityY] = [0, -Math.ceil(4 * self.jumpingInitVelY / self.serverFps)]; // unit: (virtual grid length/renderFrame^2)