Draft version for jumping with backend sync.

This commit is contained in:
genxium 2022-12-13 18:10:30 +08:00
parent 5a463239bb
commit c171ebc211
9 changed files with 342 additions and 323 deletions

View File

@ -17,12 +17,16 @@ func toPbPlayers(modelInstances map[int32]*Player, withMetaInfo bool) map[int32]
VirtualGridY: last.VirtualGridY, VirtualGridY: last.VirtualGridY,
DirX: last.DirX, DirX: last.DirX,
DirY: last.DirY, DirY: last.DirY,
ColliderRadius: last.ColliderRadius, VelX: last.VelX,
VelY: last.VelY,
Speed: last.Speed, Speed: last.Speed,
BattleState: last.BattleState, BattleState: last.BattleState,
CharacterState: last.CharacterState,
InAir: last.InAir,
JoinIndex: last.JoinIndex,
ColliderRadius: last.ColliderRadius,
Score: last.Score, Score: last.Score,
Removed: last.Removed, Removed: last.Removed,
JoinIndex: last.JoinIndex,
} }
if withMetaInfo { if withMetaInfo {
toRet[k].Name = last.Name toRet[k].Name = last.Name

View File

@ -201,6 +201,7 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocke
pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK
pPlayerFromDbInit.Speed = pR.PlayerDefaultSpeed // Hardcoded pPlayerFromDbInit.Speed = pR.PlayerDefaultSpeed // Hardcoded
pPlayerFromDbInit.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded pPlayerFromDbInit.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
pPlayerFromDbInit.InAir = true // Hardcoded
pR.Players[playerId] = pPlayerFromDbInit pR.Players[playerId] = pPlayerFromDbInit
pR.PlayerDownsyncSessionDict[playerId] = session 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.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK
pEffectiveInRoomPlayerInstance.Speed = pR.PlayerDefaultSpeed // Hardcoded pEffectiveInRoomPlayerInstance.Speed = pR.PlayerDefaultSpeed // Hardcoded
pEffectiveInRoomPlayerInstance.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded pEffectiveInRoomPlayerInstance.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
pEffectiveInRoomPlayerInstance.InAir = true // Hardcoded
pR.PlayerDownsyncSessionDict[playerId] = session pR.PlayerDownsyncSessionDict[playerId] = session
pR.PlayerSignalToCloseDict[playerId] = signalToCloseConnOfThisPlayer pR.PlayerSignalToCloseDict[playerId] = signalToCloseConnOfThisPlayer
@ -422,7 +424,7 @@ func (pR *Room) StartBattle() {
stCalculation := utils.UnixtimeNano() stCalculation := utils.UnixtimeNano()
elapsedNanosSinceLastFrameIdTriggered := stCalculation - pR.LastRenderFrameIdTriggeredAt elapsedNanosSinceLastFrameIdTriggered := stCalculation - pR.LastRenderFrameIdTriggeredAt
if elapsedNanosSinceLastFrameIdTriggered < pR.RollbackEstimatedDtNanos { 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 { if pR.RenderFrameId > pR.BattleDurationFrames {
@ -566,12 +568,12 @@ func (pR *Room) OnBattleCmdReceived(pReq *WsReq) {
atomic.StoreInt32(&(player.AckingFrameId), ackingFrameId) atomic.StoreInt32(&(player.AckingFrameId), ackingFrameId)
atomic.StoreInt32(&(player.AckingInputFrameId), ackingInputFrameId) 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() 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() { defer func() {
pR.InputsBufferLock.Unlock() 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) inputsBufferSnapshot := pR.markConfirmationIfApplicable(inputFrameUpsyncBatch, playerId, player)
@ -757,7 +759,7 @@ func (pR *Room) OnDismissed() {
pR.InputFrameUpsyncDelayTolerance = 2 pR.InputFrameUpsyncDelayTolerance = 2
pR.MaxChasingRenderFramesPerUpdate = 8 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) punchSkillId := int32(1)
pR.MeleeSkillConfig = make(map[int32]*MeleeBullet, 0) pR.MeleeSkillConfig = make(map[int32]*MeleeBullet, 0)
pR.MeleeSkillConfig[punchSkillId] = &MeleeBullet{ pR.MeleeSkillConfig[punchSkillId] = &MeleeBullet{
@ -1286,7 +1288,10 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF
VirtualGridY: currPlayerDownsync.VirtualGridY, VirtualGridY: currPlayerDownsync.VirtualGridY,
DirX: currPlayerDownsync.DirX, DirX: currPlayerDownsync.DirX,
DirY: currPlayerDownsync.DirY, DirY: currPlayerDownsync.DirY,
VelX: currPlayerDownsync.VelX,
VelY: currPlayerDownsync.VelY,
CharacterState: currPlayerDownsync.CharacterState, CharacterState: currPlayerDownsync.CharacterState,
InAir: true,
Speed: currPlayerDownsync.Speed, Speed: currPlayerDownsync.Speed,
BattleState: currPlayerDownsync.BattleState, BattleState: currPlayerDownsync.BattleState,
Score: currPlayerDownsync.Score, Score: currPlayerDownsync.Score,
@ -1301,47 +1306,106 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF
} }
} }
toRet := &RoomDownsyncFrame{ 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?
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?
}
// Guaranteed determinism regardless of traversal order // 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) 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 { for _, player := range pR.PlayersArr {
playerId := player.Id playerId := player.Id
joinIndex := player.JoinIndex 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 collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := collisionSysMap[collisionPlayerIndex] 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 { if currPlayerDownsync.InAir {
thatPlayerInNextFrame.VelX += pR.GravityX thatPlayerInNextFrame.VelX += pR.GravityX
thatPlayerInNextFrame.VelY += pR.GravityY 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 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) removedBulletsAtCurrFrame := make(map[int32]int32, 0)
for _, meleeBullet := range currRenderFrame.MeleeBullets { for _, meleeBullet := range currRenderFrame.MeleeBullets {
@ -1368,141 +1432,70 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF
} }
} }
for _, bulletCollider := range bulletColliders { // 4. Invoke collision system stepping (no-op for backend collision lib)
shouldRemove := false
meleeBullet := bulletCollider.Data.(*MeleeBullet) // 5. Calc pushbacks for each player (after its movement) w/o bullets
collisionBulletIndex := COLLISION_BULLET_INDEX_PREFIX + meleeBullet.BattleLocalId for _, player := range pR.PlayersArr {
bulletShape := bulletCollider.Shape.(*resolv.ConvexPolygon) joinIndex := player.JoinIndex
if collision := bulletCollider.Check(0, 0); collision != nil { playerId := player.Id
offender := currRenderFrame.Players[meleeBullet.OffenderPlayerId] 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 { for _, obj := range collision.Objects {
defenderShape := obj.Shape.(*resolv.ConvexPolygon) isBarrier, isAnotherPlayer, isBullet := false, false, false
switch t := obj.Data.(type) { switch obj.Data.(type) {
case *Barrier:
isBarrier = true
case *Player: case *Player:
if meleeBullet.OffenderPlayerId != t.Id { isAnotherPlayer = true
if overlapped, _, _, _ := CalcPushbacks(0, 0, bulletShape, defenderShape); overlapped { case *MeleeBullet:
xfac := float64(1.0) // By now, straight Punch offset doesn't respect "y-axis" isBullet = true
if 0 > offender.DirX { }
xfac = float64(-1.0) if isBullet {
} // ignore bullets for this step
bulletPushbacks[t.JoinIndex-1].X += xfac * meleeBullet.Pushback continue
thatAckedPlayerInCurFrame := currRenderFrame.Players[t.Id] }
thatAckedPlayerInNextFrame := nextRenderFramePlayers[t.Id] bShape := obj.Shape.(*resolv.ConvexPolygon)
thatAckedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_ATKED1 if overlapped, pushbackX, pushbackY, overlapResult := CalcPushbacks(0, 0, playerShape, bShape); overlapped {
if thatAckedPlayerInCurFrame.InAir { normAlignmentWithGravity := (overlapResult.OverlapX*float64(0) + overlapResult.OverlapY*float64(-1.0))
thatAckedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_INAIR_ATKED1 landedOnGravityPushback := (pR.SnapIntoPlatformThreshold < normAlignmentWithGravity) // prevents false snapping on the lateral sides
} if landedOnGravityPushback {
oldFramesToRecover := nextRenderFramePlayers[t.Id].FramesToRecover // kindly note that one player might land on top of another player, and snapping is also required in such case
if meleeBullet.HitStunFrames > oldFramesToRecover { pushbackX, pushbackY = (overlapResult.Overlap-pR.SnapIntoPlatformOverlap)*overlapResult.OverlapX, (overlapResult.Overlap-pR.SnapIntoPlatformOverlap)*overlapResult.OverlapY
nextRenderFramePlayers[t.Id].FramesToRecover = meleeBullet.HitStunFrames thatPlayerInNextFrame.InAir = false
} }
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))) 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: if currPlayerDownsync.InAir && landedOnGravityPushback {
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)) fallStopping = true
if isAnotherPlayer {
possiblyFallStoppedOnAnotherPlayer = true
}
}
effPushbacks[joinIndex-1].X += pushbackX
effPushbacks[joinIndex-1].Y += pushbackY
} }
} }
shouldRemove = true if fallStopping {
} thatPlayerInNextFrame.VelX = 0
if shouldRemove { thatPlayerInNextFrame.VelY = 0
removedBulletsAtCurrFrame[collisionBulletIndex] = 1 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))
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 currPlayerDownsync.InAir { 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 { func (pR *Room) decodeInput(encodedInput uint64) *InputFrameDecoded {
@ -1613,6 +1637,7 @@ func (pR *Room) refreshColliders(spaceW, spaceH int32) {
for _, barrier := range pR.Barriers { for _, barrier := range pR.Barriers {
boundaryUnaligned := barrier.Boundary boundaryUnaligned := barrier.Boundary
barrierCollider := GenerateConvexPolygonCollider(boundaryUnaligned, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Barrier") barrierCollider := GenerateConvexPolygonCollider(boundaryUnaligned, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Barrier")
barrierCollider.Data = barrier
pR.Space.Add(barrierCollider) pR.Space.Add(barrierCollider)
} }
} }
@ -1776,3 +1801,25 @@ func (pR *Room) cloneInputsBuffer(stFrameId, edFrameId int32) []*InputFrameDowns
return cloned 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
}

View File

@ -60,43 +60,6 @@ func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceO
return collider 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) { func CalcPushbacks(oldDx, oldDy float64, playerShape, barrierShape *resolv.ConvexPolygon) (bool, float64, float64, *SatResult) {
origX, origY := playerShape.Position() origX, origY := playerShape.Position()
defer func() { defer func() {

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="128" height="128" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="79"> <map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="128" height="128" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="83">
<tileset firstgid="1" source="tiles0.tsx"/> <tileset firstgid="1" source="tiles0.tsx"/>
<tileset firstgid="65" source="tiles1.tsx"/> <tileset firstgid="65" source="tiles1.tsx"/>
<tileset firstgid="129" source="tiles2.tsx"/> <tileset firstgid="129" source="tiles2.tsx"/>
@ -26,12 +26,6 @@
</properties> </properties>
<polyline points="0,0 0,18.6667 1041.33,21.3333 1041.33,-1.33333"/> <polyline points="0,0 0,18.6667 1041.33,21.3333 1041.33,-1.33333"/>
</object> </object>
<object id="9" x="650.667" y="1604.67">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,18.6667 1041.33,21.3333 1041.33,-1.33333"/>
</object>
<object id="10" x="634.485" y="505.455"> <object id="10" x="634.485" y="505.455">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
@ -274,27 +268,22 @@
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
</object> </object>
<object id="75" x="1264.25" y="1599" width="47.75" height="17.25"> <object id="79" x="624.25" y="1616" width="1094.75" height="17.25">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
</object> </object>
<object id="76" x="1264.63" y="1583.63" width="15.5" height="15.25"> <object id="80" x="1296.5" y="1600.25">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
<polyline points="0,0 -32.5,0 -32.25,-16.5 -16.5,-16.5"/>
</object> </object>
<object id="77" x="1280.5" y="1583.75"> <object id="82" x="1328.5" y="1616">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
<polyline points="0,0 0,15.25 15.75,15.25"/> <polyline points="0,0 -64.5,0 -64.0038,-15.75 -16.4734,-15.75"/>
</object>
<object id="78" x="1312" y="1599.25">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,17 15.75,17"/>
</object> </object>
</objectgroup> </objectgroup>
</map> </map>

View File

@ -440,7 +440,7 @@
"array": [ "array": [
0, 0,
0, 0,
215.64032554232523, 216.67520680312998,
0, 0,
0, 0,
0, 0,

View File

@ -454,7 +454,7 @@
"array": [ "array": [
0, 0,
0, 0,
209.57814771583418, 216.67520680312998,
0, 0,
0, 0,
0, 0,

View File

@ -128,7 +128,7 @@ cc.Class({
_interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName) { _interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName) {
if (window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(newCharacterState)) { if (window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(newCharacterState)) {
// No "framesToRecover" // No "framesToRecover"
console.warn(`#DragonBones JoinIndex=${rdfPlayer.joinIndex}, ${playingAnimName} -> ${newAnimName}`); // console.warn(`#DragonBones JoinIndex=${rdfPlayer.joinIndex}, ${playingAnimName} -> ${newAnimName}`);
underlyingAnimationCtrl.gotoAndPlayByFrame(newAnimName, 0, -1); underlyingAnimationCtrl.gotoAndPlayByFrame(newAnimName, 0, -1);
} else { } else {
const animationData = underlyingAnimationCtrl._animations[newAnimName]; const animationData = underlyingAnimationCtrl._animations[newAnimName];

View File

@ -1079,7 +1079,7 @@ cc.Class({
velX: currPlayerDownsync.velX, velX: currPlayerDownsync.velX,
velY: currPlayerDownsync.velY, velY: currPlayerDownsync.velY,
characterState: currPlayerDownsync.characterState, characterState: currPlayerDownsync.characterState,
inAir: true, // will be updated if collided with a barrier with "0 > pushbackY" inAir: true,
speed: currPlayerDownsync.speed, speed: currPlayerDownsync.speed,
battleState: currPlayerDownsync.battleState, battleState: currPlayerDownsync.battleState,
score: currPlayerDownsync.score, score: currPlayerDownsync.score,
@ -1106,7 +1106,6 @@ cc.Class({
const joinIndex = parseInt(j) + 1; const joinIndex = parseInt(j) + 1;
const playerRichInfo = self.playerRichInfoArr[j]; const playerRichInfo = self.playerRichInfoArr[j];
const playerId = playerRichInfo.id; const playerId = playerRichInfo.id;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const [currPlayerDownsync, thatPlayerInNextFrame] = [currRenderFrame.players[playerId], nextRenderFramePlayers[playerId]]; const [currPlayerDownsync, thatPlayerInNextFrame] = [currRenderFrame.players[playerId], nextRenderFramePlayers[playerId]];
if (0 < thatPlayerInNextFrame.framesToRecover) { if (0 < thatPlayerInNextFrame.framesToRecover) {
// No need to process inputs for this player, but there might be bullet pushbacks on this player // 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) { 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}`); const punchSkillId = 1;
if (self.bulletTriggerEnabled) { const punch = window.pb.protos.MeleeBullet.create(self.meleeSkillConfig[punchSkillId]);
const punchSkillId = 1; thatPlayerInNextFrame.framesToRecover = punch.recoveryFrames;
const punch = window.pb.protos.MeleeBullet.create(self.meleeSkillConfig[punchSkillId]); punch.battleLocalId = self.bulletBattleLocalIdCounter++;
thatPlayerInNextFrame.framesToRecover = punch.recoveryFrames; punch.offenderJoinIndex = joinIndex;
punch.battleLocalId = self.bulletBattleLocalIdCounter++; punch.offenderPlayerId = playerId;
punch.offenderJoinIndex = joinIndex; punch.originatedRenderFrameId = currRenderFrame.id;
punch.offenderPlayerId = playerId; nextRenderFrameMeleeBullets.push(punch);
punch.originatedRenderFrameId = currRenderFrame.id; // console.log(`playerId=${playerId}, joinIndex=${joinIndex} triggered a rising-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`);
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}`);
thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atk1[0]; thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atk1[0];
if (false == currPlayerDownsync.inAir) { if (false == currPlayerDownsync.inAir) {
thatPlayerInNextFrame.velX = 0; // prohibits simultaneous movement with Atk1 on the ground thatPlayerInNextFrame.velX = 0; // prohibits simultaneous movement with Atk1 on the ground
}
} }
} else if (0 == decodedInput.btnALevel && 1 == prevBtnALevel) { } 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}`); // 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; newBulletCollider.data = meleeBullet;
collisionSysMap.set(collisionBulletIndex, newBulletCollider); collisionSysMap.set(collisionBulletIndex, newBulletCollider);
bulletColliders.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 collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex); const playerCollider = collisionSysMap.get(collisionPlayerIndex);
const potentials = playerCollider.potentials(); 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]]; const [currPlayerDownsync, thatPlayerInNextFrame] = [currRenderFrame.players[playerId], nextRenderFramePlayers[playerId]];
let fallStopping = false; let fallStopping = false;
let possiblyFallStoppedOnAnotherPlayer = false;
for (const potential of potentials) { 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 // 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 // Test if the player collides with the wall/another player
if (!playerCollider.collides(potential, result)) continue; if (!playerCollider.collides(potential, result)) continue;
const normAlignmentWithGravity = (result.overlap_x * 0 + result.overlap_y * (-1.0)); const normAlignmentWithGravity = (result.overlap_x * 0 + result.overlap_y * (-1.0));
const landedOnGravityPushback = (self.snapIntoPlatformThreshold < normAlignmentWithGravity); // prevents false snapping on the lateral sides const landedOnGravityPushback = (self.snapIntoPlatformThreshold < normAlignmentWithGravity); // prevents false snapping on the lateral sides
// Push the player out of the wall/another player let pushback = [result.overlap * result.overlap_x, result.overlap * result.overlap_y];
let [pushbackX, pushbackY] = [result.overlap * result.overlap_x, result.overlap * result.overlap_y];
if (landedOnGravityPushback) { if (landedOnGravityPushback) {
// kindly note that one player might land on top of another player, and snapping is also required in such case // 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]; 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]) { 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]") // 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]; const projectedMagnitude = pushback[0] * hardPushbackNorm[0] + pushback[1] * hardPushbackNorm[1];
pushbackX -= projectedMagnitude * hardPushbackNorm[0]; if (isBarrier
pushbackY -= projectedMagnitude * hardPushbackNorm[1]; ||
(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][0] += pushback[0];
effPushbacks[joinIndex - 1][1] += pushbackY; effPushbacks[joinIndex - 1][1] += pushback[1];
} }
if (fallStopping) { if (fallStopping) {
@ -1267,6 +1274,9 @@ cc.Class({
thatPlayerInNextFrame.velY = 0; thatPlayerInNextFrame.velY = 0;
thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Idle1[0]; thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Idle1[0];
thatPlayerInNextFrame.framesToRecover = 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) { if (currPlayerDownsync.inAir) {
thatPlayerInNextFrame.characterState = window.toInAirConjugate(thatPlayerInNextFrame.characterState); thatPlayerInNextFrame.characterState = window.toInAirConjugate(thatPlayerInNextFrame.characterState);
@ -1282,21 +1292,27 @@ cc.Class({
if (null != potential.data && potential.data.joinIndex == bulletCollider.data.offenderJoinIndex) continue; if (null != potential.data && potential.data.joinIndex == bulletCollider.data.offenderJoinIndex) continue;
if (!bulletCollider.collides(potential, result)) continue; if (!bulletCollider.collides(potential, result)) continue;
if (null != potential.data && null != potential.data.joinIndex) { if (null != potential.data && null != potential.data.joinIndex) {
const playerId = potential.data.id;
const joinIndex = potential.data.joinIndex; const joinIndex = potential.data.joinIndex;
let xfac = 1; let xfac = 1;
if (0 > offender.dirX) { if (0 > offender.dirX) {
xfac = -1; 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]) { 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 = bulletPushback[0] * hardPushbackNorm[0] + bulletPushback[1] * hardPushbackNorm[1];
const projectedMagnitude = pushbackX * hardPushbackNorm[0] + pushbackY * hardPushbackNorm[1]; if (0 > projectedMagnitude) {
pushbackX -= projectedMagnitude * hardPushbackNorm[0]; // 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
pushbackY -= projectedMagnitude * hardPushbackNorm[1]; bulletPushback[0] -= (projectedMagnitude * hardPushbackNorm[0]);
// TODO: What if a bullet knocks down the attacked player into ground? 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; // 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][1] += pushbackY; effPushbacks[joinIndex - 1][0] += bulletPushback[0];
effPushbacks[joinIndex - 1][1] += bulletPushback[1];
const [atkedPlayerInCurFrame, atkedPlayerInNextFrame] = [currRenderFrame.players[potential.data.id], nextRenderFramePlayers[potential.data.id]]; const [atkedPlayerInCurFrame, atkedPlayerInNextFrame] = [currRenderFrame.players[potential.data.id], nextRenderFramePlayers[potential.data.id]];
atkedPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atked1[0]; atkedPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atked1[0];
if (atkedPlayerInCurFrame.inAir) { if (atkedPlayerInCurFrame.inAir) {
@ -1331,6 +1347,7 @@ cc.Class({
const playerId = self.playerRichInfoArr[j].id; const playerId = self.playerRichInfoArr[j].id;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex); const playerCollider = collisionSysMap.get(collisionPlayerIndex);
// Update "virtual grid position"
const thatPlayerInNextFrame = nextRenderFramePlayers[playerId]; 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); [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) return self.worldToPolygonColliderAnchorPos(wx, wy, halfBoundingW, halfBoundingH)
}, },
calcHardPushbacksNorms(collider, potentials, result, snapIntoPlatformThreshold, snapIntoPlatformOverlap, joinIndex, effPushbacks) { calcHardPushbacksNorms(collider, potentials, result, snapIntoPlatformOverlap, effPushback) {
let hardPushbackNorms = []; let ret = [];
let fallStopping = false;
for (const potential of potentials) { for (const potential of potentials) {
if (null == potential.data || !(true == potential.data.hardPushback)) continue; if (null == potential.data || !(true == potential.data.hardPushback)) continue;
if (!collider.collides(potential, result)) 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]; 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! ret.push([result.overlap_x, result.overlap_y]);
hardPushbackNorms.push([result.overlap_x, result.overlap_y]); effPushback[0] += pushbackX;
effPushbacks[joinIndex - 1][0] += pushbackX; effPushback[1] += pushbackY;
effPushbacks[joinIndex - 1][1] += pushbackY;
} }
return hardPushbackNorms; return ret;
}, },
}); });

View File

@ -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). 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.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.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) [self.gravityX, self.gravityY] = [0, -Math.ceil(4 * self.jumpingInitVelY / self.serverFps)]; // unit: (virtual grid length/renderFrame^2)