diff --git a/README.md b/README.md index 429e0ea..46df014 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md). -_(the following gif is sped up to 2x for file size reduction)_ -![gif_demo](./charts/melee_attack_2.gif) +_(the following gif is sped up to ~1.5x for file size reduction, kindly note that around ~11s countdown, the attack animation is resumed from a partial progress)_ +![gif_demo](./charts/melee_attack_fractional_anim_resume_spedup.gif) -Please also checkout [this demo video](https://pan.baidu.com/s/1fy0CuFKnVP_Gn2cDfrj6yg?pwd=q5uc) to see how this demo carries out a full 60fps synchronization with the help of _batched input upsync/downsync_ for satisfying network I/O performance. +Please also checkout [this demo video](https://pan.baidu.com/s/1U1wb7KWyHorZElNWcS5HHA?pwd=30wh) to see how this demo carries out a full 60fps synchronization with the help of _batched input upsync/downsync_ for satisfying network I/O performance. The video mainly shows the following features. - The backend receives inputs from frontend peers and broadcasts back for synchronization. diff --git a/charts/melee_attack_2.gif b/charts/melee_attack_2.gif deleted file mode 100644 index 88da9b8..0000000 Binary files a/charts/melee_attack_2.gif and /dev/null differ diff --git a/charts/melee_attack_fractional_anim_resume_spedup.gif b/charts/melee_attack_fractional_anim_resume_spedup.gif new file mode 100644 index 0000000..201c6f5 Binary files /dev/null and b/charts/melee_attack_fractional_anim_resume_spedup.gif differ diff --git a/frontend/assets/scenes/login.fire b/frontend/assets/scenes/login.fire index 5cbb696..76a6bfa 100644 --- a/frontend/assets/scenes/login.fire +++ b/frontend/assets/scenes/login.fire @@ -440,7 +440,7 @@ "array": [ 0, 0, - 210.4441731196186, + 216.50635094610968, 0, 0, 0, diff --git a/frontend/assets/scenes/offline_map_1.fire b/frontend/assets/scenes/offline_map_1.fire index 0ac0a62..4ce7f71 100644 --- a/frontend/assets/scenes/offline_map_1.fire +++ b/frontend/assets/scenes/offline_map_1.fire @@ -454,7 +454,7 @@ "array": [ 0, 0, - 210.4441731196186, + 216.50635094610968, 0, 0, 0, diff --git a/frontend/assets/scripts/AttackingCharacter.js b/frontend/assets/scripts/AttackingCharacter.js index dc13938..a396ca0 100644 --- a/frontend/assets/scripts/AttackingCharacter.js +++ b/frontend/assets/scripts/AttackingCharacter.js @@ -32,7 +32,7 @@ cc.Class({ this.speciesName = speciesName; this.effAnimNode = this.animNode.getChildByName(this.speciesName); this.animComp = this.effAnimNode.getComponent(dragonBones.ArmatureDisplay); - this.animComp.playAnimation(ATK_CHARACTER_STATE.Idle1[1]); + this.animComp.playAnimation(ATK_CHARACTER_STATE.Idle1[1]); // [WARNING] This is the only exception ccc's wrapper is used! this.effAnimNode.active = true; }, @@ -41,6 +41,7 @@ cc.Class({ }, updateCharacterAnim(rdfPlayer, prevRdfPlayer, forceAnimSwitch) { + const underlyingAnimationCtrl = this.animComp._armature.animation; // ALWAYS use the dragonBones api instead of ccc's wrapper! // Update directions if (this.animComp && this.animComp.node) { if (0 > rdfPlayer.dirX) { @@ -54,46 +55,47 @@ cc.Class({ let newCharacterState = rdfPlayer.characterState; let prevCharacterState = (null == prevRdfPlayer ? window.ATK_CHARACTER_STATE.Idle1[0] : prevRdfPlayer.characterState); const newAnimName = window.ATK_CHARACTER_STATE_ARR[newCharacterState][1]; + const playingAnimName = underlyingAnimationCtrl.lastAnimationName; + const isPlaying = underlyingAnimationCtrl.isPlaying; // As this function might be called after many frames of a rollback, it's possible that the playing animation was predicted, different from "prevCharacterState" but same as "newCharacterState". More granular checks are needed to determine whether we should interrupt the playing animation. if (newCharacterState != prevCharacterState) { - if (newAnimName == this.animComp.animationName) { + if (newAnimName == playingAnimName) { if (ATK_CHARACTER_STATE.Idle1[0] == newCharacterState || ATK_CHARACTER_STATE.Walking[0] == newCharacterState) { - if (false == this.animComp._playing) { - this.animComp.playAnimation(newAnimName); - } // No need to interrupt - // console.warn(`JoinIndex=${rdfPlayer.joinIndex}, not interrupting ${newAnimName} while the playing anim is also ${this.animComp.animationName}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, , to: ${JSON.stringify(rdfPlayer)}`); + // console.warn(`JoinIndex=${rdfPlayer.joinIndex}, not interrupting ${newAnimName} while the playing anim is also ${playingAnimName}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, , to: ${JSON.stringify(rdfPlayer)}`); return; } } - this._interruptPlayingAnimAndPlayNewAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName); + this._interruptPlayingAnimAndPlayNewAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl); } else { // newCharacterState == prevCharacterState - if (newAnimName != this.animComp.animationName) { + if (newAnimName != playingAnimName) { // the playing animation was falsely predicted - this._interruptPlayingAnimAndPlayNewAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName); + this._interruptPlayingAnimAndPlayNewAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl); + } else { + if (!(ATK_CHARACTER_STATE.Idle1[0] == newCharacterState || ATK_CHARACTER_STATE.Walking[0] == newCharacterState)) { + // yet there's still a chance that the playing anim is not put at the current frame + this._interruptPlayingAnimAndPlayNewAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl); + } } - // TODO: What if (newAnimName == this.animComp.animationName) but (false == this.animComp._playing) by now? Do we just force it to play from beginning or use "this._interruptPlayingAnimAndPlayNewAnim"? } }, - _interruptPlayingAnimAndPlayNewAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName) { + _interruptPlayingAnimAndPlayNewAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl) { if (ATK_CHARACTER_STATE.Idle1[0] == newCharacterState || ATK_CHARACTER_STATE.Walking[0] == newCharacterState) { // No "framesToRecover" - // console.warn(`JoinIndex=${rdfPlayer.joinIndex}, playing new ${newAnimName} from the beginning: while the playing anim is ${this.animComp.animationName}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, , to: ${JSON.stringify(rdfPlayer)}`); - this.animComp.playAnimation(newAnimName); + // console.warn(`JoinIndex=${rdfPlayer.joinIndex}, playing new ${newAnimName} from the beginning: while the playing anim is ${playAnimation}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, , to: ${JSON.stringify(rdfPlayer)}`); + underlyingAnimationCtrl.gotoAndPlayByFrame(newAnimName, 0, -1); } else { - const animationData = this.animComp._armature.animation._animations[newAnimName]; + const animationData = underlyingAnimationCtrl._animations[newAnimName]; let fromAnimFrame = (animationData.frameCount - rdfPlayer.framesToRecover); if (fromAnimFrame > 0) { - // console.warn(`JoinIndex=${rdfPlayer.joinIndex}, playing ${newAnimName} from the middle: rdfPlayer.framesToRecover=${rdfPlayer.framesToRecover} while the playing anim is ${this.animComp.animationName}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}`); } else if (fromAnimFrame < 0) { // For Atk1 or Atk2, it's possible that the "meleeBullet.recoveryFrames" is configured to be slightly larger than corresponding animation duration frames fromAnimFrame = 0; } - // console.warn(`JoinIndex=${rdfPlayer.joinIndex}, playing ${newAnimName} from the middle: fromAnimFrame=${fromAnimFrame}, animFrameCount=${animationData.frameCount}, rdfPlayer.framesToRecover=${rdfPlayer.framesToRecover} while the playing anim is ${this.animComp.animationName}`); - this.animComp._armature.animation.gotoAndPlayByFrame(newAnimName, fromAnimFrame); + underlyingAnimationCtrl.gotoAndPlayByFrame(newAnimName, fromAnimFrame, 1); } }, });